feat(全局): 部分组件调整&bug修复
This commit is contained in:
parent
59c2116b03
commit
a8676365f6
Binary file not shown.
|
@ -183,7 +183,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||||
// authenticationScheme: 'Bearer',
|
// authenticationScheme: 'Bearer',
|
||||||
authenticationScheme: '',
|
authenticationScheme: '',
|
||||||
baseURL: `${window.location.origin}/${import.meta.env.VITE_API_BASE_URL as string}`,
|
baseURL: `${window.location.origin}/${import.meta.env.VITE_API_BASE_URL as string}`,
|
||||||
timeout: 60 * 1000,
|
timeout: 120 * 1000,
|
||||||
headers: { 'Content-Type': ContentTypeEnum.JSON },
|
headers: { 'Content-Type': ContentTypeEnum.JSON },
|
||||||
// 如果是form-data格式
|
// 如果是form-data格式
|
||||||
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
|
||||||
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
|
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
|
||||||
|
|
||||||
import MSR from '@/api/http/index';
|
import MSR from '@/api/http/index';
|
||||||
|
@ -53,7 +52,6 @@ import {
|
||||||
GetRecycleCaseListUrl,
|
GetRecycleCaseListUrl,
|
||||||
GetRecycleCaseModulesCountUrl,
|
GetRecycleCaseModulesCountUrl,
|
||||||
GetReviewCommentListUrl,
|
GetReviewCommentListUrl,
|
||||||
GetReviewerListUrl,
|
|
||||||
GetSearchCustomFieldsUrl,
|
GetSearchCustomFieldsUrl,
|
||||||
GetThirdDemandUrl,
|
GetThirdDemandUrl,
|
||||||
getTransferTreeUrl,
|
getTransferTreeUrl,
|
||||||
|
@ -86,7 +84,6 @@ import type {
|
||||||
CreateOrUpdateModule,
|
CreateOrUpdateModule,
|
||||||
DeleteCaseType,
|
DeleteCaseType,
|
||||||
DemandItem,
|
DemandItem,
|
||||||
DetailCase,
|
|
||||||
DragCase,
|
DragCase,
|
||||||
ImportExcelType,
|
ImportExcelType,
|
||||||
ModulesTreeType,
|
ModulesTreeType,
|
||||||
|
@ -95,7 +92,7 @@ import type {
|
||||||
UpdateModule,
|
UpdateModule,
|
||||||
} from '@/models/caseManagement/featureCase';
|
} from '@/models/caseManagement/featureCase';
|
||||||
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
|
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
|
||||||
import type { UserListItem } from '@/models/setting/user';
|
|
||||||
// 获取模块树
|
// 获取模块树
|
||||||
export function getCaseModuleTree(params: TableQueryParams) {
|
export function getCaseModuleTree(params: TableQueryParams) {
|
||||||
return MSR.get<ModulesTreeType[]>({ url: `${GetCaseModuleTreeUrl}/${params.projectId}` });
|
return MSR.get<ModulesTreeType[]>({ url: `${GetCaseModuleTreeUrl}/${params.projectId}` });
|
||||||
|
@ -311,11 +308,6 @@ export function getDetailCaseReviewPage(data: TableQueryParams) {
|
||||||
return MSR.post<CommonList<CaseManagementTable>>({ url: GetDetailCaseReviewUrl, data });
|
return MSR.post<CommonList<CaseManagementTable>>({ url: GetDetailCaseReviewUrl, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取评审人列表
|
|
||||||
export function getReviewerList(projectId: string, keyword: string) {
|
|
||||||
return MSR.get<UserListItem[]>({ url: `${GetReviewerListUrl}/${projectId}`, params: { keyword } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用例接口用例分页列表
|
// 用例接口用例分页列表
|
||||||
export function getPublicLinkCaseList(data: TableQueryParams) {
|
export function getPublicLinkCaseList(data: TableQueryParams) {
|
||||||
return MSR.post<CommonList<CaseManagementTable>>({ url: GetAssociationPublicCasePageUrl, data });
|
return MSR.post<CommonList<CaseManagementTable>>({ url: GetAssociationPublicCasePageUrl, data });
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { ProjectListUrl, ProjectSwitchUrl } from '@/api/requrls/project-manageme
|
||||||
import type { ProjectListItem } from '@/models/setting/project';
|
import type { ProjectListItem } from '@/models/setting/project';
|
||||||
|
|
||||||
export function getProjectList(organizationId: string) {
|
export function getProjectList(organizationId: string) {
|
||||||
return MSR.get<ProjectListItem[]>({ url: ProjectListUrl, params: organizationId });
|
return MSR.get<ProjectListItem[]>({ url: ProjectListUrl, params: organizationId }, { ignoreCancelToken: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchProject(data: { projectId: string; userId: string }) {
|
export function switchProject(data: { projectId: string; userId: string }) {
|
||||||
|
|
|
@ -5,11 +5,13 @@ import {
|
||||||
BatchRemoveMemberUrl,
|
BatchRemoveMemberUrl,
|
||||||
EditProjectMemberUrl,
|
EditProjectMemberUrl,
|
||||||
GetProjectMemberListUrl,
|
GetProjectMemberListUrl,
|
||||||
|
ProjectMemberCommentOptions,
|
||||||
ProjectMemberOptions,
|
ProjectMemberOptions,
|
||||||
ProjectUserGroupUrl,
|
ProjectUserGroupUrl,
|
||||||
RemoveProjectMemberUrl,
|
RemoveProjectMemberUrl,
|
||||||
} from '@/api/requrls/project-management/projectMember';
|
} from '@/api/requrls/project-management/projectMember';
|
||||||
|
|
||||||
|
import { ReviewUserItem } from '@/models/caseManagement/caseReview';
|
||||||
import type { CommonList, TableQueryParams } from '@/models/common';
|
import type { CommonList, TableQueryParams } from '@/models/common';
|
||||||
import type { ActionProjectMember, ProjectMemberItem } from '@/models/projectManagement/projectAndPermission';
|
import type { ActionProjectMember, ProjectMemberItem } from '@/models/projectManagement/projectAndPermission';
|
||||||
|
|
||||||
|
@ -50,3 +52,11 @@ export function getProjectUserGroup(projectId: string) {
|
||||||
export function getProjectMemberOptions(projectId: string, keyword?: string) {
|
export function getProjectMemberOptions(projectId: string, keyword?: string) {
|
||||||
return MSR.get({ url: `${ProjectMemberOptions}/${projectId}`, params: { keyword } });
|
return MSR.get({ url: `${ProjectMemberOptions}/${projectId}`, params: { keyword } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 项目成员-@成员下拉选项
|
||||||
|
export function getProjectMemberCommentOptions(projectId: string, keyword?: string) {
|
||||||
|
return MSR.get<ReviewUserItem[]>({
|
||||||
|
url: `${ProjectMemberCommentOptions}/${projectId}`,
|
||||||
|
params: { keyword },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type { CommonList, TableQueryParams } from '@/models/common';
|
||||||
import type {
|
import type {
|
||||||
BatchAddParams,
|
BatchAddParams,
|
||||||
CreateUserParams,
|
CreateUserParams,
|
||||||
|
CreateUserResult,
|
||||||
DeleteUserParams,
|
DeleteUserParams,
|
||||||
ImportResult,
|
ImportResult,
|
||||||
ImportUserParams,
|
ImportUserParams,
|
||||||
|
@ -35,6 +36,8 @@ import type {
|
||||||
UserListItem,
|
UserListItem,
|
||||||
} from '@/models/setting/user';
|
} from '@/models/setting/user';
|
||||||
|
|
||||||
|
import { Result } from '#/axios';
|
||||||
|
|
||||||
// 获取用户列表
|
// 获取用户列表
|
||||||
export function getUserList(data: TableQueryParams) {
|
export function getUserList(data: TableQueryParams) {
|
||||||
return MSR.post<CommonList<UserListItem>>({ url: GetUserListUrl, data });
|
return MSR.post<CommonList<UserListItem>>({ url: GetUserListUrl, data });
|
||||||
|
@ -42,7 +45,7 @@ export function getUserList(data: TableQueryParams) {
|
||||||
|
|
||||||
// 批量创建用户
|
// 批量创建用户
|
||||||
export function batchCreateUser(data: CreateUserParams) {
|
export function batchCreateUser(data: CreateUserParams) {
|
||||||
return MSR.post({ url: CreateUserUrl, data });
|
return MSR.post<CreateUserResult>({ url: CreateUserUrl, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
|
@ -68,7 +71,7 @@ export function deleteUserInfo(data: DeleteUserParams) {
|
||||||
|
|
||||||
// 导入用户
|
// 导入用户
|
||||||
export function importUserInfo(data: ImportUserParams) {
|
export function importUserInfo(data: ImportUserParams) {
|
||||||
return MSR.uploadFile<ImportResult>({ url: ImportUserUrl }, data);
|
return MSR.uploadFile<Result<ImportResult>>({ url: ImportUserUrl }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取系统用户组
|
// 获取系统用户组
|
||||||
|
|
|
@ -104,8 +104,6 @@ export const UpdateCommentItemUrl = '/functional/case/comment/update';
|
||||||
export const DeleteCommentItemUrl = '/functional/case/comment/delete';
|
export const DeleteCommentItemUrl = '/functional/case/comment/delete';
|
||||||
// 获取详情用例评审
|
// 获取详情用例评审
|
||||||
export const GetDetailCaseReviewUrl = '/functional/case/review/page';
|
export const GetDetailCaseReviewUrl = '/functional/case/review/page';
|
||||||
// 获取有权限的评审人
|
|
||||||
export const GetReviewerListUrl = '/case/review/user-option';
|
|
||||||
// 获取用例详情弹窗关联用例接口用例
|
// 获取用例详情弹窗关联用例接口用例
|
||||||
export const GetAssociationPublicCasePageUrl = '/functional/case/test/associate/case/page';
|
export const GetAssociationPublicCasePageUrl = '/functional/case/test/associate/case/page';
|
||||||
// 获取接口测试接口模块数量
|
// 获取接口测试接口模块数量
|
||||||
|
|
|
@ -6,3 +6,4 @@ export const RemoveProjectMemberUrl = '/project/member/remove';
|
||||||
export const BatchAddUserGroup = '/project/member/add-role';
|
export const BatchAddUserGroup = '/project/member/add-role';
|
||||||
export const ProjectUserGroupUrl = '/project/member/get-role/option';
|
export const ProjectUserGroupUrl = '/project/member/get-role/option';
|
||||||
export const ProjectMemberOptions = '/project/member/get-member/option';
|
export const ProjectMemberOptions = '/project/member/get-member/option';
|
||||||
|
export const ProjectMemberCommentOptions = '/project/member/comment/user-option'; // 项目成员-@成员下拉列表
|
||||||
|
|
|
@ -213,6 +213,9 @@
|
||||||
.btn-outline-danger-active();
|
.btn-outline-danger-active();
|
||||||
.btn-outline-danger-disabled();
|
.btn-outline-danger-disabled();
|
||||||
}
|
}
|
||||||
|
.arco-btn-size-mini {
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/** 输入框,选择器,文本域 **/
|
/** 输入框,选择器,文本域 **/
|
||||||
.arco-select {
|
.arco-select {
|
||||||
|
@ -311,6 +314,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.arco-input-suffix {
|
||||||
|
.arco-icon-hover {
|
||||||
|
.arco-icon-eye-invisible,
|
||||||
|
.arco-icon-eye {
|
||||||
|
color: var(--color-text-brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.arco-textarea {
|
.arco-textarea {
|
||||||
.ms-scroll-bar();
|
.ms-scroll-bar();
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="!props.hideAdd"
|
||||||
v-show="form.list.length > 1"
|
v-show="form.list.length > 1"
|
||||||
class="minus"
|
class="minus"
|
||||||
:class="[
|
:class="[
|
||||||
|
@ -169,6 +170,7 @@
|
||||||
|
|
||||||
import type { FormItemModel, FormMode } from './types';
|
import type { FormItemModel, FormMode } from './types';
|
||||||
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
|
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
|
||||||
|
import { FieldData } from '@arco-design/web-vue/es/form/interface';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -182,10 +184,12 @@
|
||||||
isShowDrag?: boolean; // 是否可以拖拽
|
isShowDrag?: boolean; // 是否可以拖拽
|
||||||
formWidth?: string; // 自定义表单区域宽度
|
formWidth?: string; // 自定义表单区域宽度
|
||||||
showEnable?: boolean; // 是否显示启用禁用switch状态
|
showEnable?: boolean; // 是否显示启用禁用switch状态
|
||||||
|
hideAdd?: boolean; // 是否隐藏添加按钮
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
maxHeight: '30vh',
|
maxHeight: '30vh',
|
||||||
isShowDrag: false,
|
isShowDrag: false,
|
||||||
|
hideAdd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -292,10 +296,15 @@
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFields(data: Record<string, FieldData>) {
|
||||||
|
formRef.value?.setFields(data);
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
formValidate,
|
formValidate,
|
||||||
getFormResult,
|
getFormResult,
|
||||||
resetForm,
|
resetForm,
|
||||||
|
setFields,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{{ props.prefix }}
|
{{ props.prefix }}
|
||||||
</template>
|
</template>
|
||||||
<template #label="{ data }">
|
<template #label="{ data }">
|
||||||
<a-tooltip :content="getInputLabel(data)" position="top" :mouse-enter-delay="500" mini>
|
<a-tooltip :content="getInputLabelTooltip(data)" position="top" :mouse-enter-delay="500" mini>
|
||||||
<div class="one-line-text inline-block">{{ getInputLabel(data) }}</div>
|
<div class="one-line-text inline-block">{{ getInputLabel(data) }}</div>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #label="{ data }">
|
<template #label="{ data }">
|
||||||
<slot name="label" :data="{ ...data, [props.labelKey]: getInputLabel(data) }">
|
<slot name="label" :data="{ ...data, [props.labelKey]: getInputLabel(data) }">
|
||||||
<a-tooltip :content="getInputLabel(data)" position="top" :mouse-enter-delay="500" mini>
|
<a-tooltip :content="getInputLabelTooltip(data)" position="top" :mouse-enter-delay="500" mini>
|
||||||
<div class="one-line-text inline translate-y-[15%]">
|
<div class="one-line-text inline translate-y-[15%]">
|
||||||
{{ getInputLabel(data) }}
|
{{ getInputLabel(data) }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,6 +85,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, ref, watch } from 'vue';
|
import { Ref, ref, watch } from 'vue';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useSelect from '@/hooks/useSelect';
|
import useSelect from '@/hooks/useSelect';
|
||||||
|
@ -125,8 +126,9 @@
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerValue = ref<CascaderModelValue>([]);
|
const innerValue = ref<CascaderModelValue>([]);
|
||||||
const innerLevel = ref(''); // 顶级选项,该级别为单选选项
|
const innerLevel = useVModel(props, 'level', emit); // 顶级选项,该级别为单选选项
|
||||||
const cascader: Ref = ref(null);
|
const cascader: Ref = ref(null);
|
||||||
|
let selectedLabelObj: Record<string, any> = {}; // 存储已选的选项的label,用于多选时展示全部选项的 tooltip
|
||||||
|
|
||||||
const { maxTagCount, getOptionComputedStyle, calculateMaxTag } = useSelect({
|
const { maxTagCount, getOptionComputedStyle, calculateMaxTag } = useSelect({
|
||||||
selectRef: cascader,
|
selectRef: cascader,
|
||||||
|
@ -158,6 +160,16 @@
|
||||||
watch(
|
watch(
|
||||||
() => innerValue.value,
|
() => innerValue.value,
|
||||||
(val) => {
|
(val) => {
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
// 选项变化时,清理一次已选的选项的label对象
|
||||||
|
const tempObj: Record<string, any> = {};
|
||||||
|
for (let i = 0; i < val.length; i++) {
|
||||||
|
if (selectedLabelObj[val[i]]) {
|
||||||
|
tempObj[val[i]] = selectedLabelObj[val[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedLabelObj = { ...tempObj };
|
||||||
|
}
|
||||||
emit('update:modelValue', val);
|
emit('update:modelValue', val);
|
||||||
if (val === '') {
|
if (val === '') {
|
||||||
innerLevel.value = '';
|
innerLevel.value = '';
|
||||||
|
@ -166,23 +178,6 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.level,
|
|
||||||
(val) => {
|
|
||||||
innerLevel.value = val || '';
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => innerLevel.value,
|
|
||||||
(val) => {
|
|
||||||
emit('update:level', val);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
interface CascaderValue {
|
interface CascaderValue {
|
||||||
level: keyof typeof props.levelTop;
|
level: keyof typeof props.levelTop;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -213,13 +208,25 @@
|
||||||
calculateMaxTag();
|
calculateMaxTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 临时解决 arco-design 的 cascader 组件绑定值只能是 path-mode 的问题,如果实际值也包含了 ‘-’,则不要取这个值,而是取绑定的 v-model 的值
|
// TODO: 临时解决 arco-design 的 cascader 组件已选项的label只能是带路径‘/’的 path-mode 的问题
|
||||||
function getInputLabel(data: CascaderOption) {
|
function getInputLabel(data: CascaderOption) {
|
||||||
const isTagCount = data[props.labelKey].includes('+');
|
const isTagCount = data[props.labelKey].includes('+');
|
||||||
if (!props.pathMode) {
|
if (!props.pathMode) {
|
||||||
return isTagCount ? data[props.labelKey] : t(data[props.labelKey].split('-').pop());
|
return isTagCount ? data.label : t((data.label || '').split('/').pop() || ''); // 取路径最后一级
|
||||||
}
|
}
|
||||||
return isTagCount ? data[props.labelKey] : t(data[props.labelKey]);
|
return isTagCount ? data.label || '' : t(data.label || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputLabelTooltip(data: CascaderOption) {
|
||||||
|
const isTagCount = data[props.labelKey].includes('+');
|
||||||
|
if (isTagCount && Array.isArray(innerValue.value)) {
|
||||||
|
return Object.values(selectedLabelObj).join(',');
|
||||||
|
}
|
||||||
|
const label = getInputLabel(data);
|
||||||
|
if (data.value && typeof data.value === 'string') {
|
||||||
|
selectedLabelObj[data.value] = label;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearValues() {
|
function clearValues() {
|
||||||
|
|
|
@ -360,10 +360,8 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getTableList = computed(() => props.getTableFunc);
|
|
||||||
|
|
||||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||||
getTableList.value,
|
props.getTableFunc,
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
showSetting: false,
|
showSetting: false,
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
import { openWindow, regexUrl } from '@/utils';
|
import { openWindow, regexUrl } from '@/utils';
|
||||||
import { listenerRouteChange } from '@/utils/route-listener';
|
import { listenerRouteChange } from '@/utils/route-listener';
|
||||||
|
|
||||||
import { WorkbenchRouteEnum } from '@/enums/routeEnum';
|
|
||||||
|
|
||||||
import useMenuTree from './use-menu-tree';
|
import useMenuTree from './use-menu-tree';
|
||||||
import type { RouteMeta } from 'vue-router';
|
import type { RouteMeta } from 'vue-router';
|
||||||
|
|
||||||
|
@ -125,15 +123,22 @@
|
||||||
|
|
||||||
async function switchOrg(id: string) {
|
async function switchOrg(id: string) {
|
||||||
try {
|
try {
|
||||||
Message.loading(t('personal.switchOrgLoading'));
|
appStore.showLoading(t('personal.switchOrgLoading'));
|
||||||
await switchUserOrg(id, userStore.id || '');
|
await switchUserOrg(id, userStore.id || '');
|
||||||
switchOrgVisible.value = false;
|
switchOrgVisible.value = false;
|
||||||
Message.clear();
|
appStore.hideLoading();
|
||||||
Message.success(t('personal.switchOrgSuccess'));
|
Message.success(t('personal.switchOrgSuccess'));
|
||||||
personalMenusVisible.value = false;
|
personalMenusVisible.value = false;
|
||||||
orgKeyword.value = '';
|
orgKeyword.value = '';
|
||||||
await router.replace({ name: WorkbenchRouteEnum.WORKBENCH });
|
await userStore.isLogin(true);
|
||||||
userStore.isLogin();
|
router.replace({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
organizationId: appStore.currentOrgId,
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -212,7 +217,7 @@
|
||||||
return (
|
return (
|
||||||
<a-trigger
|
<a-trigger
|
||||||
v-model:popup-visible={personalMenusVisible.value}
|
v-model:popup-visible={personalMenusVisible.value}
|
||||||
trigger="click"
|
trigger="hover"
|
||||||
unmount-on-close={false}
|
unmount-on-close={false}
|
||||||
popup-offset={4}
|
popup-offset={4}
|
||||||
position="right"
|
position="right"
|
||||||
|
@ -309,7 +314,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a-menu-item class="flex items-center justify-between" key="personalInfo">
|
<a-menu-item class="flex items-center justify-between" key="personalInfo">
|
||||||
<div class="flex items-center gap-[8px] hover:!bg-transparent">
|
<div class="relative flex items-center gap-[8px] hover:!bg-transparent">
|
||||||
<MsAvatar avatar={userStore.avatar} size={20} />
|
<MsAvatar avatar={userStore.avatar} size={20} />
|
||||||
{userStore.name}
|
{userStore.name}
|
||||||
</div>
|
</div>
|
||||||
|
@ -427,6 +432,7 @@
|
||||||
@apply !bg-transparent;
|
@apply !bg-transparent;
|
||||||
}
|
}
|
||||||
.arco-menu-icon {
|
.arco-menu-icon {
|
||||||
|
margin-right: 8px;
|
||||||
.arco-icon {
|
.arco-icon {
|
||||||
&:not(.arco-icon-down) {
|
&:not(.arco-icon-down) {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
@ -456,6 +462,9 @@
|
||||||
}
|
}
|
||||||
.arco-menu-pop {
|
.arco-menu-pop {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(var(--primary-1)) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -464,6 +473,10 @@
|
||||||
}
|
}
|
||||||
.arco-menu-collapsed {
|
.arco-menu-collapsed {
|
||||||
width: 86px;
|
width: 86px;
|
||||||
|
.arco-avatar,
|
||||||
|
.arco-icon {
|
||||||
|
margin-right: 2px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.arco-menu {
|
.arco-menu {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -484,6 +497,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.arco-menu-item-tooltip {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
.switch-org-dropdown {
|
.switch-org-dropdown {
|
||||||
@apply absolute max-h-none;
|
@apply absolute max-h-none;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<a-radio v-for="(option, i) of group.options || []" :key="`option${i}`" :value="option.value">
|
<a-radio v-for="(option, i) of group.options || []" :key="`option${i}`" :value="option.value">
|
||||||
{{ t(option.label) }}
|
{{ t(option.label || '') }}
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<a-date-picker
|
<a-date-picker
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
</div>
|
</div>
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="timeModalVisible"
|
v-model:visible="timeModalVisible"
|
||||||
:title="t('ms.personal.changeAvatar')"
|
:title="t('ms.personal.setValidTime')"
|
||||||
title-align="start"
|
title-align="start"
|
||||||
:ok-text="t('common.save')"
|
:ok-text="t('common.save')"
|
||||||
class="ms-usemodal"
|
class="ms-usemodal"
|
||||||
|
|
|
@ -253,11 +253,20 @@
|
||||||
|
|
||||||
const avatarModalVisible = ref(false);
|
const avatarModalVisible = ref(false);
|
||||||
const avatarList = ref<string[]>([]);
|
const avatarList = ref<string[]>([]);
|
||||||
let i = 1;
|
|
||||||
while (i <= 46) {
|
watch(
|
||||||
avatarList.value.push(`/images/avatar/avatar-${i}.jpg`);
|
() => avatarModalVisible.value,
|
||||||
i++;
|
(val) => {
|
||||||
}
|
if (val && avatarList.value.length === 0) {
|
||||||
|
// 初始化头像列表,避免一开始就加载所有头像
|
||||||
|
let i = 1;
|
||||||
|
while (i <= 46) {
|
||||||
|
avatarList.value.push(`/images/avatar/avatar-${i}.jpg`);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function changeAvatar(avatar: string) {
|
function changeAvatar(avatar: string) {
|
||||||
activeAvatar.value = avatar;
|
activeAvatar.value = avatar;
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
import MsMenuPanel from '@/components/pure/ms-menu-panel/index.vue';
|
import MsMenuPanel from '@/components/pure/ms-menu-panel/index.vue';
|
||||||
import apiKey from './components/apiKey.vue';
|
import apiKey from './components/apiKey.vue';
|
||||||
|
@ -41,27 +43,9 @@
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerVisible = ref(false);
|
const innerVisible = useVModel(props, 'visible', emit);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.visible,
|
|
||||||
(val) => {
|
|
||||||
innerVisible.value = val;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeMenu = ref('baseInfo');
|
const activeMenu = ref('baseInfo');
|
||||||
|
|
||||||
watch(
|
|
||||||
() => innerVisible.value,
|
|
||||||
(val) => {
|
|
||||||
emit('update:visible', val);
|
|
||||||
if (!val) {
|
|
||||||
activeMenu.value = 'baseInfo';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const menuList = ref([
|
const menuList = ref([
|
||||||
{
|
{
|
||||||
name: 'personal',
|
name: 'personal',
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
||||||
'ms.personal.avatar': 'Avatar{index}',
|
'ms.personal.avatar': 'Avatar{index}',
|
||||||
'ms.personal.currentPsw': 'Current Password',
|
'ms.personal.currentPsw': 'Current Password',
|
||||||
'ms.personal.newPsw': 'New Password',
|
'ms.personal.newPsw': 'New Password',
|
||||||
'ms.personal.changePswTip': 'After changing the password, you need to use the new email to log in to the system',
|
'ms.personal.changePswTip': 'After changing the password, you need to use the new password to log in to the system',
|
||||||
'ms.personal.updatePswSuccess':
|
'ms.personal.updatePswSuccess':
|
||||||
'The password has been modified successfully and will automatically log out in {count} seconds. Please log in with the new password.',
|
'The password has been modified successfully and will automatically log out in {count} seconds. Please log in with the new password.',
|
||||||
'ms.personal.apiKeyTip': 'After adding, you can access MeterSphere',
|
'ms.personal.apiKeyTip': 'After adding, you can access MeterSphere',
|
||||||
|
@ -33,6 +33,7 @@ export default {
|
||||||
'ms.personal.expired': 'Expired',
|
'ms.personal.expired': 'Expired',
|
||||||
'ms.personal.expiredTip': 'The expiration time can be changed in [Settings]',
|
'ms.personal.expiredTip': 'The expiration time can be changed in [Settings]',
|
||||||
'ms.personal.validTime': 'Effective time',
|
'ms.personal.validTime': 'Effective time',
|
||||||
|
'ms.personal.setValidTime': 'Set effective time',
|
||||||
'ms.personal.desc': 'Description',
|
'ms.personal.desc': 'Description',
|
||||||
'ms.personal.createTime': 'Created time',
|
'ms.personal.createTime': 'Created time',
|
||||||
'ms.personal.copySuccess': 'Copied successfully',
|
'ms.personal.copySuccess': 'Copied successfully',
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default {
|
||||||
'ms.personal.apiKey': 'APIKEY',
|
'ms.personal.apiKey': 'APIKEY',
|
||||||
'ms.personal.tripartite': '三方平台账号',
|
'ms.personal.tripartite': '三方平台账号',
|
||||||
'ms.personal.changeAvatar': '更换头像',
|
'ms.personal.changeAvatar': '更换头像',
|
||||||
'ms.personal.name': '用户名称',
|
'ms.personal.name': '用户姓名',
|
||||||
'ms.personal.namePlaceholder': '请输入用户名称',
|
'ms.personal.namePlaceholder': '请输入用户名称',
|
||||||
'ms.personal.nameRequired': '用户名称不能为空',
|
'ms.personal.nameRequired': '用户名称不能为空',
|
||||||
'ms.personal.email': '邮箱',
|
'ms.personal.email': '邮箱',
|
||||||
|
@ -24,13 +24,14 @@ export default {
|
||||||
'ms.personal.avatar': '头像{index}',
|
'ms.personal.avatar': '头像{index}',
|
||||||
'ms.personal.currentPsw': '当前密码',
|
'ms.personal.currentPsw': '当前密码',
|
||||||
'ms.personal.newPsw': '新密码',
|
'ms.personal.newPsw': '新密码',
|
||||||
'ms.personal.changePswTip': '修改密码后,需要使用新的邮箱登录系统',
|
'ms.personal.changePswTip': '修改密码后,需要使用新的密码登录系统',
|
||||||
'ms.personal.updatePswSuccess': '密码修改成功,将在 {count} 秒后自动退出,请使用新密码登录',
|
'ms.personal.updatePswSuccess': '密码修改成功,将在 {count} 秒后自动退出,请使用新密码登录',
|
||||||
'ms.personal.apiKeyTip': '新增后,可访问 MeterSphere',
|
'ms.personal.apiKeyTip': '新增后,可访问 MeterSphere',
|
||||||
'ms.personal.expireTime': '过期时间',
|
'ms.personal.expireTime': '过期时间',
|
||||||
'ms.personal.expired': '已到期',
|
'ms.personal.expired': '已到期',
|
||||||
'ms.personal.expiredTip': '可在【设置】内更改到期时间',
|
'ms.personal.expiredTip': '可在【设置】内更改到期时间',
|
||||||
'ms.personal.validTime': '有效时间',
|
'ms.personal.validTime': '有效时间',
|
||||||
|
'ms.personal.setValidTime': '设置有效时间',
|
||||||
'ms.personal.desc': '描述',
|
'ms.personal.desc': '描述',
|
||||||
'ms.personal.createTime': '创建时间',
|
'ms.personal.createTime': '创建时间',
|
||||||
'ms.personal.copySuccess': '复制成功',
|
'ms.personal.copySuccess': '复制成功',
|
||||||
|
|
|
@ -36,9 +36,13 @@ export interface MsSearchSelectProps {
|
||||||
triggerProps?: TriggerProps; // 触发器属性
|
triggerProps?: TriggerProps; // 触发器属性
|
||||||
loading?: boolean; // 加载状态
|
loading?: boolean; // 加载状态
|
||||||
fallbackOption?: boolean | ((value: string | number | boolean | Record<string, unknown>) => SelectOptionData); // 自定义值中不存在的选项
|
fallbackOption?: boolean | ((value: string | number | boolean | Record<string, unknown>) => SelectOptionData); // 自定义值中不存在的选项
|
||||||
|
shouldCalculateMaxTag?: boolean; // 是否需要计算最大展示选项数量
|
||||||
|
disabled?: boolean; // 是否禁用
|
||||||
|
size?: 'mini' | 'small' | 'medium' | 'large'; // 尺寸
|
||||||
remoteFunc?(params: Record<string, any>): Promise<any>; // 远程模式下的请求函数,返回一个 Promise
|
remoteFunc?(params: Record<string, any>): Promise<any>; // 远程模式下的请求函数,返回一个 Promise
|
||||||
optionLabelRender?: (item: SelectOptionData) => string; // 自定义 option 的 label 渲染,返回一个 html 字符串,默认使用 item.label
|
optionLabelRender?: (item: SelectOptionData) => string; // 自定义 option 的 label 渲染,返回一个 html 字符串,默认使用 item.label
|
||||||
optionTooltipContent?: (item: SelectOptionData) => string; // 自定义 option 的 tooltip 内容,返回一个字符串,默认使用 item.label
|
optionTooltipContent?: (item: SelectOptionData) => string; // 自定义 option 的 tooltip 内容,返回一个字符串,默认使用 item.label
|
||||||
|
remoteFilterFunc?: (options: SelectOptionData[]) => SelectOptionData[]; // 自定义过滤函数,会在远程请求返回数据后执行
|
||||||
}
|
}
|
||||||
export interface RadioProps {
|
export interface RadioProps {
|
||||||
options: SelectOptionData[];
|
options: SelectOptionData[];
|
||||||
|
@ -61,6 +65,8 @@ export default defineComponent(
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerValue = ref(props.modelValue);
|
const innerValue = ref(props.modelValue);
|
||||||
|
const inputValue = ref('');
|
||||||
|
const tempInputValue = ref('');
|
||||||
const filterOptions = ref<SelectOptionData[]>([...props.options]); // 实际渲染的 options,会根据搜索关键字进行过滤
|
const filterOptions = ref<SelectOptionData[]>([...props.options]); // 实际渲染的 options,会根据搜索关键字进行过滤
|
||||||
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]); // 远程模式下的原始 options,接口返回的数据会存储在这里
|
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]); // 远程模式下的原始 options,接口返回的数据会存储在这里
|
||||||
|
|
||||||
|
@ -68,7 +74,7 @@ export default defineComponent(
|
||||||
const { maxTagCount, getOptionComputedStyle, singleTagMaxWidth, calculateMaxTag } = useSelect({
|
const { maxTagCount, getOptionComputedStyle, singleTagMaxWidth, calculateMaxTag } = useSelect({
|
||||||
selectRef,
|
selectRef,
|
||||||
selectVal: innerValue,
|
selectVal: innerValue,
|
||||||
isCascade: true,
|
isCascade: false,
|
||||||
valueKey: props.valueKey,
|
valueKey: props.valueKey,
|
||||||
labelKey: props.labelKey,
|
labelKey: props.labelKey,
|
||||||
});
|
});
|
||||||
|
@ -77,8 +83,8 @@ export default defineComponent(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(val) => {
|
(val) => {
|
||||||
innerValue.value = val;
|
innerValue.value = val;
|
||||||
if (Array.isArray(val) && val.length > 0 && props.multiple) {
|
if (props.shouldCalculateMaxTag !== false && props.multiple) {
|
||||||
calculateMaxTag(remoteOriginOptions.value);
|
calculateMaxTag();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -139,6 +145,9 @@ export default defineComponent(
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if (props.remoteFilterFunc && typeof props.remoteFilterFunc === 'function') {
|
||||||
|
remoteOriginOptions.value = props.remoteFilterFunc(remoteOriginOptions.value);
|
||||||
|
}
|
||||||
emit('remoteSearch', remoteOriginOptions.value);
|
emit('remoteSearch', remoteOriginOptions.value);
|
||||||
}
|
}
|
||||||
if (val.trim() === '') {
|
if (val.trim() === '') {
|
||||||
|
@ -161,10 +170,10 @@ export default defineComponent(
|
||||||
for (let i = 0; i < props.searchKeys.length; i++) {
|
for (let i = 0; i < props.searchKeys.length; i++) {
|
||||||
// 遍历传入的搜索字段
|
// 遍历传入的搜索字段
|
||||||
const key = props.searchKeys[i];
|
const key = props.searchKeys[i];
|
||||||
if (e[key].includes(val)) {
|
if (e[key]?.toLowerCase().includes(val.toLowerCase())) {
|
||||||
// 是否匹配
|
// 是否匹配
|
||||||
hasMatch = true;
|
hasMatch = true;
|
||||||
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
item[props.labelKey || 'label'] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,6 +189,9 @@ export default defineComponent(
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter((e) => e) as SelectOptionData[];
|
.filter((e) => e) as SelectOptionData[];
|
||||||
|
if (props.shouldCalculateMaxTag !== false && props.multiple) {
|
||||||
|
calculateMaxTag();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -188,13 +200,14 @@ export default defineComponent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionItemLabelRender = (item: SelectOptionData) =>
|
const optionItemLabelRender = (item: SelectOptionData) => {
|
||||||
h('div', {
|
return h('div', {
|
||||||
innerHTML:
|
innerHTML:
|
||||||
typeof props.optionLabelRender === 'function'
|
typeof props.optionLabelRender === 'function'
|
||||||
? props.optionLabelRender(item)
|
? props.optionLabelRender(item)
|
||||||
: item[props.labelKey || 'label'],
|
: item[props.labelKey || 'label'],
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 半选状态
|
// 半选状态
|
||||||
const indeterminate = computed(() => {
|
const indeterminate = computed(() => {
|
||||||
|
@ -238,17 +251,24 @@ export default defineComponent(
|
||||||
handleSelectAllChange(true);
|
handleSelectAllChange(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
calculateMaxTag(val);
|
if (props.shouldCalculateMaxTag !== false && props.multiple) {
|
||||||
|
calculateMaxTag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function getOptionItemDisabled(item: SelectOptionData) {
|
function getOptionItemDisabled(item: SelectOptionData) {
|
||||||
return (
|
return (
|
||||||
!!props.multiple &&
|
!!item.disabled ||
|
||||||
!!props.atLeastOne &&
|
(!!props.multiple &&
|
||||||
Array.isArray(innerValue.value) &&
|
!!props.atLeastOne &&
|
||||||
!!innerValue.value.find((e) => e[props.valueKey || 'value'] === item[props.valueKey || 'value']) &&
|
Array.isArray(innerValue.value) &&
|
||||||
innerValue.value.length === 1
|
!!innerValue.value.find((e) =>
|
||||||
|
props.objectValue
|
||||||
|
? e[props.valueKey || 'value'] === item[props.valueKey || 'value']
|
||||||
|
: e === item[props.valueKey || 'value']
|
||||||
|
) &&
|
||||||
|
innerValue.value.length === 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,57 +360,110 @@ export default defineComponent(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理选择器值变化
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function handleChange(value: ModelType) {
|
||||||
|
if (props.multiple) {
|
||||||
|
nextTick(() => {
|
||||||
|
inputValue.value = tempInputValue.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
emit('change', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理输入框搜索值变化
|
||||||
|
* @param val 搜索值
|
||||||
|
*/
|
||||||
|
function handleInputValueChange(val: string) {
|
||||||
|
inputValue.value = val;
|
||||||
|
if (val !== '') {
|
||||||
|
// 只存储有值的搜索值,因为当搜索完选中一个选项后,arco-select 会自动清空输入框,这里需要过滤掉
|
||||||
|
tempInputValue.value = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFullTooltip = computed(() => {
|
||||||
|
const values = Array.isArray(innerValue.value) ? innerValue.value : [innerValue.value];
|
||||||
|
let tooltip = '';
|
||||||
|
if (props.objectValue) {
|
||||||
|
// 对象模式下,直接取 tooltipContent 或 label(若搜索的情况下会携带高亮代码,所以优先取tooltipContent)
|
||||||
|
tooltip = values.map((e) => e.tooltipContent || e[props.labelKey || 'label']).join(',');
|
||||||
|
} else {
|
||||||
|
// 非对象模式下,需要根据 valueKey 取 label
|
||||||
|
tooltip = remoteOriginOptions.value
|
||||||
|
.filter((e) => values.includes(e[props.valueKey || 'value']))
|
||||||
|
.map((e) => e[props.labelKey || 'label'])
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
return tooltip;
|
||||||
|
});
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<a-select
|
<div class="w-full">
|
||||||
ref={selectRef}
|
<a-tooltip content={selectFullTooltip.value} position="top" mouse-enter-delay={500} mini>
|
||||||
default-value={innerValue}
|
<a-select
|
||||||
placeholder={t(props.placeholder || '')}
|
ref={selectRef}
|
||||||
allow-clear={props.allowClear}
|
class="ms-select"
|
||||||
allow-search={props.allowSearch}
|
default-value={innerValue}
|
||||||
filter-option={false}
|
input-value={inputValue.value}
|
||||||
loading={loading.value}
|
placeholder={t(props.placeholder || '')}
|
||||||
multiple={props.multiple}
|
allow-clear={props.allowClear}
|
||||||
max-tag-count={maxTagCount.value}
|
allow-search={props.allowSearch}
|
||||||
search-delay={300}
|
filter-option={true}
|
||||||
class="ms-select"
|
loading={loading.value}
|
||||||
value-key={props.valueKey || 'value'}
|
multiple={props.multiple}
|
||||||
popup-container={props.popupContainer || document.body}
|
max-tag-count={maxTagCount.value}
|
||||||
trigger-props={props.triggerProps}
|
search-delay={300}
|
||||||
fallback-option={props.fallbackOption}
|
value-key={props.valueKey || 'value'}
|
||||||
onChange={(value: ModelType) => {
|
popup-container={props.popupContainer || document.body}
|
||||||
emit('update:modelValue', value);
|
trigger-props={props.triggerProps}
|
||||||
emit('change', value);
|
fallback-option={props.fallbackOption}
|
||||||
}}
|
disabled={props.disabled}
|
||||||
onSearch={handleSearch}
|
size={props.size}
|
||||||
onPopupVisibleChange={(val: boolean) => {
|
onChange={handleChange}
|
||||||
handleSearch('', true);
|
onSearch={handleSearch}
|
||||||
emit('popupVisibleChange', val);
|
onPopupVisibleChange={(val: boolean) => {
|
||||||
}}
|
if (val) {
|
||||||
onRemove={(val: string | number | boolean | Record<string, any> | undefined) => emit('remove', val)}
|
handleSearch('', true);
|
||||||
onKeyup={(e: KeyboardEvent) => {
|
} else {
|
||||||
// 阻止组件在回车时自动触发的事件
|
inputValue.value = '';
|
||||||
if (e.code === 'Enter') {
|
tempInputValue.value = '';
|
||||||
e.preventDefault();
|
}
|
||||||
e.stopPropagation();
|
emit('popupVisibleChange', val);
|
||||||
handleSearch('', true);
|
}}
|
||||||
}
|
onRemove={(val: string | number | boolean | Record<string, any> | undefined) => emit('remove', val)}
|
||||||
}}
|
onKeyup={(e: KeyboardEvent) => {
|
||||||
>
|
// 阻止组件在回车时自动触发的事件
|
||||||
{{
|
if (e.code === 'Enter') {
|
||||||
prefix: props.prefix ? () => t(props.prefix || '') : null,
|
e.preventDefault();
|
||||||
label: ({ data }: { data: SelectOptionData }) => (
|
e.stopPropagation();
|
||||||
<a-tooltip content={data.label} position="top" mouse-enter-delay={500} mini>
|
handleSearch('', true);
|
||||||
<div
|
}
|
||||||
class="one-line-text"
|
if (e.code === 'Backspace' && inputValue.value === '') {
|
||||||
style={singleTagMaxWidth.value > 0 ? { maxWidth: `${singleTagMaxWidth.value}px` } : {}}
|
tempInputValue.value = '';
|
||||||
>
|
}
|
||||||
{slots.label ? slots.label(data) : data.label}
|
}}
|
||||||
</div>
|
onInputValueChange={handleInputValueChange}
|
||||||
</a-tooltip>
|
>
|
||||||
),
|
{{
|
||||||
...selectSlots(),
|
prefix: props.prefix ? () => t(props.prefix || '') : null,
|
||||||
}}
|
label: ({ data }: { data: SelectOptionData }) => (
|
||||||
</a-select>
|
<div
|
||||||
|
class="one-line-text"
|
||||||
|
style={singleTagMaxWidth.value > 0 ? { maxWidth: `${singleTagMaxWidth.value}px` } : {}}
|
||||||
|
>
|
||||||
|
{slots.label ? slots.label(data) : data.label}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
...selectSlots(),
|
||||||
|
}}
|
||||||
|
</a-select>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -421,6 +494,10 @@ export default defineComponent(
|
||||||
'labelKey',
|
'labelKey',
|
||||||
'atLeastOne',
|
'atLeastOne',
|
||||||
'objectValue',
|
'objectValue',
|
||||||
|
'remoteFilterFunc',
|
||||||
|
'shouldCalculateMaxTag',
|
||||||
|
'disabled',
|
||||||
|
'size',
|
||||||
],
|
],
|
||||||
emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove', 'change'],
|
emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove', 'change'],
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
|
|
||||||
const fileType = computed(() => {
|
const fileType = computed(() => {
|
||||||
if (props.type) {
|
if (props.type) {
|
||||||
return getFileEnum(`/${props.type.toLowerCase()}`);
|
return getFileEnum(props.type.toLowerCase());
|
||||||
}
|
}
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
});
|
});
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
:deep(.arco-menu-inner) {
|
:deep(.arco-menu-inner) {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: 9px 20px;
|
padding: 9px 0;
|
||||||
.arco-menu-selected-label {
|
.arco-menu-selected-label {
|
||||||
bottom: -8px !important;
|
bottom: -8px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,21 @@
|
||||||
}"
|
}"
|
||||||
:target-input-search-props="props.targetInputSearchProps"
|
:target-input-search-props="props.targetInputSearchProps"
|
||||||
>
|
>
|
||||||
|
<template #source-title="{ countSelected, checked, indeterminate, onSelectAllChange }">
|
||||||
|
<div class="flex items-center gap-[8px]">
|
||||||
|
<a-checkbox :model-value="checked" :indeterminate="indeterminate" @change="onSelectAllChange" />
|
||||||
|
{{ t('ms.transfer.optional', { count: countSelected }) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #target-title="{ countTotal, checked, indeterminate, onSelectAllChange, onClear }">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-[8px]">
|
||||||
|
<a-checkbox :model-value="checked" :indeterminate="indeterminate" @change="onSelectAllChange" />
|
||||||
|
{{ t('ms.transfer.selected', { count: countTotal }) }}
|
||||||
|
</div>
|
||||||
|
<MsButton type="text" @click="onClear">{{ t('ms.transfer.clear') }}</MsButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #source="{ selectedKeys, onSelect }">
|
<template #source="{ selectedKeys, onSelect }">
|
||||||
<MsTree
|
<MsTree
|
||||||
:checkable="true"
|
:checkable="true"
|
||||||
|
@ -19,24 +34,33 @@
|
||||||
:keyword="sourceKeyword"
|
:keyword="sourceKeyword"
|
||||||
block-node
|
block-node
|
||||||
default-expand-all
|
default-expand-all
|
||||||
|
:selectable="false"
|
||||||
@check="onSelect"
|
@check="onSelect"
|
||||||
>
|
>
|
||||||
<template #title="nodeData">
|
<template #title="nodeData">
|
||||||
<div class="one-line-text">
|
<div class="one-line-text text-[var(--color-text-1)]">
|
||||||
{{ nodeData.title }}
|
{{ nodeData.title }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MsTree>
|
</MsTree>
|
||||||
</template>
|
</template>
|
||||||
|
<template #item="{ label }">
|
||||||
|
<a-tooltip :content="label">
|
||||||
|
<div class="one-line-text">{{ label }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
</a-transfer>
|
</a-transfer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
import type { MsTreeFieldNames, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import type { MsTreeFieldNames, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
export interface TransferDataItem {
|
export interface TransferDataItem {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -66,6 +90,8 @@
|
||||||
);
|
);
|
||||||
const emit = defineEmits(['update:modelValue']);
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerTarget = ref<string[]>([]);
|
const innerTarget = ref<string[]>([]);
|
||||||
const transferData = ref<TransferDataItem[]>([]);
|
const transferData = ref<TransferDataItem[]>([]);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
'ms.transfer.optional': 'Optional {count} items',
|
||||||
|
'ms.transfer.selected': 'Selected {count} items',
|
||||||
|
'ms.transfer.clear': 'Clear',
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
'ms.transfer.optional': '可选 {count} 项',
|
||||||
|
'ms.transfer.selected': '已选 {count} 项',
|
||||||
|
'ms.transfer.clear': '清空',
|
||||||
|
};
|
|
@ -152,7 +152,7 @@
|
||||||
});
|
});
|
||||||
const originalTreeData = ref<MsTreeNodeData[]>([]);
|
const originalTreeData = ref<MsTreeNodeData[]>([]);
|
||||||
|
|
||||||
function init() {
|
function init(isFirstInit = false) {
|
||||||
originalTreeData.value = mapTree<MsTreeNodeData>(props.data, (node: MsTreeNodeData) => {
|
originalTreeData.value = mapTree<MsTreeNodeData>(props.data, (node: MsTreeNodeData) => {
|
||||||
if (!props.showLine) {
|
if (!props.showLine) {
|
||||||
// 不展示连接线时才设置节点图标,因为展示连接线时非叶子节点会展示默认的折叠图标。它不会覆盖 switcherIcon,但是会被 switcherIcon 覆盖
|
// 不展示连接线时才设置节点图标,因为展示连接线时非叶子节点会展示默认的折叠图标。它不会覆盖 switcherIcon,但是会被 switcherIcon 覆盖
|
||||||
|
@ -169,22 +169,24 @@
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (props.defaultExpandAll) {
|
if (isFirstInit) {
|
||||||
treeRef.value?.expandAll(true);
|
if (props.defaultExpandAll) {
|
||||||
}
|
treeRef.value?.expandAll(true);
|
||||||
if (!isInitListener.value && treeRef.value) {
|
}
|
||||||
setContainer(
|
if (!isInitListener.value && treeRef.value) {
|
||||||
props.virtualListProps?.height
|
setContainer(
|
||||||
? (treeRef.value.$el.querySelector('.arco-virtual-list') as HTMLElement)
|
props.virtualListProps?.height
|
||||||
: treeRef.value.$el
|
? (treeRef.value.$el.querySelector('.arco-virtual-list') as HTMLElement)
|
||||||
);
|
: treeRef.value.$el
|
||||||
initScrollListener();
|
);
|
||||||
|
initScrollListener();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
init();
|
init(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
|
@ -102,9 +102,6 @@
|
||||||
|
|
||||||
right: 20px;
|
right: 20px;
|
||||||
}
|
}
|
||||||
.arco-list-item-meta-content {
|
|
||||||
@apply flex-1;
|
|
||||||
}
|
|
||||||
.item-wrap {
|
.item-wrap {
|
||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
allow-clear
|
allow-clear
|
||||||
@press-enter="emit('keywordSearch', innerKeyword)"
|
@press-enter="emit('keywordSearch', innerKeyword)"
|
||||||
@search="emit('keywordSearch', innerKeyword)"
|
@search="emit('keywordSearch', innerKeyword)"
|
||||||
|
@clear="emit('keywordSearch', innerKeyword)"
|
||||||
></a-input-search>
|
></a-input-search>
|
||||||
<MsTag
|
<MsTag
|
||||||
:type="visible ? 'primary' : 'default'"
|
:type="visible ? 'primary' : 'default'"
|
||||||
|
@ -17,8 +18,9 @@
|
||||||
size="large"
|
size="large"
|
||||||
class="min-w-[64px] cursor-pointer"
|
class="min-w-[64px] cursor-pointer"
|
||||||
no-margin
|
no-margin
|
||||||
|
@click="handleOpenFilter"
|
||||||
>
|
>
|
||||||
<span :class="!visible ? 'text-[var(--color-text-4)]' : ''" @click="handleOpenFilter">
|
<span :class="!visible ? 'text-[var(--color-text-4)]' : ''">
|
||||||
<icon-filter class="text-[16px]" />
|
<icon-filter class="text-[16px]" />
|
||||||
<span class="ml-[4px]">
|
<span class="ml-[4px]">
|
||||||
<span v-if="filterCount">{{ filterCount }}</span>
|
<span v-if="filterCount">{{ filterCount }}</span>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
'ms-card',
|
'ms-card',
|
||||||
'relative',
|
'relative',
|
||||||
'h-full',
|
'h-full',
|
||||||
props.isFullscreen || isFullScreen ? 'ms-card--no-radius' : '',
|
props.isFullscreen || isFullScreen ? 'ms-card--fullScreen' : '',
|
||||||
props.autoHeight ? '' : 'min-h-[500px]',
|
props.autoHeight ? '' : 'min-h-[500px]',
|
||||||
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
|
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
|
||||||
props.noBottomRadius ? 'ms-card--noBottomRadius' : '',
|
props.noBottomRadius ? 'ms-card--noBottomRadius' : '',
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<slot name="headerRight"></slot>
|
<slot name="headerRight"></slot>
|
||||||
<div
|
<div
|
||||||
v-if="props.showFullScreen"
|
v-if="props.showFullScreen"
|
||||||
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
|
class="cursor-pointer text-right !text-[var(--color-text-4)]"
|
||||||
@click="toggleFullScreen"
|
@click="toggleFullScreen"
|
||||||
>
|
>
|
||||||
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
|
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
|
||||||
|
@ -48,8 +48,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!props.hideFooter && !props.simple"
|
v-if="!props.hideFooter && !props.simple"
|
||||||
class="fixed bottom-0 right-[16px] z-[100] flex items-center bg-white p-[24px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]"
|
class="ms-card-footer"
|
||||||
:style="{ width: `calc(100% - ${menuWidth + 16}px)` }"
|
:style="{ width: props.isFullscreen || isFullScreen ? '100%' : `calc(100% - ${menuWidth + 16}px)` }"
|
||||||
>
|
>
|
||||||
<div class="ml-0 mr-auto">
|
<div class="ml-0 mr-auto">
|
||||||
<slot name="footerLeft"></slot>
|
<slot name="footerLeft"></slot>
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import useFullScreen from '@/hooks/useFullScreen';
|
import useFullScreen from '@/hooks/useFullScreen';
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits(['saveAndContinue', 'save']);
|
const emit = defineEmits(['saveAndContinue', 'save', 'toggleFullScreen']);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -134,9 +134,27 @@
|
||||||
const fullRef = ref<HTMLElement | null>();
|
const fullRef = ref<HTMLElement | null>();
|
||||||
const { isFullScreen, toggleFullScreen } = useFullScreen(fullRef);
|
const { isFullScreen, toggleFullScreen } = useFullScreen(fullRef);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isFullScreen.value,
|
||||||
|
(val) => {
|
||||||
|
emit('toggleFullScreen', val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const _specialHeight = props.hasBreadcrumb ? 32 + props.specialHeight : props.specialHeight; // 有面包屑的话,默认面包屑高度32
|
const _specialHeight = props.hasBreadcrumb ? 32 + props.specialHeight : props.specialHeight; // 有面包屑的话,默认面包屑高度32
|
||||||
|
|
||||||
const cardOverHeight = computed(() => {
|
const cardOverHeight = computed(() => {
|
||||||
|
if (isFullScreen.value) {
|
||||||
|
if (props.simple) {
|
||||||
|
// 简单模式没有标题、没有底部
|
||||||
|
return props.noContentPadding ? 0 : 48;
|
||||||
|
}
|
||||||
|
if (props.hideFooter) {
|
||||||
|
// 隐藏底部
|
||||||
|
return props.noContentPadding ? 88 : 118;
|
||||||
|
}
|
||||||
|
return 246;
|
||||||
|
}
|
||||||
if (props.simple) {
|
if (props.simple) {
|
||||||
// 简单模式没有标题、没有底部
|
// 简单模式没有标题、没有底部
|
||||||
return props.noContentPadding ? 88 + _specialHeight : 136 + _specialHeight;
|
return props.noContentPadding ? 88 + _specialHeight : 136 + _specialHeight;
|
||||||
|
@ -153,7 +171,7 @@
|
||||||
return {
|
return {
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: 'auto',
|
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight.value}px)`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (props.noContentPadding) {
|
if (props.noContentPadding) {
|
||||||
|
@ -218,10 +236,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-card-container {
|
.ms-card-container {
|
||||||
@apply h-full;
|
@apply relative;
|
||||||
|
}
|
||||||
|
.ms-card-footer {
|
||||||
|
@apply fixed justify-between bg-white;
|
||||||
|
|
||||||
|
right: 16px;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 0;
|
||||||
|
|
||||||
|
--tw-shadow: 0 -1px 4px rgb(2 2 2 / 10%);
|
||||||
|
--tw-shadow-colored: 0 -1px 4px var(--tw-shadow-color);
|
||||||
|
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #00000000), var(--tw-ring-shadow, 0 0 #00000000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-card--no-radius {
|
.ms-card--fullScreen {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
.ms-card-footer {
|
||||||
|
@apply left-0 right-0 w-full;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,14 +18,16 @@
|
||||||
<div class="ms-description-item-label" :style="{ width: props.labelWidth || '120px' }">
|
<div class="ms-description-item-label" :style="{ width: props.labelWidth || '120px' }">
|
||||||
<slot name="item-label">{{ item.label }}</slot>
|
<slot name="item-label">{{ item.label }}</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-description-item-value">
|
<div :class="item.isTag ? 'ms-description-item-value--tagline' : 'ms-description-item-value'">
|
||||||
<slot name="item-value" :item="item">
|
<slot name="item-value" :item="item">
|
||||||
<template v-if="item.isTag">
|
<template v-if="item.isTag">
|
||||||
<slot name="tag" :item="item">
|
<slot name="tag" :item="item">
|
||||||
<MsTag
|
<MsTag
|
||||||
v-for="tag of Array.isArray(item.value) ? item.value : [item.value]"
|
v-for="tag of Array.isArray(item.value) ? item.value : [item.value]"
|
||||||
:key="`${tag}`"
|
:key="`${tag}`"
|
||||||
theme="outline"
|
:theme="item.tagTheme || 'outline'"
|
||||||
|
:type="item.tagType || 'primary'"
|
||||||
|
:max-width="item.tagMaxWidth"
|
||||||
color="var(--color-text-n8)"
|
color="var(--color-text-n8)"
|
||||||
:class="`mb-[8px] mr-[8px] font-normal !text-[var(--color-text-1)] ${item.tagClass || ''}`"
|
:class="`mb-[8px] mr-[8px] font-normal !text-[var(--color-text-1)] ${item.tagClass || ''}`"
|
||||||
:closable="item.closable"
|
:closable="item.closable"
|
||||||
|
@ -53,7 +55,15 @@
|
||||||
{{ t('ms.description.addTagRepeat') }}
|
{{ t('ms.description.addTagRepeat') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<MsTag v-else type="primary" theme="outline" class="cursor-pointer" @click="handleEdit">
|
<!-- 标签数量大于等于10时,不显示添加标签 -->
|
||||||
|
<MsTag
|
||||||
|
v-else-if="Array.isArray(item.value) && item.value.length < 10"
|
||||||
|
type="primary"
|
||||||
|
theme="outline"
|
||||||
|
:max-width="item.tagMaxWidth"
|
||||||
|
class="inline-flex cursor-pointer items-center gap-[4px]"
|
||||||
|
@click="handleEdit"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<MsIcon type="icon-icon_add_outlined" class="text-[rgb(var(--primary-5))]" />
|
<MsIcon type="icon-icon_add_outlined" class="text-[rgb(var(--primary-5))]" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -102,7 +112,7 @@
|
||||||
|
|
||||||
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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
import MsTag, { TagType, Theme } from '@/components/pure/ms-tag/ms-tag.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
@ -112,6 +122,9 @@
|
||||||
key?: string;
|
key?: string;
|
||||||
isTag?: boolean; // 是否标签
|
isTag?: boolean; // 是否标签
|
||||||
tagClass?: string; // 标签自定义类名
|
tagClass?: string; // 标签自定义类名
|
||||||
|
tagType?: TagType; // 标签类型
|
||||||
|
tagTheme?: Theme; // 标签主题
|
||||||
|
tagMaxWidth?: string; // 标签最大宽度
|
||||||
closable?: boolean; // 标签是否可关闭
|
closable?: boolean; // 标签是否可关闭
|
||||||
showTagAdd?: boolean; // 是否显示添加标签
|
showTagAdd?: boolean; // 是否显示添加标签
|
||||||
isButton?: boolean;
|
isButton?: boolean;
|
||||||
|
@ -227,9 +240,11 @@
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.ms-description-item-value {
|
.ms-description-item-value,
|
||||||
|
.ms-description-item-value--tagline {
|
||||||
@apply relative flex-1 overflow-hidden break-all align-top;
|
@apply relative flex-1 overflow-hidden break-all align-top;
|
||||||
|
}
|
||||||
|
.ms-description-item-value {
|
||||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
@ -123,12 +123,12 @@
|
||||||
const props = withDefaults(defineProps<DrawerProps>(), {
|
const props = withDefaults(defineProps<DrawerProps>(), {
|
||||||
footer: true,
|
footer: true,
|
||||||
mask: true,
|
mask: true,
|
||||||
|
closable: true,
|
||||||
showSkeleton: false,
|
showSkeleton: false,
|
||||||
showContinue: false,
|
showContinue: false,
|
||||||
popupContainer: 'body',
|
popupContainer: 'body',
|
||||||
disabledWidthDrag: false,
|
disabledWidthDrag: false,
|
||||||
okPermission: () => [], // 确认按钮权限
|
okPermission: () => [], // 确认按钮权限
|
||||||
closable: true,
|
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue']);
|
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue']);
|
||||||
|
|
||||||
|
|
|
@ -241,6 +241,9 @@
|
||||||
}
|
}
|
||||||
.arco-list-item-meta {
|
.arco-list-item-meta {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
|
.arco-list-item-meta-content {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
.arco-list-item-meta-title:not(:last-child) {
|
.arco-list-item-meta-title:not(:last-child) {
|
||||||
@apply mb-0;
|
@apply mb-0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -514,6 +514,15 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(.halo-rich-text-editor) {
|
||||||
|
padding: 16px 24px !important;
|
||||||
|
.editor-header {
|
||||||
|
justify-content: start !important;
|
||||||
|
}
|
||||||
|
p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
:deep(.editor-header) {
|
:deep(.editor-header) {
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import MentionList from './MentionList.vue';
|
import MentionList from './MentionList.vue';
|
||||||
|
|
||||||
import { getReviewerList } from '@/api/modules/case-management/featureCase';
|
import { getReviewUsers } from '@/api/modules/case-management/caseReview';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
import type { UserListItem } from '@/models/setting/user';
|
import { ReviewUserItem } from '@/models/caseManagement/caseReview';
|
||||||
|
|
||||||
import { Extension, VueRenderer } from '@halo-dev/richtext-editor';
|
import { VueRenderer } from '@halo-dev/richtext-editor';
|
||||||
import Suggestion from '@tiptap/suggestion';
|
|
||||||
import type { Instance } from 'tippy.js';
|
import type { Instance } from 'tippy.js';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const projectMember = ref<UserListItem[]>([]);
|
const projectMember = ref<ReviewUserItem[]>([]);
|
||||||
|
|
||||||
async function getMembersToolBar(query: string) {
|
async function getMembersToolBar(query: string) {
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -20,8 +19,9 @@ async function getMembersToolBar(query: string) {
|
||||||
keyword: query,
|
keyword: query,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
projectMember.value = await getReviewerList(params.projectId, params.keyword);
|
projectMember.value = await getReviewUsers(params.projectId, params.keyword);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,9 @@ async function getMembersToolBar(query: string) {
|
||||||
export default {
|
export default {
|
||||||
items: async ({ query }: any) => {
|
items: async ({ query }: any) => {
|
||||||
await getMembersToolBar(query);
|
await getMembersToolBar(query);
|
||||||
return projectMember.value.filter((item: UserListItem) => item.name.toLowerCase().startsWith(query.toLowerCase()));
|
return projectMember.value.filter((item: ReviewUserItem) =>
|
||||||
|
item.name.toLowerCase().startsWith(query.toLowerCase())
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: () => {
|
render: () => {
|
||||||
|
|
|
@ -18,13 +18,17 @@
|
||||||
<slot name="optional" v-bind="{ rowIndex, record }" />
|
<slot name="optional" v-bind="{ rowIndex, record }" />
|
||||||
</template>
|
</template>
|
||||||
<template #columns>
|
<template #columns>
|
||||||
<a-table-column v-if="attrs.selectable && props.selectedKeys" :width="props.firstColumnWidth || 60">
|
<a-table-column
|
||||||
|
v-if="attrs.selectable && props.selectedKeys"
|
||||||
|
:width="props.firstColumnWidth || 60"
|
||||||
|
fixed="left"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<SelectALL
|
<SelectALL
|
||||||
v-if="attrs.selectorType === 'checkbox'"
|
v-if="attrs.selectorType === 'checkbox'"
|
||||||
:total="selectTotal"
|
:total="selectTotal"
|
||||||
:current="selectCurrent"
|
:current="selectCurrent"
|
||||||
:show-select-all="(attrs.showPagination as boolean) && props.showSelectorAll"
|
:show-select-all="!!attrs.showPagination && props.showSelectorAll"
|
||||||
:disabled="(attrs.data as []).length === 0"
|
:disabled="(attrs.data as []).length === 0"
|
||||||
@change="handleSelectAllChange"
|
@change="handleSelectAllChange"
|
||||||
/>
|
/>
|
||||||
|
@ -211,7 +215,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-[500px]">
|
<div class="min-w-[500px]">
|
||||||
<ms-pagination
|
<ms-pagination
|
||||||
v-if="attrs.showPagination"
|
v-if="!!attrs.showPagination"
|
||||||
v-show="props.selectorStatus !== SelectAllEnum.CURRENT"
|
v-show="props.selectorStatus !== SelectAllEnum.CURRENT"
|
||||||
size="small"
|
size="small"
|
||||||
v-bind="(attrs.msPagination as MsPaginationI)"
|
v-bind="(attrs.msPagination as MsPaginationI)"
|
||||||
|
@ -226,7 +230,7 @@
|
||||||
v-model:visible="columnSelectorVisible"
|
v-model:visible="columnSelectorVisible"
|
||||||
:show-jump-method="(attrs.showJumpMethod as boolean)"
|
:show-jump-method="(attrs.showJumpMethod as boolean)"
|
||||||
:table-key="(attrs.tableKey as string)"
|
:table-key="(attrs.tableKey as string)"
|
||||||
:show-pagination="(attrs.showPagination as boolean)"
|
:show-pagination="!!attrs.showPagination"
|
||||||
@init-data="handleInitColumn"
|
@init-data="handleInitColumn"
|
||||||
@page-size-change="pageSizeChange"
|
@page-size-change="pageSizeChange"
|
||||||
></ColumnSelector>
|
></ColumnSelector>
|
||||||
|
@ -569,9 +573,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-base-table--hasQuickCreate {
|
.ms-base-table--hasQuickCreate {
|
||||||
:deep(.arco-table-body) {
|
:deep(.arco-table-body:not(.arco-scrollbar-container)) {
|
||||||
padding-top: 54px;
|
padding-top: 54px;
|
||||||
}
|
}
|
||||||
|
:deep(.arco-table-element:not(.arco-table-header .arco-table-element)) {
|
||||||
|
padding-bottom: 54px;
|
||||||
|
tbody {
|
||||||
|
transform: translateY(54px);
|
||||||
|
}
|
||||||
|
}
|
||||||
:deep(.arco-table-tr:first-child) {
|
:deep(.arco-table-tr:first-child) {
|
||||||
.arco-table-td {
|
.arco-table-td {
|
||||||
border-top: 1px solid var(--color-text-n8);
|
border-top: 1px solid var(--color-text-n8);
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
...typeStyle,
|
...typeStyle,
|
||||||
'margin-right': noMargin ? 0 : tagMargin,
|
'margin-right': noMargin ? 0 : tagMargin,
|
||||||
'min-width': props.width && `${props.width}ch`,
|
'min-width': props.width && `${props.width}ch`,
|
||||||
'max-width': '144px',
|
'max-width': props.maxWidth || '144px',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot name="icon"></slot>
|
<slot name="icon"></slot>
|
||||||
|
@ -33,6 +33,7 @@
|
||||||
theme?: Theme; // tag主题
|
theme?: Theme; // tag主题
|
||||||
selfStyle?: any; // 自定义样式
|
selfStyle?: any; // 自定义样式
|
||||||
width?: number; // tag宽度,不传入时绑定max-width
|
width?: number; // tag宽度,不传入时绑定max-width
|
||||||
|
maxWidth?: string;
|
||||||
noMargin?: boolean; // tag之间是否有间距
|
noMargin?: boolean; // tag之间是否有间距
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,112 +1,137 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div>
|
||||||
v-if="props.mode === 'remote' && props.showTab"
|
<div
|
||||||
class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white"
|
v-if="props.mode === 'remote' && props.showTab"
|
||||||
>
|
class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white"
|
||||||
<a-radio-group v-model:model-value="fileListTab" type="button" size="small">
|
>
|
||||||
<a-radio value="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio>
|
<a-radio-group v-model:model-value="fileListTab" type="button" size="small">
|
||||||
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio>
|
<a-radio value="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio>
|
||||||
<a-radio value="success">{{ `${t('ms.upload.success')} (${totalSuccessFileList.length})` }}</a-radio>
|
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio>
|
||||||
<a-radio value="error">{{ `${t('ms.upload.fail')} (${totalFailFileList.length})` }}</a-radio>
|
<a-radio value="success">{{ `${t('ms.upload.success')} (${totalSuccessFileList.length})` }}</a-radio>
|
||||||
</a-radio-group>
|
<a-radio value="error">{{ `${t('ms.upload.fail')} (${totalFailFileList.length})` }}</a-radio>
|
||||||
<slot name="tabExtra"></slot>
|
</a-radio-group>
|
||||||
</div>
|
<slot name="tabExtra"></slot>
|
||||||
<MsList :data="filterFileList" :bordered="false" :split="false" item-border no-hover>
|
</div>
|
||||||
<template #item="{ item }">
|
<MsList
|
||||||
<a-list-item
|
v-if="props.showMode === 'fileList'"
|
||||||
class="mb-[8px] w-full rounded-[var(--border-radius-small)] border border-solid border-[var(--color-text-n8)] !p-[8px_12px]"
|
:data="filterFileList"
|
||||||
>
|
:bordered="false"
|
||||||
<a-list-item-meta>
|
:split="false"
|
||||||
<template #avatar>
|
item-border
|
||||||
<a-avatar shape="square" class="rounded-[var(--border-radius-mini)] bg-[var(--color-text-n9)]">
|
no-hover
|
||||||
<a-image v-if="item.file.type.includes('image/')" :src="item.url" width="40" height="40" hide-footer />
|
>
|
||||||
<MsIcon
|
<template #item="{ item }">
|
||||||
v-else
|
<a-list-item
|
||||||
:type="getFileIcon(item)"
|
class="mb-[8px] w-full rounded-[var(--border-radius-small)] border border-solid border-[var(--color-text-n8)] !p-[8px_12px]"
|
||||||
size="24"
|
>
|
||||||
:class="getFileEnum(item.file?.type) === 'unknown' ? 'text-[var(--color-text-4)]' : ''"
|
<a-list-item-meta>
|
||||||
/>
|
<template #avatar>
|
||||||
</a-avatar>
|
<a-avatar shape="square" class="rounded-[var(--border-radius-mini)] bg-[var(--color-text-n9)]">
|
||||||
</template>
|
<a-image v-if="item.file.type.includes('image/')" :src="item.url" width="40" height="40" hide-footer />
|
||||||
<template #title>
|
<MsIcon
|
||||||
<div class="flex items-center">
|
v-else
|
||||||
<a-tooltip :content="item.file.name">
|
:type="getFileIcon(item)"
|
||||||
<div class="one-line-text max-w-[80%] font-normal">{{ item.file.name }}</div>
|
size="24"
|
||||||
</a-tooltip>
|
:class="item.status === UploadStatus.init ? 'text-[var(--color-text-4)]' : ''"
|
||||||
<slot name="title" :item="item"></slot>
|
/>
|
||||||
</div>
|
</a-avatar>
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #title>
|
||||||
<div v-if="item.status === UploadStatus.init" class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
|
||||||
{{ t('ms.upload.waiting') }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="item.status === UploadStatus.done"
|
|
||||||
class="flex items-center gap-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
`${formatFileSize(item.file.size)} ${t('ms.upload.uploadAt')} ${dayjs(item.uploadedTime).format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss'
|
|
||||||
)}`
|
|
||||||
}}
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<MsIcon type="icon-icon_succeed_colorful" />
|
<a-tooltip :content="item.file.name">
|
||||||
{{ t('ms.upload.uploadSuccess') }}
|
<div class="one-line-text max-w-[80%] font-normal">{{ item.file.name }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
<slot name="title" :item="item"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<a-progress
|
<template #description>
|
||||||
v-else-if="item.status === UploadStatus.uploading"
|
<div
|
||||||
:percent="asyncTaskStore.uploadFileTask.singleProgress / 100"
|
v-if="item.status === UploadStatus.init"
|
||||||
:show-text="false"
|
class="text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
||||||
size="large"
|
>
|
||||||
class="w-[200px]"
|
{{ t('ms.upload.waiting') }}
|
||||||
/>
|
</div>
|
||||||
<div v-else-if="item.status === UploadStatus.error" class="text-[rgb(var(--danger-6))]">
|
<div
|
||||||
{{ item.errMsg || t('ms.upload.uploadFail') }}
|
v-else-if="item.status === UploadStatus.done"
|
||||||
|
class="flex items-center gap-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
`${formatFileSize(item.file.size)} ${t('ms.upload.uploadAt')} ${dayjs(item.uploadedTime).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss'
|
||||||
|
)}`
|
||||||
|
}}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<MsIcon type="icon-icon_succeed_colorful" />
|
||||||
|
{{ t('ms.upload.uploadSuccess') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-progress
|
||||||
|
v-else-if="item.status === UploadStatus.uploading"
|
||||||
|
:percent="asyncTaskStore.uploadFileTask.singleProgress / 100"
|
||||||
|
:show-text="false"
|
||||||
|
size="large"
|
||||||
|
class="w-[200px]"
|
||||||
|
/>
|
||||||
|
<div v-else-if="item.status === UploadStatus.error" class="text-[rgb(var(--danger-6))]">
|
||||||
|
{{ item.errMsg || t('ms.upload.uploadFail') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<MsButton
|
||||||
|
v-if="item.file.type.includes('image/')"
|
||||||
|
type="button"
|
||||||
|
status="primary"
|
||||||
|
class="!mr-0"
|
||||||
|
@click="handlePreview(item)"
|
||||||
|
>
|
||||||
|
{{ t('ms.upload.preview') }}
|
||||||
|
</MsButton>
|
||||||
|
<MsButton
|
||||||
|
v-if="item.status === UploadStatus.error"
|
||||||
|
type="button"
|
||||||
|
status="secondary"
|
||||||
|
class="!mr-0"
|
||||||
|
@click="reupload(item)"
|
||||||
|
>
|
||||||
|
{{ t('ms.upload.reUpload') }}
|
||||||
|
</MsButton>
|
||||||
|
<MsButton
|
||||||
|
v-if="props.showDelete"
|
||||||
|
type="button"
|
||||||
|
:status="item.deleteContent ? 'primary' : 'danger'"
|
||||||
|
class="!mr-[4px]"
|
||||||
|
@click="deleteFile(item)"
|
||||||
|
>
|
||||||
|
{{ t(item.deleteContent) || t('ms.upload.delete') }}
|
||||||
|
</MsButton>
|
||||||
|
<slot name="actions" :item="item"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-list-item-meta>
|
</a-list-item>
|
||||||
<template #actions>
|
</template>
|
||||||
<div class="flex items-center">
|
</MsList>
|
||||||
<MsButton
|
<div v-else class="flex w-full items-center gap-[8px]">
|
||||||
v-if="item.file.type.includes('image/')"
|
<div v-for="item of filterFileList" :key="item.uid" class="image-item">
|
||||||
type="button"
|
<a-image
|
||||||
status="primary"
|
:src="item.url"
|
||||||
class="!mr-0"
|
width="40"
|
||||||
@click="handlePreview(item)"
|
height="40"
|
||||||
>
|
:preview="false"
|
||||||
{{ t('ms.upload.preview') }}
|
class="cursor-pointer"
|
||||||
</MsButton>
|
@click="handlePreview(item)"
|
||||||
<MsButton
|
/>
|
||||||
v-if="item.status === UploadStatus.error"
|
<icon-close-circle-fill class="image-item-close-icon" @click="deleteFile(item)" />
|
||||||
type="button"
|
</div>
|
||||||
status="secondary"
|
</div>
|
||||||
class="!mr-0"
|
<a-image-preview-group
|
||||||
@click="reupload(item)"
|
v-model:visible="previewVisible"
|
||||||
>
|
v-model:current="previewCurrent"
|
||||||
{{ t('ms.upload.reUpload') }}
|
infinite
|
||||||
</MsButton>
|
:src-list="previewList"
|
||||||
<MsButton
|
/>
|
||||||
v-if="props.showDelete"
|
</div>
|
||||||
type="button"
|
|
||||||
:status="item.deleteContent ? 'primary' : 'danger'"
|
|
||||||
class="!mr-[4px]"
|
|
||||||
@click="deleteFile(item)"
|
|
||||||
>
|
|
||||||
{{ t(item.deleteContent) || t('ms.upload.delete') }}
|
|
||||||
</MsButton>
|
|
||||||
<slot name="actions" :item="item"></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-list-item>
|
|
||||||
</template>
|
|
||||||
</MsList>
|
|
||||||
<a-image-preview-group
|
|
||||||
v-model:visible="previewVisible"
|
|
||||||
v-model:current="previewCurrent"
|
|
||||||
infinite
|
|
||||||
:src-list="previewList"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -123,13 +148,14 @@
|
||||||
|
|
||||||
import { UploadStatus } from '@/enums/uploadEnum';
|
import { UploadStatus } from '@/enums/uploadEnum';
|
||||||
|
|
||||||
import { getFileEnum, getFileIcon } from './iconMap';
|
import { getFileIcon } from './iconMap';
|
||||||
import type { MsFileItem } from './types';
|
import type { MsFileItem } from './types';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
mode?: 'static' | 'remote'; // 静态|远程
|
mode?: 'static' | 'remote'; // 静态|远程
|
||||||
fileList: MsFileItem[];
|
fileList: MsFileItem[];
|
||||||
|
showMode?: 'fileList' | 'imageList'; // 展示模式, 文件列表|图片列表
|
||||||
uploadFunc?: (params: any) => Promise<any>; // 上传文件时,自定义上传方法
|
uploadFunc?: (params: any) => Promise<any>; // 上传文件时,自定义上传方法
|
||||||
requestParams?: Record<string, any>; // 上传文件时,额外的请求参数
|
requestParams?: Record<string, any>; // 上传文件时,额外的请求参数
|
||||||
route?: string; // 用于后台上传文件时,查看详情跳转的路由
|
route?: string; // 用于后台上传文件时,查看详情跳转的路由
|
||||||
|
@ -144,6 +170,7 @@
|
||||||
mode: 'remote',
|
mode: 'remote',
|
||||||
showTab: true,
|
showTab: true,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
|
showMode: 'fileList',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -294,4 +321,23 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped>
|
||||||
|
.image-item {
|
||||||
|
@apply relative;
|
||||||
|
&:hover {
|
||||||
|
.image-item-close-icon {
|
||||||
|
@apply visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.image-item-close-icon {
|
||||||
|
@apply invisible absolute cursor-pointer rounded-full;
|
||||||
|
|
||||||
|
top: -7px;
|
||||||
|
right: -5px;
|
||||||
|
z-index: 10;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
background-color: var(--color-text-n8);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -87,7 +87,7 @@ export function getFileEnum(fileType?: string): keyof typeof UploadAcceptEnum {
|
||||||
const keys = Object.keys(UploadAcceptEnum);
|
const keys = Object.keys(UploadAcceptEnum);
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = keys[i] as unknown as keyof typeof UploadAcceptEnum;
|
const key = keys[i] as unknown as keyof typeof UploadAcceptEnum;
|
||||||
if (UploadAcceptEnum[key].split(',').includes(`.${fileType.split('/')[1]}`)) {
|
if (UploadAcceptEnum[key].includes(fileType)) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,10 +98,13 @@ export function getFileEnum(fileType?: string): keyof typeof UploadAcceptEnum {
|
||||||
/**
|
/**
|
||||||
* 获取文件图标
|
* 获取文件图标
|
||||||
* @param item 文件项
|
* @param item 文件项
|
||||||
|
* @param status 文件状态
|
||||||
*/
|
*/
|
||||||
export function getFileIcon(item: MsFileItem) {
|
export function getFileIcon(item: MsFileItem, status?: UploadStatus) {
|
||||||
if (item.status === UploadStatus.done) {
|
const fileType = item.file?.name.split('.').pop(); // 通过文件后缀判断文件类型
|
||||||
return FileIconMap[getFileEnum(item.file?.type)]?.[item.status] ?? FileIconMap.unknown[UploadStatus.done];
|
const _status = status || item.status;
|
||||||
|
if (_status === UploadStatus.done) {
|
||||||
|
return FileIconMap[getFileEnum(fileType)]?.[_status] ?? FileIconMap.unknown[UploadStatus.done];
|
||||||
}
|
}
|
||||||
return FileIconMap[getFileEnum(item.file?.type)]?.[UploadStatus.init] ?? FileIconMap.unknown[UploadStatus.done];
|
return FileIconMap[getFileEnum(fileType)]?.[_status || UploadStatus.init] ?? FileIconMap.unknown[UploadStatus.done];
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
}"
|
}"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
@before-upload="beforeUpload"
|
@before-upload="beforeUpload"
|
||||||
|
@exceed-limit="() => Message.warning(t('ms.upload.overLimit', { limit: props.limit }))"
|
||||||
>
|
>
|
||||||
<template #upload-button>
|
<template #upload-button>
|
||||||
<slot>
|
<slot>
|
||||||
|
@ -23,8 +24,8 @@
|
||||||
<div class="ms-upload-icon-box">
|
<div class="ms-upload-icon-box">
|
||||||
<MsIcon
|
<MsIcon
|
||||||
v-if="props.accept !== UploadAcceptEnum.none"
|
v-if="props.accept !== UploadAcceptEnum.none"
|
||||||
:type="FileIconMap[props.accept][UploadStatus.done]"
|
:type="fileIconType"
|
||||||
class="ms-upload-icon"
|
class="ms-upload-icon text-[var(--color-text-4)]"
|
||||||
/>
|
/>
|
||||||
<div v-else class="ms-upload-icon ms-upload-icon--default"></div>
|
<div v-else class="ms-upload-icon ms-upload-icon--default"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +70,7 @@
|
||||||
|
|
||||||
import { UploadAcceptEnum, UploadStatus } from '@/enums/uploadEnum';
|
import { UploadAcceptEnum, UploadStatus } from '@/enums/uploadEnum';
|
||||||
|
|
||||||
import { FileIconMap } from './iconMap';
|
import { FileIconMap, getFileIcon } from './iconMap';
|
||||||
import type { MsFileItem, UploadType } from './types';
|
import type { MsFileItem, UploadType } from './types';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -92,6 +93,7 @@
|
||||||
isAllScreen?: boolean; // 是否是全屏显示拖拽上传
|
isAllScreen?: boolean; // 是否是全屏显示拖拽上传
|
||||||
cutHeight: number; // 被剪切高度
|
cutHeight: number; // 被剪切高度
|
||||||
fileTypeTip?: string; // 上传文件类型错误提示
|
fileTypeTip?: string; // 上传文件类型错误提示
|
||||||
|
limit: number; // 限制上传文件数量
|
||||||
}> & {
|
}> & {
|
||||||
accept: UploadType;
|
accept: UploadType;
|
||||||
fileList: MsFileItem[];
|
fileList: MsFileItem[];
|
||||||
|
@ -124,6 +126,15 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fileIconType = computed(() => {
|
||||||
|
// 单选并且选了文件,按文件类型展示图标(单选文件选择后直接展示绿色图标)
|
||||||
|
if (fileList.value.length > 0 && !props.multiple) {
|
||||||
|
return getFileIcon(fileList.value[0], UploadStatus.done);
|
||||||
|
}
|
||||||
|
// 多选直接按照类型展示
|
||||||
|
return FileIconMap[props.accept][UploadStatus.init];
|
||||||
|
});
|
||||||
|
|
||||||
async function beforeUpload(file: File) {
|
async function beforeUpload(file: File) {
|
||||||
if (!props.multiple && fileList.value.length > 0) {
|
if (!props.multiple && fileList.value.length > 0) {
|
||||||
// 单文件上传时,清空之前的文件
|
// 单文件上传时,清空之前的文件
|
||||||
|
|
|
@ -16,4 +16,5 @@ export default {
|
||||||
'ms.upload.success': 'Success',
|
'ms.upload.success': 'Success',
|
||||||
'ms.upload.detail': 'Detail',
|
'ms.upload.detail': 'Detail',
|
||||||
'ms.upload.fileTypeValidate': 'Only files in {type} format are supported',
|
'ms.upload.fileTypeValidate': 'Only files in {type} format are supported',
|
||||||
|
'ms.upload.overLimit': 'The number of files exceeds the limit, the maximum number of uploads is {limit}',
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,4 +16,5 @@ export default {
|
||||||
'ms.upload.fail': '失败',
|
'ms.upload.fail': '失败',
|
||||||
'ms.upload.detail': '查看详情',
|
'ms.upload.detail': '查看详情',
|
||||||
'ms.upload.fileTypeValidate': '仅支持 {type} 格式的文件',
|
'ms.upload.fileTypeValidate': '仅支持 {type} 格式的文件',
|
||||||
|
'ms.upload.overLimit': '文件数量超出限制,最大上传数量为 {limit} 个',
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
<template v-if="showProjectSelect">
|
<template v-if="showProjectSelect">
|
||||||
<a-divider direction="vertical" class="ml-0" />
|
<a-divider direction="vertical" class="ml-0" />
|
||||||
<a-select
|
<a-select
|
||||||
|
v-model:model-value="appStore.currentProjectId"
|
||||||
class="w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
|
class="w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
|
||||||
:default-value="appStore.currentProjectId"
|
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
allow-search
|
allow-search
|
||||||
@change="selectProject"
|
@change="selectProject"
|
||||||
|
@ -21,7 +21,12 @@
|
||||||
<template #arrow-icon>
|
<template #arrow-icon>
|
||||||
<icon-caret-down />
|
<icon-caret-down />
|
||||||
</template>
|
</template>
|
||||||
<a-tooltip v-for="project of projectList" :key="project.id" :mouse-enter-delay="500" :content="project.name">
|
<a-tooltip
|
||||||
|
v-for="project of appStore.projectList"
|
||||||
|
:key="project.id"
|
||||||
|
:mouse-enter-delay="500"
|
||||||
|
:content="project.name"
|
||||||
|
>
|
||||||
<a-option
|
<a-option
|
||||||
:value="project.id"
|
:value="project.id"
|
||||||
:class="project.id === appStore.currentProjectId ? 'arco-select-option-selected' : ''"
|
:class="project.id === appStore.currentProjectId ? 'arco-select-option-selected' : ''"
|
||||||
|
@ -35,7 +40,7 @@
|
||||||
<TopMenu />
|
<TopMenu />
|
||||||
</div>
|
</div>
|
||||||
<ul v-if="!props.isPreview && !props.hideRight" class="right-side">
|
<ul v-if="!props.isPreview && !props.hideRight" class="right-side">
|
||||||
<li>
|
<!-- <li>
|
||||||
<a-tooltip :content="t('settings.navbar.search')">
|
<a-tooltip :content="t('settings.navbar.search')">
|
||||||
<a-button type="secondary">
|
<a-button type="secondary">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -43,7 +48,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</li>
|
</li> -->
|
||||||
<li>
|
<li>
|
||||||
<a-tooltip :content="t('settings.navbar.alerts')">
|
<a-tooltip :content="t('settings.navbar.alerts')">
|
||||||
<div class="message-box-trigger">
|
<div class="message-box-trigger">
|
||||||
|
@ -87,10 +92,25 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption v-for="item in helpCenterList" :key="item.name" :value="item.name">
|
<a-doption value="doc">
|
||||||
<component :is="item.icon"></component>
|
<component :is="IconQuestionCircle"></component>
|
||||||
{{ t(item.name) }}
|
{{ t('settings.help.doc') }}
|
||||||
</a-doption>
|
</a-doption>
|
||||||
|
<a-popover position="left">
|
||||||
|
<a-doption value="version">
|
||||||
|
<component :is="IconInfoCircle"></component>
|
||||||
|
{{ t('settings.help.versionInfo') }}
|
||||||
|
</a-doption>
|
||||||
|
<template #content>
|
||||||
|
<div
|
||||||
|
class="flex cursor-pointer items-center gap-[4px] text-[14px] text-[var(--color-text-1)]"
|
||||||
|
@click="copyVersion"
|
||||||
|
>
|
||||||
|
<div class="text-[var(--color-text-4)]">{{ t('settings.help.version') }}:</div>
|
||||||
|
{{ appStore.version }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</li>
|
</li>
|
||||||
|
@ -118,13 +138,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeMount, Ref, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
import TopMenu from '@/components/business/ms-top-menu/index.vue';
|
import TopMenu from '@/components/business/ms-top-menu/index.vue';
|
||||||
import MessageBox from '../message-box/index.vue';
|
import MessageBox from '../message-box/index.vue';
|
||||||
|
|
||||||
import { getProjectList, switchProject } from '@/api/modules/project-management/project';
|
import { switchProject } from '@/api/modules/project-management/project';
|
||||||
import { MENU_LEVEL, type PathMapRoute } from '@/config/pathMap';
|
import { MENU_LEVEL, type PathMapRoute } from '@/config/pathMap';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import usePathMap from '@/hooks/usePathMap';
|
import usePathMap from '@/hooks/usePathMap';
|
||||||
|
@ -132,9 +154,6 @@
|
||||||
import useLocale from '@/locale/useLocale';
|
import useLocale from '@/locale/useLocale';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
import useUserStore from '@/store/modules/user';
|
import useUserStore from '@/store/modules/user';
|
||||||
import { hasAnyPermission } from '@/utils/permission';
|
|
||||||
|
|
||||||
import type { ProjectListItem } from '@/models/setting/project';
|
|
||||||
|
|
||||||
import { IconInfoCircle, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
|
import { IconInfoCircle, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
|
||||||
|
|
||||||
|
@ -151,30 +170,13 @@
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const projectList: Ref<ProjectListItem[]> = ref([]);
|
|
||||||
|
|
||||||
async function initProjects() {
|
|
||||||
try {
|
|
||||||
if (appStore.currentOrgId && hasAnyPermission(['SYSTEM_PARAMETER_SETTING_BASE:READ'])) {
|
|
||||||
const res = await getProjectList(appStore.getCurrentOrgId);
|
|
||||||
projectList.value = res;
|
|
||||||
} else {
|
|
||||||
projectList.value = [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
initProjects();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => appStore.currentOrgId,
|
() => appStore.currentOrgId,
|
||||||
async () => {
|
async () => {
|
||||||
initProjects();
|
appStore.initProjectList();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -189,6 +191,7 @@
|
||||||
) {
|
) {
|
||||||
appStore.setCurrentProjectId(value as string);
|
appStore.setCurrentProjectId(value as string);
|
||||||
try {
|
try {
|
||||||
|
appStore.showLoading();
|
||||||
await switchProject({
|
await switchProject({
|
||||||
projectId: value as string,
|
projectId: value as string,
|
||||||
userId: userStore.id || '',
|
userId: userStore.id || '',
|
||||||
|
@ -197,6 +200,7 @@
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
appStore.hideLoading();
|
||||||
router.replace({
|
router.replace({
|
||||||
path: route.path,
|
path: route.path,
|
||||||
query: {
|
query: {
|
||||||
|
@ -208,28 +212,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const helpCenterList = [
|
const { copy, isSupported } = useClipboard();
|
||||||
// {
|
function copyVersion() {
|
||||||
// name: 'settings.help.guide',
|
if (isSupported) {
|
||||||
// icon: IconCompass,
|
copy(appStore.version);
|
||||||
// route: '/help-center/guide',
|
Message.success(t('common.copySuccess'));
|
||||||
// },
|
} else {
|
||||||
{
|
Message.warning(t('common.copyNotSupport'));
|
||||||
name: 'settings.help.doc',
|
}
|
||||||
icon: IconQuestionCircle,
|
}
|
||||||
route: '/help-center/guide',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// name: 'settings.help.APIDoc',
|
|
||||||
// icon: IconFile,
|
|
||||||
// route: '/help-center/guide',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: 'settings.help.version',
|
|
||||||
icon: IconInfoCircle,
|
|
||||||
route: '/help-center/guide',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { changeLocale, currentLocale } = useLocale();
|
const { changeLocale, currentLocale } = useLocale();
|
||||||
const locales = [...LOCALE_OPTIONS];
|
const locales = [...LOCALE_OPTIONS];
|
||||||
|
@ -334,4 +325,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/models/setting/project @/api/modules/setting/project @/api/modules/project-management/project
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 文件关联用例-用例类型
|
||||||
|
export const associateFileSourceLocaleMap = {
|
||||||
|
BUG: 'project.fileManagement.caseTypeBug',
|
||||||
|
FUNCTIONAL_CASE: 'project.fileManagement.caseTypeFeature',
|
||||||
|
API_DEBUG: 'project.fileManagement.caseTypeApiDebug',
|
||||||
|
API_SCENARIO: 'project.fileManagement.caseTypeApiScene',
|
||||||
|
API_TEST_CASE: 'project.fileManagement.caseTypeApiCase',
|
||||||
|
API_DEFINITION: 'project.fileManagement.caseTypeApiDefine',
|
||||||
|
API_DEFINITION_MOCK: 'project.fileManagement.caseTypeApiMock',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {};
|
|
@ -30,33 +30,27 @@ export default function useSelect(config: UseSelectOption) {
|
||||||
* 计算最大标签数量
|
* 计算最大标签数量
|
||||||
* @param options 选择器的选项
|
* @param options 选择器的选项
|
||||||
*/
|
*/
|
||||||
function calculateMaxTag(options?: CascaderOption[] | SelectOptionData[]) {
|
function calculateMaxTag() {
|
||||||
nextTick(() => {
|
setTimeout(() => {
|
||||||
if (config.selectRef.value && selectViewInner.value && Array.isArray(config.selectVal.value)) {
|
if (config.selectRef.value && selectViewInner.value) {
|
||||||
if (maxTagCount.value >= 1 && config.selectVal.value.length > maxTagCount.value) return; // 已经超过最大数量的展示,不需要再计算
|
|
||||||
const innerViewWidth = selectViewInner.value?.getBoundingClientRect().width;
|
const innerViewWidth = selectViewInner.value?.getBoundingClientRect().width;
|
||||||
let lastWidth = innerViewWidth - 60; // 60px 是“+N”的标签宽度+聚焦输入框的宽度
|
let lastWidth = innerViewWidth - 60; // 60px 是“+N”的标签宽度+聚焦输入框的宽度
|
||||||
let tagCount = 0;
|
const childrenNodes = selectViewInner.value.children;
|
||||||
const values = Object.values(config.selectVal.value);
|
if (maxTagCount.value >= 1 && maxTagCount.value < config.selectVal.value.length) {
|
||||||
for (let i = 0; i < values.length; i++) {
|
return;
|
||||||
const option = options?.find((e) => e[config.valueKey || 'value'] === values[i]);
|
}
|
||||||
const tagWidth = (option ? option[config.labelKey || 'label']?.length || 0 : values[i].length) * 12; // 计算每个标签渲染出来的宽度,文字大小在12px时宽度也是 12px
|
for (let i = 0; i < childrenNodes.length; i++) {
|
||||||
|
const child = childrenNodes[i];
|
||||||
if (lastWidth > tagWidth + 36) {
|
if (child.classList.contains('arco-tag')) {
|
||||||
tagCount += 1;
|
lastWidth -= child.clientWidth - 6; // 6px 是标签的边距、边框等宽度
|
||||||
lastWidth -= tagWidth + 36; // 36px是标签的边距、边框等宽度
|
}
|
||||||
} else {
|
if (lastWidth < 30) {
|
||||||
lastWidth = 0; // 当剩余宽度已经放不下刚添加的标签,则剩余宽度置为 0,避免后面再进行计算
|
// 30px 是隐藏的输入搜索框的宽度+边距+容错宽度
|
||||||
break;
|
maxTagCount.value = Math.max(1, i - 1);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
maxTagCount.value = Infinity;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (lastWidth === 0) {
|
|
||||||
maxTagCount.value = tagCount || 1;
|
|
||||||
}
|
|
||||||
if (tagCount <= 1 && values.length > 0) {
|
|
||||||
singleTagMaxWidth.value = innerViewWidth - 100; // 100px 是 60px + 标签边距边框和 x 图标等40px
|
|
||||||
} else {
|
|
||||||
singleTagMaxWidth.value = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -210,7 +210,7 @@
|
||||||
background-color: var(--color-bg-3);
|
background-color: var(--color-bg-3);
|
||||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
.arco-layout-content {
|
.arco-layout-content {
|
||||||
padding: 16px 16px 0 0;
|
padding: 8px 16px 0 0;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ export default {
|
||||||
'common.expandAll': 'Expand all',
|
'common.expandAll': 'Expand all',
|
||||||
'common.copy': 'Copy',
|
'common.copy': 'Copy',
|
||||||
'common.copySuccess': 'Copy successfully',
|
'common.copySuccess': 'Copy successfully',
|
||||||
|
'common.copyNotSupport': 'Your browser does not support automatic copying. Please copy manually.',
|
||||||
'common.fork': 'Fork',
|
'common.fork': 'Fork',
|
||||||
'common.forked': 'Forked',
|
'common.forked': 'Forked',
|
||||||
'common.more': 'More',
|
'common.more': 'More',
|
||||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
||||||
'settings.help.guide': 'Use Guide',
|
'settings.help.guide': 'Use Guide',
|
||||||
'settings.help.doc': 'Help docs',
|
'settings.help.doc': 'Help docs',
|
||||||
'settings.help.APIDoc': 'API docs',
|
'settings.help.APIDoc': 'API docs',
|
||||||
|
'settings.help.versionInfo': 'Version info',
|
||||||
'settings.help.version': 'Version',
|
'settings.help.version': 'Version',
|
||||||
'settings.menu': 'Menu',
|
'settings.menu': 'Menu',
|
||||||
'settings.tabBar': 'Tab Bar',
|
'settings.tabBar': 'Tab Bar',
|
||||||
|
|
|
@ -77,6 +77,7 @@ export default {
|
||||||
'common.expandAll': '展开全部',
|
'common.expandAll': '展开全部',
|
||||||
'common.copy': '复制',
|
'common.copy': '复制',
|
||||||
'common.copySuccess': '复制成功',
|
'common.copySuccess': '复制成功',
|
||||||
|
'common.copyNotSupport': '您的浏览器不支持自动复制,请手动复制',
|
||||||
'common.fork': '关注',
|
'common.fork': '关注',
|
||||||
'common.forked': '已关注',
|
'common.forked': '已关注',
|
||||||
'common.more': '更多',
|
'common.more': '更多',
|
||||||
|
|
|
@ -15,7 +15,8 @@ export default {
|
||||||
'settings.help.guide': '新手指引',
|
'settings.help.guide': '新手指引',
|
||||||
'settings.help.doc': '帮助文档',
|
'settings.help.doc': '帮助文档',
|
||||||
'settings.help.APIDoc': 'API文档',
|
'settings.help.APIDoc': 'API文档',
|
||||||
'settings.help.version': '版本信息',
|
'settings.help.versionInfo': '版本信息',
|
||||||
|
'settings.help.version': '版本',
|
||||||
'settings.menu': '菜单栏',
|
'settings.menu': '菜单栏',
|
||||||
'settings.tabBar': '多页签',
|
'settings.tabBar': '多页签',
|
||||||
'settings.footer': '底部',
|
'settings.footer': '底部',
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { BatchApiParams, TableQueryParams } from '@/models/common';
|
import { BatchApiParams, TableQueryParams } from '@/models/common';
|
||||||
|
|
||||||
|
export type FileStorageType = 'GIT' | 'MINIO';
|
||||||
// 文件列表查询参数
|
// 文件列表查询参数
|
||||||
export interface FileListQueryParams extends TableQueryParams {
|
export interface FileListQueryParams extends TableQueryParams {
|
||||||
moduleIds: string[];
|
moduleIds: string[];
|
||||||
|
@ -25,13 +26,13 @@ export interface FileItem {
|
||||||
branch?: string; // 分支
|
branch?: string; // 分支
|
||||||
filePath?: string; // 文件路径
|
filePath?: string; // 文件路径
|
||||||
fileVersion?: string; // 文件版本
|
fileVersion?: string; // 文件版本
|
||||||
|
storage?: FileStorageType; // 存储方式
|
||||||
}
|
}
|
||||||
// 文件详情
|
// 文件详情
|
||||||
export interface FileDetail extends FileItem {
|
export interface FileDetail extends FileItem {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
moduleName: string; // 所属模块名
|
moduleName: string; // 所属模块名
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
storage?: string; // 存储方式
|
|
||||||
createUser: string;
|
createUser: string;
|
||||||
createTime: number;
|
createTime: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,3 +120,8 @@ export interface RegisterByInviteParams {
|
||||||
password: string;
|
password: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateUserResult {
|
||||||
|
errorEmails: Record<string, any>;
|
||||||
|
successList: any[];
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,16 @@ const ApiTest: AppRouteRecordRaw = {
|
||||||
isTopMenu: true,
|
isTopMenu: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'management',
|
||||||
|
name: ApiTestRouteEnum.API_TEST_MANAGEMENT,
|
||||||
|
component: () => import('@/views/api-test/management/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.apiTest.management',
|
||||||
|
roles: ['*'],
|
||||||
|
isTopMenu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ const ProjectManagement: AppRouteRecordRaw = {
|
||||||
component: () => import('@/views/project-management/projectAndPermission/projectVersion/index.vue'),
|
component: () => import('@/views/project-management/projectAndPermission/projectVersion/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'project.permission.projectVersion',
|
locale: 'project.permission.projectVersion',
|
||||||
roles: ['*'],
|
roles: ['PROJECT_VERSION:READ'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 成员
|
// 成员
|
||||||
|
@ -252,7 +252,7 @@ const ProjectManagement: AppRouteRecordRaw = {
|
||||||
component: () => import('@/views/project-management/messageManagement/edit.vue'),
|
component: () => import('@/views/project-management/messageManagement/edit.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.projectManagement.messageManagementEdit',
|
locale: 'menu.projectManagement.messageManagementEdit',
|
||||||
roles: ['*'],
|
roles: ['PROJECT_MESSAGE:READ+UPDATE'],
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT,
|
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
|
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
|
||||||
|
|
||||||
import { getProjectInfo } from '@/api/modules/project-management/basicInfo';
|
import { getProjectList } from '@/api/modules/project-management/project';
|
||||||
import { getPageConfig } from '@/api/modules/setting/config';
|
import { getPageConfig } from '@/api/modules/setting/config';
|
||||||
import { getPackageType, getSystemVersion } from '@/api/modules/system';
|
import { getPackageType, getSystemVersion } from '@/api/modules/system';
|
||||||
import { getMenuList } from '@/api/modules/user';
|
import { getMenuList } from '@/api/modules/user';
|
||||||
|
@ -13,6 +13,7 @@ import { useI18n } from '@/hooks/useI18n';
|
||||||
import { watchStyle, watchTheme } from '@/utils/theme';
|
import { watchStyle, watchTheme } from '@/utils/theme';
|
||||||
|
|
||||||
import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config';
|
import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config';
|
||||||
|
import { ProjectListItem } from '@/models/setting/project';
|
||||||
|
|
||||||
import type { AppState } from './types';
|
import type { AppState } from './types';
|
||||||
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface';
|
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface';
|
||||||
|
@ -59,6 +60,7 @@ const useAppStore = defineStore('app', {
|
||||||
...defaultPlatformConfig,
|
...defaultPlatformConfig,
|
||||||
},
|
},
|
||||||
packageType: '',
|
packageType: '',
|
||||||
|
projectList: [] as ProjectListItem[],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -218,6 +220,7 @@ const useAppStore = defineStore('app', {
|
||||||
try {
|
try {
|
||||||
this.version = await getSystemVersion();
|
this.version = await getSystemVersion();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -228,6 +231,7 @@ const useAppStore = defineStore('app', {
|
||||||
try {
|
try {
|
||||||
this.packageType = await getPackageType();
|
this.packageType = await getPackageType();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -293,6 +297,7 @@ const useAppStore = defineStore('app', {
|
||||||
window.document.title = this.pageConfig.title;
|
window.document.title = this.pageConfig.title;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -303,6 +308,19 @@ const useAppStore = defineStore('app', {
|
||||||
async setCurrentMenuConfig(menuConfig: string[]) {
|
async setCurrentMenuConfig(menuConfig: string[]) {
|
||||||
this.currentMenuConfig = menuConfig;
|
this.currentMenuConfig = menuConfig;
|
||||||
},
|
},
|
||||||
|
async initProjectList() {
|
||||||
|
try {
|
||||||
|
if (this.currentOrgId) {
|
||||||
|
const res = await getProjectList(this.getCurrentOrgId);
|
||||||
|
this.projectList = res;
|
||||||
|
} else {
|
||||||
|
this.projectList = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
paths: ['currentOrgId', 'currentProjectId', 'pageConfig'],
|
paths: ['currentOrgId', 'currentProjectId', 'pageConfig'],
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||||
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
|
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
|
||||||
|
|
||||||
import type { LoginConfig, PageConfig, PlatformConfig, ThemeConfig } from '@/models/setting/config';
|
import type { LoginConfig, PageConfig, PlatformConfig, ThemeConfig } from '@/models/setting/config';
|
||||||
import { UserGroupAuthSetting } from '@/models/setting/usergroup';
|
import { ProjectListItem } from '@/models/setting/project';
|
||||||
|
|
||||||
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ export interface AppState {
|
||||||
innerHeight: number;
|
innerHeight: number;
|
||||||
currentMenuConfig: string[];
|
currentMenuConfig: string[];
|
||||||
packageType: string;
|
packageType: string;
|
||||||
|
projectList: ProjectListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadFileTaskState {
|
export interface UploadFileTaskState {
|
||||||
|
|
|
@ -111,8 +111,11 @@ const useUserStore = defineStore('user', {
|
||||||
this.logoutCallBack();
|
this.logoutCallBack();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 是否已经登录
|
/**
|
||||||
async isLogin() {
|
* 判断用户是否登录并设置用户信息
|
||||||
|
* @param forceSet 是否强制设置项目 id 和组织 id,用于切换组织和项目时重写用户信息
|
||||||
|
*/
|
||||||
|
async isLogin(forceSet = false) {
|
||||||
try {
|
try {
|
||||||
const res = await userIsLogin();
|
const res = await userIsLogin();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -120,10 +123,10 @@ const useUserStore = defineStore('user', {
|
||||||
this.setInfo(res);
|
this.setInfo(res);
|
||||||
const { organizationId, projectId } = getHashParameters();
|
const { organizationId, projectId } = getHashParameters();
|
||||||
// 如果访问页面的时候携带了组织 ID和项目 ID,则不设置
|
// 如果访问页面的时候携带了组织 ID和项目 ID,则不设置
|
||||||
if (!organizationId) {
|
if (!organizationId || forceSet) {
|
||||||
appStore.setCurrentOrgId(res.lastOrganizationId || '');
|
appStore.setCurrentOrgId(res.lastOrganizationId || '');
|
||||||
}
|
}
|
||||||
if (!projectId) {
|
if (!projectId || forceSet) {
|
||||||
appStore.setCurrentProjectId(res.lastProjectId || '');
|
appStore.setCurrentProjectId(res.lastProjectId || '');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import JSEncrypt from 'jsencrypt';
|
import JSEncrypt from 'jsencrypt';
|
||||||
|
|
||||||
import { codeCharset } from '@/config/apiTest';
|
|
||||||
|
|
||||||
import { isObject } from './is';
|
import { isObject } from './is';
|
||||||
|
|
||||||
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
|
<MsTag
|
||||||
|
v-if="props.isTag"
|
||||||
|
:self-style="{ border: `1px solid ${methodColor}`, color: methodColor, backgroundColor: 'white' }"
|
||||||
|
>
|
||||||
|
{{ props.method }}
|
||||||
|
</MsTag>
|
||||||
|
<div v-else class="font-medium" :style="{ color: methodColor }">{{ props.method }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||||
|
|
||||||
import { RequestMethods } from '@/enums/apiEnum';
|
import { RequestMethods } from '@/enums/apiEnum';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
method: RequestMethods;
|
method: RequestMethods;
|
||||||
|
isTag?: boolean; // 是否展示为标签
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const colorMaps = [
|
const colorMaps = [
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
v-model:model-value="method"
|
||||||
|
:placeholder="t('common.pleaseSelect')"
|
||||||
|
@change="(val) => emit('change', val as string)"
|
||||||
|
>
|
||||||
|
<template #label="{ data }">
|
||||||
|
<apiMethodName :method="data.value" class="inline-block" />
|
||||||
|
</template>
|
||||||
|
<a-option v-for="item of RequestMethods" :key="item" :value="item">
|
||||||
|
<apiMethodName :method="item" />
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { RequestMethods } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
(e: 'change', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const method = useVModel(props, 'modelValue', emit);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<MsTag :self-style="status.style"> {{ status.text }}</MsTag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { RequestDefinitionStatus } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
status: RequestDefinitionStatus;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
[RequestDefinitionStatus.DEPRECATED]: {
|
||||||
|
bgColor: 'var(--color-text-n8)',
|
||||||
|
color: 'var(--color-text-4)',
|
||||||
|
text: 'apiTestManagement.deprecate',
|
||||||
|
},
|
||||||
|
[RequestDefinitionStatus.PROCESSING]: {
|
||||||
|
bgColor: 'rgb(var(--link-2))',
|
||||||
|
color: 'rgb(var(--link-5))',
|
||||||
|
text: 'apiTestManagement.processing',
|
||||||
|
},
|
||||||
|
[RequestDefinitionStatus.DEBUGGING]: {
|
||||||
|
bgColor: 'rgb(var(--link-2))',
|
||||||
|
color: 'rgb(var(--link-6))',
|
||||||
|
text: 'apiTestManagement.debugging',
|
||||||
|
},
|
||||||
|
[RequestDefinitionStatus.DONE]: {
|
||||||
|
bgColor: 'rgb(var(--success-2))',
|
||||||
|
color: 'rgb(var(--success-6))',
|
||||||
|
text: 'apiTestManagement.done',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const status = computed(() => {
|
||||||
|
const config = statusMap[props.status];
|
||||||
|
return {
|
||||||
|
style: {
|
||||||
|
backgroundColor: config.bgColor,
|
||||||
|
color: config.color,
|
||||||
|
},
|
||||||
|
text: t(config.text),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -66,7 +66,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mode: 'add' | 'rename' | 'fileRename' | 'fileUpdateDesc' | 'repositoryRename';
|
mode: 'add' | 'rename';
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
allNames: string[];
|
allNames: string[];
|
|
@ -26,14 +26,11 @@
|
||||||
@change="handleActiveDebugChange"
|
@change="handleActiveDebugChange"
|
||||||
/>
|
/>
|
||||||
<a-input-group class="flex-1">
|
<a-input-group class="flex-1">
|
||||||
<a-select v-model:model-value="activeDebug.method" class="w-[140px]" @change="handleActiveDebugChange">
|
<apiMethodSelect
|
||||||
<template #label="{ data }">
|
v-model:model-value="activeDebug.method"
|
||||||
<apiMethodName :method="data.value" class="inline-block" />
|
class="w-[140px]"
|
||||||
</template>
|
@change="handleActiveDebugChange"
|
||||||
<a-option v-for="method of RequestMethods" :key="method" :value="method">
|
/>
|
||||||
<apiMethodName :method="method" />
|
|
||||||
</a-option>
|
|
||||||
</a-select>
|
|
||||||
<a-input
|
<a-input
|
||||||
v-model:model-value="activeDebug.url"
|
v-model:model-value="activeDebug.url"
|
||||||
:max-length="255"
|
:max-length="255"
|
||||||
|
@ -198,6 +195,7 @@
|
||||||
import debugRest from './rest.vue';
|
import debugRest from './rest.vue';
|
||||||
import debugSetting from './setting.vue';
|
import debugSetting from './setting.vue';
|
||||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
|
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
|
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
import popConfirm from './popConfirm.vue';
|
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
||||||
|
|
||||||
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview';
|
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
@ -336,7 +336,7 @@
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.folder {
|
.folder {
|
||||||
@apply flex cursor-pointer items-center justify-between;
|
@apply flex items-center justify-between;
|
||||||
|
|
||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
|
@ -344,7 +344,7 @@
|
||||||
background-color: rgb(var(--primary-1));
|
background-color: rgb(var(--primary-1));
|
||||||
}
|
}
|
||||||
.folder-text {
|
.folder-text {
|
||||||
@apply flex cursor-pointer items-center;
|
@apply flex items-center;
|
||||||
.folder-icon {
|
.folder-icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--color-text-4);
|
color: var(--color-text-4);
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default {
|
||||||
'apiTestDebug.storageByResult': '按结果存储',
|
'apiTestDebug.storageByResult': '按结果存储',
|
||||||
'apiTestDebug.storageByResultPlaceholder': '如 {a}',
|
'apiTestDebug.storageByResultPlaceholder': '如 {a}',
|
||||||
'apiTestDebug.extractParameter': '提取参数',
|
'apiTestDebug.extractParameter': '提取参数',
|
||||||
'apiTestDebug.searchTip': '请输入分组名称',
|
'apiTestDebug.searchTip': '请输入模块/请求名称',
|
||||||
'apiTestDebug.allRequest': '全部请求',
|
'apiTestDebug.allRequest': '全部请求',
|
||||||
'apiTestDebug.deleteFolderTipTitle': '是否删除 `{name}` 模块?',
|
'apiTestDebug.deleteFolderTipTitle': '是否删除 `{name}` 模块?',
|
||||||
'apiTestDebug.deleteFolderTipContent': '该操作会删除模块及其下所有资源,请谨慎操作!',
|
'apiTestDebug.deleteFolderTipContent': '该操作会删除模块及其下所有资源,请谨慎操作!',
|
||||||
|
|
|
@ -0,0 +1,409 @@
|
||||||
|
<template>
|
||||||
|
<MsDrawer
|
||||||
|
v-model:visible="visible"
|
||||||
|
width="100%"
|
||||||
|
:popup-container="props.popupContainer"
|
||||||
|
:closable="false"
|
||||||
|
:ok-disabled="disabledConfirm"
|
||||||
|
disabled-width-drag
|
||||||
|
no-title
|
||||||
|
@confirm="confirmImport"
|
||||||
|
@cancel="cancelImport"
|
||||||
|
>
|
||||||
|
<template #title> </template>
|
||||||
|
<div class="flex items-center justify-between p-[12px_8px]">
|
||||||
|
<div class="font-medium text-[var(--color-text-1)]">{{ t('apiTestManagement.importApi') }}</div>
|
||||||
|
<a-radio-group v-model:model-value="importType" type="button">
|
||||||
|
<a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio>
|
||||||
|
<a-radio value="time">{{ t('apiTestManagement.timeImport') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="importType === 'file'"
|
||||||
|
class="my-[16px] flex items-center gap-[16px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item of importFormatList"
|
||||||
|
:key="item.value"
|
||||||
|
:class="`import-item ${importFormat === item.value ? 'import-item--active' : ''}`"
|
||||||
|
@click="() => setActiveImportFormat(item.value)"
|
||||||
|
>
|
||||||
|
<div class="flex h-[24px] w-[24px] items-center justify-center rounded-[var(--border-radius-small)] bg-white">
|
||||||
|
<MsIcon :type="item.icon" :class="`text-[${item.iconColor}]`" :size="18" />
|
||||||
|
</div>
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ item.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-form ref="importFormRef" :model="importForm" layout="vertical">
|
||||||
|
<template v-if="importType === 'file'">
|
||||||
|
<a-form-item :label="t('apiTestManagement.belongModule')">
|
||||||
|
<a-tree-select
|
||||||
|
v-model:modelValue="importForm.module"
|
||||||
|
:data="moduleTree"
|
||||||
|
class="w-[436px]"
|
||||||
|
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||||
|
allow-search
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex items-center gap-[2px]">
|
||||||
|
{{ t('apiTestManagement.importMode') }}
|
||||||
|
<a-tooltip position="right">
|
||||||
|
<icon-question-circle
|
||||||
|
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip1') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip2') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip3') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip4') }}</div>
|
||||||
|
<div class="h-[22px] w-full"></div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip5') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip6') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip7') }}</div>
|
||||||
|
</template>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-select v-model:model-value="importForm.mode" class="w-[240px]">
|
||||||
|
<a-option value="cover">{{ t('apiTestManagement.cover') }}</a-option>
|
||||||
|
<a-option value="uncover">{{ t('apiTestManagement.uncover') }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false">
|
||||||
|
<a-collapse-item :key="1">
|
||||||
|
<template #header>
|
||||||
|
<MsButton
|
||||||
|
type="text"
|
||||||
|
@click="() => (moreSettingActive.length > 0 ? (moreSettingActive = []) : (moreSettingActive = [1]))"
|
||||||
|
>
|
||||||
|
{{ t('apiTestDebug.moreSetting') }}
|
||||||
|
<icon-down v-if="moreSettingActive.length > 0" class="text-rgb(var(--primary-5))" />
|
||||||
|
<icon-right v-else class="text-rgb(var(--primary-5))" />
|
||||||
|
</MsButton>
|
||||||
|
</template>
|
||||||
|
<div class="mt-[16px]">
|
||||||
|
<a-checkbox v-model:model-value="importForm.syncImportCase" class="mr-[24px]">
|
||||||
|
{{ t('apiTestManagement.syncImportCase') }}
|
||||||
|
</a-checkbox>
|
||||||
|
<a-checkbox v-model:model-value="importForm.syncUpdateDirectory">
|
||||||
|
{{ t('apiTestManagement.syncUpdateDirectory') }}
|
||||||
|
</a-checkbox>
|
||||||
|
</div>
|
||||||
|
</a-collapse-item>
|
||||||
|
</a-collapse>
|
||||||
|
<a-form-item :label="t('apiTestManagement.importType')" class="mt-[8px]">
|
||||||
|
<a-radio-group v-model:model-value="importForm.importType" type="button">
|
||||||
|
<a-radio value="file">{{ t('apiTestManagement.fileImport') }}</a-radio>
|
||||||
|
<a-radio value="url">{{ t('apiTestManagement.urlImport') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<MsUpload
|
||||||
|
v-if="importForm.importType === 'file'"
|
||||||
|
v-model:file-list="importForm.file"
|
||||||
|
accept="json"
|
||||||
|
:auto-upload="false"
|
||||||
|
draggable
|
||||||
|
size-unit="MB"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #subText>
|
||||||
|
<div class="flex">
|
||||||
|
{{ t('apiTestManagement.importSwaggerFileTip1') }}
|
||||||
|
<div class="text-[rgb(var(--warning-6))]">{{ t('apiTestManagement.importSwaggerFileTip2') }}</div>
|
||||||
|
{{ t('apiTestManagement.importSwaggerFileTip3') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsUpload>
|
||||||
|
<template v-else>
|
||||||
|
<a-form-item
|
||||||
|
field="url"
|
||||||
|
label="SwaggerURL"
|
||||||
|
asterisk-position="end"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="importForm.url"
|
||||||
|
:placeholder="t('apiTestManagement.urlImportPlaceholder')"
|
||||||
|
class="w-[700px]"
|
||||||
|
allow-clear
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="mb-[16px] flex items-center gap-[8px]">
|
||||||
|
<a-switch v-model:model-value="importForm.basicAuth" type="line" size="small"></a-switch>
|
||||||
|
{{ t('apiTestManagement.basicAuth') }}
|
||||||
|
</div>
|
||||||
|
<template v-if="importForm.basicAuth">
|
||||||
|
<a-form-item
|
||||||
|
field="account"
|
||||||
|
:label="t('apiTestManagement.account')"
|
||||||
|
asterisk-position="end"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="importForm.account"
|
||||||
|
:placeholder="t('common.pleaseInput')"
|
||||||
|
class="w-[500px]"
|
||||||
|
allow-clear
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="password"
|
||||||
|
:label="t('apiTestManagement.password')"
|
||||||
|
asterisk-position="end"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
<a-input-password
|
||||||
|
v-model:model-value="importForm.password"
|
||||||
|
:placeholder="t('common.pleaseInput')"
|
||||||
|
class="w-[500px]"
|
||||||
|
autocomplete="new-password"
|
||||||
|
allow-clear
|
||||||
|
></a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-form-item
|
||||||
|
field="taskName"
|
||||||
|
:label="t('apiTestManagement.taskName')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.taskNameRequired') }]"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center gap-[8px]">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="importForm.taskName"
|
||||||
|
:placeholder="t('apiTestManagement.taskNamePlaceholder')"
|
||||||
|
:max-length="255"
|
||||||
|
class="flex-1"
|
||||||
|
></a-input>
|
||||||
|
<MsButton type="text">{{ t('apiTestManagement.timeTaskList') }}</MsButton>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="url"
|
||||||
|
label="SwaggerURL"
|
||||||
|
asterisk-position="end"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.swaggerURLRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="importForm.url"
|
||||||
|
:placeholder="t('apiTestManagement.urlImportPlaceholder')"
|
||||||
|
class="w-[700px]"
|
||||||
|
allow-clear
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="mb-[16px] flex items-center gap-[8px]">
|
||||||
|
<a-switch v-model:model-value="importForm.basicAuth" type="line" size="small"></a-switch>
|
||||||
|
{{ t('apiTestManagement.basicAuth') }}
|
||||||
|
</div>
|
||||||
|
<template v-if="importForm.basicAuth">
|
||||||
|
<a-form-item
|
||||||
|
field="account"
|
||||||
|
:label="t('apiTestManagement.account')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.accountRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="importForm.account"
|
||||||
|
:placeholder="t('common.pleaseInput')"
|
||||||
|
class="w-[500px]"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="password"
|
||||||
|
:label="t('apiTestManagement.password')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.passwordRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
<a-input-password
|
||||||
|
v-model:model-value="importForm.password"
|
||||||
|
:placeholder="t('common.pleaseInput')"
|
||||||
|
class="w-[500px]"
|
||||||
|
autocomplete="new-password"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<a-form-item :label="t('apiTestManagement.belongModule')">
|
||||||
|
<a-tree-select
|
||||||
|
v-model:modelValue="importForm.module"
|
||||||
|
:data="moduleTree"
|
||||||
|
class="w-[436px]"
|
||||||
|
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||||
|
allow-search
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex items-center gap-[2px]">
|
||||||
|
{{ t('apiTestManagement.importMode') }}
|
||||||
|
<a-tooltip position="right">
|
||||||
|
<icon-question-circle
|
||||||
|
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip1') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip2') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip3') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip4') }}</div>
|
||||||
|
<div class="h-[22px] w-full"></div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip5') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip6') }}</div>
|
||||||
|
<div>{{ t('apiTestManagement.importModeTip7') }}</div>
|
||||||
|
</template>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-select v-model:model-value="importForm.mode" class="w-[240px]">
|
||||||
|
<a-option value="cover">{{ t('apiTestManagement.cover') }}</a-option>
|
||||||
|
<a-option value="uncover">{{ t('apiTestManagement.uncover') }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('apiTestManagement.syncFrequency')">
|
||||||
|
<a-select v-model:model-value="importForm.syncFrequency" class="w-[240px]">
|
||||||
|
<template #label="{ data }">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{ data.value }}
|
||||||
|
<div class="ml-[4px] text-[var(--color-text-4)]">{{ data.label.split('?')[1] }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value" class="block">
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
{{ item.value }}
|
||||||
|
<div class="ml-[4px] text-[var(--color-text-4)]">{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</a-option>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center p-[4px_8px]">
|
||||||
|
<MsButton type="text">{{ t('apiTestManagement.customFrequency') }}</MsButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</a-form>
|
||||||
|
</MsDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { mapTree } from '@/utils';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||||
|
import { RequestImportFormat } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
moduleTree: ModuleTreeNode[];
|
||||||
|
popupContainer?: string;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['update:visible']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const visible = useVModel(props, 'visible', emit);
|
||||||
|
const importType = ref<'file' | 'time'>('file');
|
||||||
|
const importFormat = ref<keyof typeof RequestImportFormat>('SWAGGER');
|
||||||
|
const importFormatList = [
|
||||||
|
{
|
||||||
|
name: 'Swagger',
|
||||||
|
value: RequestImportFormat.SWAGGER,
|
||||||
|
icon: 'icon-icon_swagger',
|
||||||
|
iconColor: 'rgb(var(--success-7))',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function setActiveImportFormat(format: RequestImportFormat) {
|
||||||
|
importFormat.value = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
taskName: '',
|
||||||
|
module: 'root',
|
||||||
|
mode: 'cover',
|
||||||
|
syncImportCase: true,
|
||||||
|
syncUpdateDirectory: false,
|
||||||
|
importType: 'file',
|
||||||
|
file: [],
|
||||||
|
url: '',
|
||||||
|
basicAuth: false,
|
||||||
|
account: '',
|
||||||
|
password: '',
|
||||||
|
syncFrequency: '0 0 0/1 * ?',
|
||||||
|
};
|
||||||
|
const importForm = ref({ ...defaultForm });
|
||||||
|
const importFormRef = ref<FormInstance>();
|
||||||
|
const moreSettingActive = ref<number[]>([]);
|
||||||
|
const disabledConfirm = computed(() => {
|
||||||
|
if (importType.value === 'file') {
|
||||||
|
if (importForm.value.importType === 'file') {
|
||||||
|
return !importForm.value.file.length;
|
||||||
|
}
|
||||||
|
return !importForm.value.url;
|
||||||
|
}
|
||||||
|
return !importForm.value.taskName || !importForm.value.url;
|
||||||
|
});
|
||||||
|
const moduleTree = computed(() => mapTree(props.moduleTree, (node) => ({ ...node, draggable: false })));
|
||||||
|
const syncFrequencyOptions = [
|
||||||
|
{ label: t('apiTestManagement.timeTaskHour'), value: '0 0 0/1 * ?' },
|
||||||
|
{ label: t('apiTestManagement.timeTaskSixHour'), value: '0 0 0/6 * ?' },
|
||||||
|
{ label: t('apiTestManagement.timeTaskTwelveHour'), value: '0 0 0/12 * ?' },
|
||||||
|
{ label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * ?' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function cancelImport() {
|
||||||
|
visible.value = false;
|
||||||
|
importForm.value = { ...defaultForm };
|
||||||
|
importFormRef.value?.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmImport() {
|
||||||
|
importFormRef.value?.validate(async (errors) => {
|
||||||
|
if (!errors) {
|
||||||
|
try {
|
||||||
|
Message.success(t('common.importSuccess'));
|
||||||
|
cancelImport();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.import-item {
|
||||||
|
@apply flex cursor-pointer items-center bg-white;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
width: 200px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
}
|
||||||
|
.import-item--active {
|
||||||
|
border: 1px solid rgb(var(--primary-5));
|
||||||
|
background-color: rgb(var(--primary-1));
|
||||||
|
}
|
||||||
|
:deep(.arco-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
:deep(.arco-select-view-value::after) {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,821 @@
|
||||||
|
<template>
|
||||||
|
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
|
||||||
|
<MsEditableTab
|
||||||
|
v-model:active-tab="activeRequestTab"
|
||||||
|
v-model:tabs="apiTabs"
|
||||||
|
:more-action-list="tabMoreActionList"
|
||||||
|
@add="addDebugTab"
|
||||||
|
@close="closeDebugTab"
|
||||||
|
@change="setActiveDebug"
|
||||||
|
@more-action-select="handleTabMoreActionSelect"
|
||||||
|
>
|
||||||
|
<template #label="{ tab }">
|
||||||
|
<apiMethodName v-if="tab.id !== 'all'" :method="tab.method" class="mr-[4px]" />
|
||||||
|
{{ tab.label }}
|
||||||
|
</template>
|
||||||
|
</MsEditableTab>
|
||||||
|
</div>
|
||||||
|
<div class="p-[16px_22px]">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-[8px]">
|
||||||
|
<a-switch v-model:model-value="showSubdirectory" size="small" type="line"></a-switch>
|
||||||
|
{{ t('apiTestManagement.showSubdirectory') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-[8px]">
|
||||||
|
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
|
||||||
|
<template #icon>
|
||||||
|
<icon-location class="text-[var(--color-text-4)]" />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
<MsSelect
|
||||||
|
v-model:model-value="checkedEnv"
|
||||||
|
mode="static"
|
||||||
|
:options="envOptions"
|
||||||
|
class="w-[200px]"
|
||||||
|
:search-keys="['label']"
|
||||||
|
allow-search
|
||||||
|
/>
|
||||||
|
<a-input-search
|
||||||
|
v-model:model-value="keyword"
|
||||||
|
:placeholder="t('apiTestManagement.searchPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
class="mr-[8px] w-[240px]"
|
||||||
|
@search="loadApiList"
|
||||||
|
@press-enter="loadApiList"
|
||||||
|
/>
|
||||||
|
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
|
||||||
|
<template #icon>
|
||||||
|
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ms-base-table
|
||||||
|
v-bind="propsRes"
|
||||||
|
:action-config="batchActions"
|
||||||
|
no-disable
|
||||||
|
filter-icon-align-left
|
||||||
|
v-on="propsEvent"
|
||||||
|
@selected-change="handleTableSelect"
|
||||||
|
@batch-action="handleTableBatch"
|
||||||
|
>
|
||||||
|
<template #typeFilter="{ columnConfig }">
|
||||||
|
<a-trigger v-model:popup-visible="typeFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
|
||||||
|
<a-button type="text" class="arco-btn-text--secondary" @click="typeFilterVisible = true">
|
||||||
|
{{ t(columnConfig.title as string) }}
|
||||||
|
<icon-down :class="typeFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<div class="arco-table-filters-content">
|
||||||
|
<div class="flex items-center justify-center px-[6px] py-[2px]">
|
||||||
|
<a-checkbox-group v-model:model-value="typeFilters" direction="vertical" size="small">
|
||||||
|
<a-checkbox v-for="key of RequestMethods" :key="key" :value="key">
|
||||||
|
<apiMethodName :method="key" />
|
||||||
|
</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-trigger>
|
||||||
|
</template>
|
||||||
|
<template #statusFilter="{ columnConfig }">
|
||||||
|
<a-trigger
|
||||||
|
v-model:popup-visible="statusFilterVisible"
|
||||||
|
trigger="click"
|
||||||
|
@popup-visible-change="handleFilterHidden"
|
||||||
|
>
|
||||||
|
<a-button type="text" class="arco-btn-text--secondary" @click="statusFilterVisible = true">
|
||||||
|
{{ t(columnConfig.title as string) }}
|
||||||
|
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<div class="arco-table-filters-content">
|
||||||
|
<div class="flex items-center justify-center px-[6px] py-[2px]">
|
||||||
|
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
|
||||||
|
<a-checkbox v-for="key of RequestDefinitionStatus" :key="key" :value="key">
|
||||||
|
<apiStatus :status="key" />
|
||||||
|
</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-trigger>
|
||||||
|
</template>
|
||||||
|
<template #type="{ record }">
|
||||||
|
<apiMethodName :method="record.type" is-tag />
|
||||||
|
</template>
|
||||||
|
<template #status="{ record }">
|
||||||
|
<a-select
|
||||||
|
v-model:model-value="record.status"
|
||||||
|
:placeholder="t('common.pleaseSelect')"
|
||||||
|
class="param-input w-full"
|
||||||
|
@change="() => handleStatusChange(record)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<apiStatus :status="record.status" />
|
||||||
|
</template>
|
||||||
|
<a-option v-for="item of RequestDefinitionStatus" :key="item" :value="item">
|
||||||
|
<apiStatus :status="item" />
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
<template #action="{ record }">
|
||||||
|
<MsButton type="text" class="!mr-0">
|
||||||
|
{{ t('apiTestManagement.execute') }}
|
||||||
|
</MsButton>
|
||||||
|
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||||
|
<MsButton type="text" class="!mr-0">
|
||||||
|
{{ t('common.copy') }}
|
||||||
|
</MsButton>
|
||||||
|
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||||
|
<MsTableMoreAction :list="tableMoreActionList" @select="handleTableMoreActionSelect($event, record)" />
|
||||||
|
</template>
|
||||||
|
</ms-base-table>
|
||||||
|
</div>
|
||||||
|
<a-modal v-model:visible="showBatchModal" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
|
||||||
|
<template #title>
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
<div class="text-[var(--color-text-4)]">
|
||||||
|
{{
|
||||||
|
t('apiTestManagement.batchModalSubTitle', {
|
||||||
|
count: batchParams.currentSelectCount || tableSelected.length,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-form ref="batchFormRef" class="rounded-[4px]" :model="batchForm" layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
field="attr"
|
||||||
|
:label="t('apiTestManagement.chooseAttr')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.attrRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-select v-model="batchForm.attr" :placeholder="t('common.pleaseSelect')">
|
||||||
|
<a-option v-for="item of attrOptions" :key="item.value" :value="item.value">
|
||||||
|
{{ t(item.name) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="batchForm.attr === 'tag'"
|
||||||
|
field="values"
|
||||||
|
:label="t('apiTestManagement.batchUpdate')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<MsTagsInput
|
||||||
|
v-model:model-value="batchForm.values"
|
||||||
|
placeholder="common.tagsInputPlaceholder"
|
||||||
|
allow-clear
|
||||||
|
unique-value
|
||||||
|
retain-input-value
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-else
|
||||||
|
field="value"
|
||||||
|
:label="t('apiTestManagement.batchUpdate')"
|
||||||
|
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<apiMethodSelect
|
||||||
|
v-if="batchForm.attr === 'type'"
|
||||||
|
v-model:model-value="batchForm.value"
|
||||||
|
@change="handleActiveDebugChange"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-else
|
||||||
|
v-model="batchForm.value"
|
||||||
|
:placeholder="t('common.pleaseSelect')"
|
||||||
|
:disabled="batchForm.attr === ''"
|
||||||
|
>
|
||||||
|
<a-option v-for="item of valueOptions" :key="item.value" :value="item.value">
|
||||||
|
{{ t(item.name) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<template #footer>
|
||||||
|
<a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :loading="batchUpdateLoading" @click="batchUpdate">
|
||||||
|
{{ t('common.update') }}
|
||||||
|
</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('apiTestManagement.batchMoveConfirm')"
|
||||||
|
:ok-button-props="{ disabled: selectedModuleKeys.length === 0 }"
|
||||||
|
:cancel-button-props="{ disabled: batchMoveApiLoading }"
|
||||||
|
:on-before-ok="handleApiMove"
|
||||||
|
@close="handleMoveApiModalCancel"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{ isBatchMove ? t('common.batchMove') : t('common.move') }}
|
||||||
|
<div
|
||||||
|
class="one-line-text ml-[4px] max-w-[70%] text-[var(--color-text-4)]"
|
||||||
|
:title="
|
||||||
|
isBatchMove
|
||||||
|
? t('apiTestManagement.batchModalSubTitle', { count: tableSelected.length })
|
||||||
|
: `(${activeApi?.name})`
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
isBatchMove
|
||||||
|
? t('apiTestManagement.batchModalSubTitle', { count: tableSelected.length })
|
||||||
|
: `(${activeApi?.name})`
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<moduleTree
|
||||||
|
v-if="moveModalVisible"
|
||||||
|
:is-expand-all="true"
|
||||||
|
is-modal
|
||||||
|
:active-module="props.activeModule"
|
||||||
|
@folder-node-select="folderNodeSelect"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||||
|
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||||
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
|
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||||
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
|
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||||
|
import MsSelect from '@/components/business/ms-select';
|
||||||
|
import moduleTree from '../moduleTree.vue';
|
||||||
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
|
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
||||||
|
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
import useTableStore from '@/hooks/useTableStore';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
|
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
|
||||||
|
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
module: string;
|
||||||
|
allCount: number;
|
||||||
|
activeModule: string;
|
||||||
|
offspringIds: string[];
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'init', params: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const activeRequestTab = ref<string | number>('all');
|
||||||
|
const apiTabs = ref<TabItem[]>([
|
||||||
|
{
|
||||||
|
id: 'all',
|
||||||
|
label: `${t('apiTestManagement.allApi')}(${props.allCount})`,
|
||||||
|
closable: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const activeApiTab = ref<TabItem>(apiTabs.value[0]);
|
||||||
|
|
||||||
|
function setActiveDebug(item: TabItem) {
|
||||||
|
activeApiTab.value = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleActiveDebugChange() {
|
||||||
|
activeApiTab.value.unSaved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDebugTab(defaultProps?: Partial<TabItem>) {
|
||||||
|
const id = `debug-${Date.now()}`;
|
||||||
|
apiTabs.value.push({
|
||||||
|
module: props.module,
|
||||||
|
label: t('apiTestDebug.newApi'),
|
||||||
|
id,
|
||||||
|
...defaultProps,
|
||||||
|
});
|
||||||
|
activeRequestTab.value = id;
|
||||||
|
nextTick(() => {
|
||||||
|
if (defaultProps) {
|
||||||
|
handleActiveDebugChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDebugTab(tab: TabItem) {
|
||||||
|
const index = apiTabs.value.findIndex((item) => item.id === tab.id);
|
||||||
|
apiTabs.value.splice(index, 1);
|
||||||
|
if (activeRequestTab.value === tab.id) {
|
||||||
|
activeRequestTab.value = apiTabs.value[0]?.id || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabMoreActionList = [
|
||||||
|
{
|
||||||
|
eventTag: 'closeAll',
|
||||||
|
label: t('apiTestManagement.closeAll'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventTag: 'closeOther',
|
||||||
|
label: t('apiTestManagement.closeOther'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleTabMoreActionSelect(event: ActionsItem) {
|
||||||
|
if (event.eventTag === 'closeOther') {
|
||||||
|
apiTabs.value = apiTabs.value.filter((item) => item.id === activeRequestTab.value || item.closable === false);
|
||||||
|
} else if (event.eventTag === 'closeAll') {
|
||||||
|
apiTabs.value = apiTabs.value.filter((item) => item.id === 'all');
|
||||||
|
activeRequestTab.value = 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSubdirectory = ref(false);
|
||||||
|
const checkedEnv = ref('DEV');
|
||||||
|
const envOptions = ref([
|
||||||
|
{
|
||||||
|
label: 'DEV',
|
||||||
|
value: 'DEV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TEST',
|
||||||
|
value: 'TEST',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PRE',
|
||||||
|
value: 'PRE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PROD',
|
||||||
|
value: 'PROD',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const keyword = ref('');
|
||||||
|
|
||||||
|
const columns: MsTableColumn = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'num',
|
||||||
|
sortIndex: 1,
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.apiName',
|
||||||
|
dataIndex: 'name',
|
||||||
|
showTooltip: true,
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.apiType',
|
||||||
|
dataIndex: 'type',
|
||||||
|
slotName: 'type',
|
||||||
|
titleSlotName: 'typeFilter',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.apiStatus',
|
||||||
|
dataIndex: 'status',
|
||||||
|
slotName: 'status',
|
||||||
|
titleSlotName: 'statusFilter',
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.responsiblePerson',
|
||||||
|
titleSlotName: 'responsiblePersonFilter',
|
||||||
|
showTooltip: true,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.path',
|
||||||
|
slotName: 'path',
|
||||||
|
dataIndex: 'path',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.tag',
|
||||||
|
dataIndex: 'tags',
|
||||||
|
isTag: true,
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.version',
|
||||||
|
dataIndex: 'version',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.createTime',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.updateTime',
|
||||||
|
dataIndex: 'updateTime',
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.operation',
|
||||||
|
slotName: 'action',
|
||||||
|
dataIndex: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const tableStore = useTableStore();
|
||||||
|
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
|
||||||
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||||
|
() =>
|
||||||
|
Promise.resolve({
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1001,
|
||||||
|
name: 'asdasdasd',
|
||||||
|
type: RequestMethods.CONNECT,
|
||||||
|
status: RequestDefinitionStatus.DEBUGGING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10011,
|
||||||
|
name: '1123',
|
||||||
|
type: RequestMethods.OPTIONS,
|
||||||
|
status: RequestDefinitionStatus.DEPRECATED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10012,
|
||||||
|
name: 'vfd',
|
||||||
|
type: RequestMethods.POST,
|
||||||
|
status: RequestDefinitionStatus.DONE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10013,
|
||||||
|
name: 'ccf',
|
||||||
|
type: RequestMethods.DELETE,
|
||||||
|
status: RequestDefinitionStatus.PROCESSING,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 0,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
tableKey: TableKeyEnum.API_TEST,
|
||||||
|
showSetting: true,
|
||||||
|
selectable: true,
|
||||||
|
showSelectAll: true,
|
||||||
|
draggable: { type: 'handle', width: 32 },
|
||||||
|
},
|
||||||
|
(item) => ({
|
||||||
|
...item,
|
||||||
|
createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const batchActions = {
|
||||||
|
baseAction: [
|
||||||
|
{
|
||||||
|
label: 'common.export',
|
||||||
|
eventTag: 'export',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common.edit',
|
||||||
|
eventTag: 'edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common.move',
|
||||||
|
eventTag: 'move',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
moreAction: [
|
||||||
|
{
|
||||||
|
label: 'common.delete',
|
||||||
|
eventTag: 'delete',
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const tableMoreActionList = [
|
||||||
|
{
|
||||||
|
eventTag: 'delete',
|
||||||
|
label: t('common.delete'),
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const typeFilterVisible = ref(false);
|
||||||
|
const typeFilters = ref(Object.keys(RequestMethods));
|
||||||
|
const statusFilterVisible = ref(false);
|
||||||
|
const statusFilters = ref(Object.keys(RequestDefinitionStatus));
|
||||||
|
const tableQueryParams = ref<any>();
|
||||||
|
function loadApiList() {
|
||||||
|
const params = {
|
||||||
|
keyword: keyword.value,
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule, ...props.offspringIds],
|
||||||
|
env: checkedEnv.value,
|
||||||
|
showSubdirectory: showSubdirectory.value,
|
||||||
|
filter: { status: statusFilters.value, type: typeFilters.value },
|
||||||
|
};
|
||||||
|
setLoadListParams(params);
|
||||||
|
loadList();
|
||||||
|
tableQueryParams.value = {
|
||||||
|
...params,
|
||||||
|
current: propsRes.value.msPagination?.current,
|
||||||
|
pageSize: propsRes.value.msPagination?.pageSize,
|
||||||
|
};
|
||||||
|
emit('init', {
|
||||||
|
...tableQueryParams.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterHidden(val: boolean) {
|
||||||
|
if (!val) {
|
||||||
|
loadApiList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStatusChange(record: any) {
|
||||||
|
try {
|
||||||
|
Message.success(t('common.updateSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
loadApiList();
|
||||||
|
});
|
||||||
|
|
||||||
|
function emitTableParams() {
|
||||||
|
emit('init', {
|
||||||
|
keyword: keyword.value,
|
||||||
|
moduleIds: [],
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
current: propsRes.value.msPagination?.current,
|
||||||
|
pageSize: propsRes.value.msPagination?.pageSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableSelected = ref<(string | number)[]>([]);
|
||||||
|
const batchParams = ref<BatchActionQueryParams>({
|
||||||
|
selectedIds: [],
|
||||||
|
selectAll: false,
|
||||||
|
excludeIds: [],
|
||||||
|
currentSelectCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除接口
|
||||||
|
*/
|
||||||
|
function deleteApi(record?: any, isBatch?: boolean, params?: BatchActionQueryParams) {
|
||||||
|
let title = t('apiTestManagement.deleteApiTipTitle', { name: record?.name });
|
||||||
|
let selectIds = [record?.id || ''];
|
||||||
|
if (isBatch) {
|
||||||
|
title = t('apiTestManagement.batchDeleteApiTip', {
|
||||||
|
count: params?.currentSelectCount || tableSelected.value.length,
|
||||||
|
});
|
||||||
|
selectIds = tableSelected.value as string[];
|
||||||
|
}
|
||||||
|
openModal({
|
||||||
|
type: 'error',
|
||||||
|
title,
|
||||||
|
content: t('apiTestManagement.deleteApiTip'),
|
||||||
|
okText: t('common.confirmDelete'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
maskClosable: false,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
try {
|
||||||
|
Message.success(t('common.deleteSuccess'));
|
||||||
|
resetSelector();
|
||||||
|
loadList();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理表格更多按钮事件
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
function handleTableMoreActionSelect(item: ActionsItem, record: any) {
|
||||||
|
switch (item.eventTag) {
|
||||||
|
case 'delete':
|
||||||
|
deleteApi(record);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理表格选中
|
||||||
|
*/
|
||||||
|
function handleTableSelect(arr: (string | number)[]) {
|
||||||
|
tableSelected.value = arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showBatchModal = ref(false);
|
||||||
|
const batchUpdateLoading = ref(false);
|
||||||
|
const batchFormRef = ref<FormInstance>();
|
||||||
|
const batchForm = ref({
|
||||||
|
attr: '',
|
||||||
|
value: '',
|
||||||
|
values: [],
|
||||||
|
});
|
||||||
|
const attrOptions = [
|
||||||
|
{
|
||||||
|
name: 'apiTestManagement.apiStatus',
|
||||||
|
value: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiTestManagement.apiType',
|
||||||
|
value: 'type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'common.tag',
|
||||||
|
value: 'tag',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const valueOptions = computed(() => {
|
||||||
|
switch (batchForm.value.attr) {
|
||||||
|
case 'status':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'apiTestManagement.processing',
|
||||||
|
value: RequestDefinitionStatus.PROCESSING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiTestManagement.done',
|
||||||
|
value: RequestDefinitionStatus.DONE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiTestManagement.deprecate',
|
||||||
|
value: RequestDefinitionStatus.DEPRECATED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiTestManagement.debugging',
|
||||||
|
value: RequestDefinitionStatus.DEBUGGING,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function cancelBatch() {
|
||||||
|
showBatchModal.value = false;
|
||||||
|
batchFormRef.value?.resetFields();
|
||||||
|
batchForm.value = {
|
||||||
|
attr: '',
|
||||||
|
value: '',
|
||||||
|
values: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function batchUpdate() {
|
||||||
|
batchFormRef.value?.validate(async (errors) => {
|
||||||
|
if (!errors) {
|
||||||
|
try {
|
||||||
|
batchUpdateLoading.value = true;
|
||||||
|
Message.success(t('common.updateSuccess'));
|
||||||
|
cancelBatch();
|
||||||
|
resetSelector();
|
||||||
|
loadList();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
batchUpdateLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveModalVisible = ref(false);
|
||||||
|
const selectedModuleKeys = ref<(string | number)[]>([]); // 移动文件选中节点
|
||||||
|
const isBatchMove = ref(false); // 是否批量移动文件
|
||||||
|
const activeApi = ref<any | null>(null); // 当前查看的接口项
|
||||||
|
const batchMoveApiLoading = ref(false); // 批量移动文件loading
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个/批量移动接口
|
||||||
|
*/
|
||||||
|
async function handleApiMove() {
|
||||||
|
try {
|
||||||
|
batchMoveApiLoading.value = true;
|
||||||
|
// await batchMoveFile({
|
||||||
|
// selectIds: isBatchMove.value ? batchParams.value?.selectedIds || [] : [activeApi.value?.id || ''],
|
||||||
|
// selectAll: !!batchParams.value?.selectAll,
|
||||||
|
// excludeIds: batchParams.value?.excludeIds || [],
|
||||||
|
// condition: { keyword: keyword.value },
|
||||||
|
// projectId: appStore.currentProjectId,
|
||||||
|
// moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||||
|
// moveModuleId: selectedModuleKeys.value[0],
|
||||||
|
// });
|
||||||
|
Message.success(t('apiTestManagement.batchMoveSuccess'));
|
||||||
|
if (isBatchMove.value) {
|
||||||
|
tableSelected.value = [];
|
||||||
|
isBatchMove.value = false;
|
||||||
|
} else {
|
||||||
|
activeApi.value = null;
|
||||||
|
}
|
||||||
|
loadList();
|
||||||
|
resetSelector();
|
||||||
|
emitTableParams();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
batchMoveApiLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveApiModalCancel() {
|
||||||
|
moveModalVisible.value = false;
|
||||||
|
selectedModuleKeys.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件夹树节点选中事件
|
||||||
|
*/
|
||||||
|
function folderNodeSelect(keys: (string | number)[]) {
|
||||||
|
selectedModuleKeys.value = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理表格选中后批量操作
|
||||||
|
* @param event 批量操作事件对象
|
||||||
|
*/
|
||||||
|
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
|
||||||
|
tableSelected.value = params?.selectedIds || [];
|
||||||
|
batchParams.value = params;
|
||||||
|
switch (event.eventTag) {
|
||||||
|
case 'delete':
|
||||||
|
deleteApi(undefined, true, params);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
showBatchModal.value = true;
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
isBatchMove.value = true;
|
||||||
|
moveModalVisible.value = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
|
||||||
|
&:not(:hover) {
|
||||||
|
border-color: transparent !important;
|
||||||
|
.arco-input::placeholder {
|
||||||
|
@apply invisible;
|
||||||
|
}
|
||||||
|
.arco-select-view-icon {
|
||||||
|
@apply invisible;
|
||||||
|
}
|
||||||
|
.arco-select-view-value {
|
||||||
|
color: var(--color-text-brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<a-tabs v-model:active-key="activeTab" animation lazy-load>
|
||||||
|
<a-tab-pane key="api" title="API">
|
||||||
|
<api
|
||||||
|
:active-module="activeModule"
|
||||||
|
:module="props.module"
|
||||||
|
:all-count="props.allCount"
|
||||||
|
:offspring-ids="props.offspringIds"
|
||||||
|
/>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="case" title="CASE"> </a-tab-pane>
|
||||||
|
<a-tab-pane key="mock" title="MOCK"> </a-tab-pane>
|
||||||
|
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')"> </a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import api from './api.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
module: string;
|
||||||
|
allCount: number;
|
||||||
|
activeModule: string;
|
||||||
|
offspringIds: string[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const activeTab = ref('api');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.arco-tabs-nav) {
|
||||||
|
border-bottom: 1px solid var(--color-text-n8);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,446 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="!props.isModal">
|
||||||
|
<a-select
|
||||||
|
v-model:model-value="moduleProtocol"
|
||||||
|
:options="moduleProtocolOptions"
|
||||||
|
class="mb-[8px]"
|
||||||
|
@change="(val) => handleProtocolChange(val as string)"
|
||||||
|
/>
|
||||||
|
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||||
|
<a-input v-model:model-value="moduleKeyword" :placeholder="t('apiTestManagement.searchTip')" allow-clear />
|
||||||
|
<a-dropdown @select="handleSelect">
|
||||||
|
<a-button type="primary">{{ t('apiTestManagement.newApi') }}</a-button>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||||
|
<a-doption value="import">{{ t('apiTestManagement.importApi') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="folder" @click="setActiveFolder('all')">
|
||||||
|
<div :class="allFolderClass">
|
||||||
|
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||||
|
<div class="folder-name">{{ t('apiTestManagement.allApi') }}</div>
|
||||||
|
<div class="folder-count">({{ allFileCount }})</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center">
|
||||||
|
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
|
||||||
|
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
|
||||||
|
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||||
|
</MsButton>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-dropdown @select="handleSelect">
|
||||||
|
<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>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="newApi">{{ t('apiTestManagement.newApi') }}</a-doption>
|
||||||
|
<a-doption value="addModule">{{ t('apiTestManagement.addSubModule') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
|
||||||
|
<span id="addModulePopSpan"></span>
|
||||||
|
</popConfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-divider class="my-[8px]" />
|
||||||
|
</template>
|
||||||
|
<a-input
|
||||||
|
v-if="props.isModal"
|
||||||
|
v-model:model-value="moduleKeyword"
|
||||||
|
:placeholder="t('apiTestManagement.moveSearchTip')"
|
||||||
|
allow-clear
|
||||||
|
class="mb-[16px]"
|
||||||
|
/>
|
||||||
|
<a-spin class="min-h-[400px] w-full" :loading="loading">
|
||||||
|
<MsTree
|
||||||
|
v-model:focus-node-key="focusNodeKey"
|
||||||
|
v-model:selected-keys="selectedKeys"
|
||||||
|
:data="folderTree"
|
||||||
|
:keyword="moduleKeyword"
|
||||||
|
:node-more-actions="folderMoreActions"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:expand-all="isExpandAll"
|
||||||
|
:empty-text="t('apiTestManagement.noMatchModule')"
|
||||||
|
:virtual-list-props="virtualListProps"
|
||||||
|
:field-names="{
|
||||||
|
title: 'name',
|
||||||
|
key: 'id',
|
||||||
|
children: 'children',
|
||||||
|
count: 'count',
|
||||||
|
}"
|
||||||
|
block-node
|
||||||
|
title-tooltip-position="left"
|
||||||
|
@select="folderNodeSelect"
|
||||||
|
@more-action-select="handleFolderMoreSelect"
|
||||||
|
@more-actions-close="moreActionsClose"
|
||||||
|
@drop="handleDrop"
|
||||||
|
>
|
||||||
|
<template #title="nodeData">
|
||||||
|
<div class="inline-flex w-full">
|
||||||
|
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||||
|
<div v-if="!props.isModal" class="ml-auto text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="!props.isModal" #extra="nodeData">
|
||||||
|
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||||
|
<popConfirm
|
||||||
|
v-if="nodeData.id !== 'root'"
|
||||||
|
mode="add"
|
||||||
|
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||||
|
:parent-id="nodeData.id"
|
||||||
|
@close="resetFocusNodeKey"
|
||||||
|
@add-finish="() => initModules()"
|
||||||
|
>
|
||||||
|
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusNodeKey(nodeData)">
|
||||||
|
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||||
|
</MsButton>
|
||||||
|
</popConfirm>
|
||||||
|
<popConfirm
|
||||||
|
v-if="nodeData.id !== 'root'"
|
||||||
|
mode="rename"
|
||||||
|
:parent-id="nodeData.id"
|
||||||
|
:node-id="nodeData.id"
|
||||||
|
:field-config="{ field: renameFolderTitle }"
|
||||||
|
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||||
|
@close="resetFocusNodeKey"
|
||||||
|
@rename-finish="initModules"
|
||||||
|
>
|
||||||
|
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
|
||||||
|
</popConfirm>
|
||||||
|
</template>
|
||||||
|
</MsTree>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||||
|
import { Message } 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 type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
||||||
|
|
||||||
|
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { mapTree } from '@/utils';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modulesCount?: Record<string, number>; // 模块数量统计对象
|
||||||
|
isExpandAll?: boolean; // 是否展开所有节点
|
||||||
|
activeModule?: string | number; // 选中的节点 key
|
||||||
|
isModal?: boolean; // 是否是弹窗模式
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
activeModule: 'all',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const emit = defineEmits(['init', 'change', 'newApi', 'import', 'folderNodeSelect']);
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const moduleProtocol = ref('http');
|
||||||
|
const moduleProtocolOptions = ref([
|
||||||
|
{
|
||||||
|
label: 'HTTP',
|
||||||
|
value: 'http',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleProtocolChange(value: string | number | Record<string, any>) {
|
||||||
|
console.log(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(value: string | number | Record<string, any> | undefined) {
|
||||||
|
switch (value) {
|
||||||
|
case 'newApi':
|
||||||
|
emit('newApi');
|
||||||
|
break;
|
||||||
|
case 'import':
|
||||||
|
emit('import');
|
||||||
|
break;
|
||||||
|
case 'addModule':
|
||||||
|
document.querySelector('#addModulePopSpan')?.dispatchEvent(new Event('click'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualListProps = computed(() => {
|
||||||
|
if (props.isModal) {
|
||||||
|
return {
|
||||||
|
height: 'calc(60vh - 190px)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
height: 'calc(100vh - 305px)',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const allFileCount = ref(0);
|
||||||
|
const isExpandAll = ref(props.isExpandAll);
|
||||||
|
const rootModulesName = ref<string[]>([]); // 根模块名称列表
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isExpandAll,
|
||||||
|
(val) => {
|
||||||
|
isExpandAll.value = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function changeExpand() {
|
||||||
|
isExpandAll.value = !isExpandAll.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleKeyword = ref('');
|
||||||
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
const focusNodeKey = ref<string | number>('');
|
||||||
|
const selectedKeys = ref<Array<string | number>>([props.activeModule]);
|
||||||
|
const allFolderClass = computed(() =>
|
||||||
|
selectedKeys.value[0] === 'all' ? 'folder-text folder-text--active' : 'folder-text'
|
||||||
|
);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedKeys.value,
|
||||||
|
(arr) => {
|
||||||
|
emit('change', arr ? arr[0] : '');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function setActiveFolder(id: string) {
|
||||||
|
selectedKeys.value = [id];
|
||||||
|
}
|
||||||
|
function setFocusNodeKey(node: MsTreeNodeData) {
|
||||||
|
focusNodeKey.value = node.id || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderMoreActions: ActionsItem[] = [
|
||||||
|
{
|
||||||
|
label: 'common.rename',
|
||||||
|
eventTag: 'rename',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'apiTestManagement.execute',
|
||||||
|
eventTag: 'execute',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'apiTestManagement.share',
|
||||||
|
eventTag: 'share',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common.delete',
|
||||||
|
eventTag: 'delete',
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const renamePopVisible = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模块树
|
||||||
|
* @param isSetDefaultKey 是否设置第一个节点为选中节点
|
||||||
|
*/
|
||||||
|
async function initModules(isSetDefaultKey = false) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getReviewModules(appStore.currentProjectId);
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
hideMoreAction: e.id === 'root',
|
||||||
|
draggable: e.id !== 'root' && !props.isModal,
|
||||||
|
disabled: e.id === selectedKeys.value[0] && props.isModal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (isSetDefaultKey) {
|
||||||
|
selectedKeys.value = [folderTree.value[0].id];
|
||||||
|
}
|
||||||
|
emit('init', folderTree.value);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件夹树节点选中事件
|
||||||
|
*/
|
||||||
|
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
|
||||||
|
const offspringIds: string[] = [];
|
||||||
|
mapTree(node.children || [], (e) => {
|
||||||
|
offspringIds.push(e.id);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件夹
|
||||||
|
* @param node 节点信息
|
||||||
|
*/
|
||||||
|
function deleteFolder(node: MsTreeNodeData) {
|
||||||
|
openModal({
|
||||||
|
type: 'error',
|
||||||
|
title: t('apiTestDebug.deleteFolderTipTitle', { name: node.name }),
|
||||||
|
content: t('apiTestDebug.deleteFolderTipContent'),
|
||||||
|
okText: t('apiTestDebug.deleteConfirm'),
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
maskClosable: false,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteReviewModule(node.id);
|
||||||
|
Message.success(t('apiTestDebug.deleteSuccess'));
|
||||||
|
initModules();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameFolderTitle = ref(''); // 重命名的文件夹名称
|
||||||
|
|
||||||
|
function resetFocusNodeKey() {
|
||||||
|
focusNodeKey.value = '';
|
||||||
|
renamePopVisible.value = false;
|
||||||
|
renameFolderTitle.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理树节点更多按钮事件
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
function handleFolderMoreSelect(item: ActionsItem, node: MsTreeNodeData) {
|
||||||
|
switch (item.eventTag) {
|
||||||
|
case 'delete':
|
||||||
|
deleteFolder(node);
|
||||||
|
resetFocusNodeKey();
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
renameFolderTitle.value = node.name || '';
|
||||||
|
renamePopVisible.value = true;
|
||||||
|
document.querySelector(`#renameSpan${node.id}`)?.dispatchEvent(new Event('click'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件夹树节点拖拽事件
|
||||||
|
* @param tree 树数据
|
||||||
|
* @param dragNode 拖拽节点
|
||||||
|
* @param dropNode 释放节点
|
||||||
|
* @param dropPosition 释放位置
|
||||||
|
*/
|
||||||
|
async function handleDrop(
|
||||||
|
tree: MsTreeNodeData[],
|
||||||
|
dragNode: MsTreeNodeData,
|
||||||
|
dropNode: MsTreeNodeData,
|
||||||
|
dropPosition: number
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await moveReviewModule({
|
||||||
|
dragNodeId: dragNode.id as string,
|
||||||
|
dropNodeId: dropNode.id || '',
|
||||||
|
dropPosition,
|
||||||
|
});
|
||||||
|
Message.success(t('apiTestDebug.moduleMoveSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
initModules();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moreActionsClose() {
|
||||||
|
if (!renamePopVisible.value) {
|
||||||
|
// 当下拉菜单关闭时,若不是触发重命名气泡显示,则清空聚焦节点 key
|
||||||
|
resetFocusNodeKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模块文件数量
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.modulesCount,
|
||||||
|
(obj) => {
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
count: obj?.[node.id] || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
initModules,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.folder {
|
||||||
|
@apply flex cursor-pointer items-center justify-between;
|
||||||
|
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(var(--primary-1));
|
||||||
|
}
|
||||||
|
.folder-text {
|
||||||
|
@apply flex flex-1 cursor-pointer items-center;
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.folder-name {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
.folder-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.folder-text--active {
|
||||||
|
.folder-icon,
|
||||||
|
.folder-name,
|
||||||
|
.folder-count {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<MsCard simple no-content-padding>
|
||||||
|
<MsSplitBox :size="0.25" :max="0.5">
|
||||||
|
<template #first>
|
||||||
|
<div class="p-[24px]">
|
||||||
|
<moduleTree
|
||||||
|
@init="(val) => (folderTree = val)"
|
||||||
|
@new-api="newApi"
|
||||||
|
@change="(val) => (activeModule = val)"
|
||||||
|
@import="importDrawerVisible = true"
|
||||||
|
@folder-node-select="(keys, _offspringIds) => (offspringIds = _offspringIds)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="b-0 absolute w-[88%]">
|
||||||
|
<a-divider class="!my-0 !mb-2" />
|
||||||
|
<div class="case h-[38px]">
|
||||||
|
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
|
||||||
|
<MsIcon type="icon-icon_delete-trash_outlined" class="folder-icon" />
|
||||||
|
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.recycle') }}</div>
|
||||||
|
<div class="folder-count">({{ recycleModulesCount.all || 0 }})</div></div
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</template>
|
||||||
|
<template #second>
|
||||||
|
<div class="relative flex h-full flex-col">
|
||||||
|
<div
|
||||||
|
id="managementContainer"
|
||||||
|
:class="['absolute z-[101] h-full w-full', importDrawerVisible ? '' : 'invisible']"
|
||||||
|
style="transition: all 0.3s"
|
||||||
|
>
|
||||||
|
<importApi
|
||||||
|
v-model:visible="importDrawerVisible"
|
||||||
|
:module-tree="folderTree"
|
||||||
|
popup-container="#managementContainer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<management
|
||||||
|
:module="activeModule"
|
||||||
|
:all-count="allCount"
|
||||||
|
:active-module="activeModule"
|
||||||
|
:offspring-ids="offspringIds"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsSplitBox>
|
||||||
|
</MsCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||||
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
|
import importApi from './components/import.vue';
|
||||||
|
import management from './components/management/index.vue';
|
||||||
|
import moduleTree from './components/moduleTree.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const activeModule = ref<string>('all');
|
||||||
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
const allCount = ref(0);
|
||||||
|
const importDrawerVisible = ref(false);
|
||||||
|
const offspringIds = ref<string[]>([]);
|
||||||
|
|
||||||
|
function newApi() {
|
||||||
|
// debugRef.value?.addDebugTab();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -0,0 +1 @@
|
||||||
|
export default {};
|
|
@ -0,0 +1,75 @@
|
||||||
|
export default {
|
||||||
|
'apiTestManagement.newApi': '新建接口',
|
||||||
|
'apiTestManagement.importApi': '导入接口',
|
||||||
|
'apiTestManagement.fileImport': '文件导入',
|
||||||
|
'apiTestManagement.timeImport': '定时导入',
|
||||||
|
'apiTestManagement.addSubModule': '添加子模块',
|
||||||
|
'apiTestManagement.allApi': '全部接口',
|
||||||
|
'apiTestManagement.searchTip': '请输入模块/接口名称',
|
||||||
|
'apiTestManagement.moveSearchTip': '请输入模块名称搜索',
|
||||||
|
'apiTestManagement.noMatchModule': '暂无匹配的模块/接口',
|
||||||
|
'apiTestManagement.execute': '执行',
|
||||||
|
'apiTestManagement.share': '分享 API',
|
||||||
|
'apiTestManagement.doc': '文档',
|
||||||
|
'apiTestManagement.closeAll': '关闭全部tab',
|
||||||
|
'apiTestManagement.closeOther': '关闭其他tab',
|
||||||
|
'apiTestManagement.showSubdirectory': '显示子目录用例',
|
||||||
|
'apiTestManagement.searchPlaceholder': '输入 ID/名称/api路径搜索',
|
||||||
|
'apiTestManagement.apiName': '接口名称',
|
||||||
|
'apiTestManagement.apiType': '请求类型',
|
||||||
|
'apiTestManagement.apiStatus': '状态',
|
||||||
|
'apiTestManagement.responsiblePerson': '责任人',
|
||||||
|
'apiTestManagement.path': '路径',
|
||||||
|
'apiTestManagement.version': '版本',
|
||||||
|
'apiTestManagement.createTime': '创建时间',
|
||||||
|
'apiTestManagement.updateTime': '更新时间',
|
||||||
|
'apiTestManagement.deprecate': '已废弃',
|
||||||
|
'apiTestManagement.processing': '进行中',
|
||||||
|
'apiTestManagement.debugging': '联调中',
|
||||||
|
'apiTestManagement.done': '已完成',
|
||||||
|
'apiTestManagement.deleteApiTipTitle': '确认删除 {name} 吗?',
|
||||||
|
'apiTestManagement.deleteApiTip': '删除后,接口将放入回收站,可在回收站内进行数据恢复',
|
||||||
|
'apiTestManagement.batchDeleteApiTip': '确认删除已选中的 {count} 个接口吗?',
|
||||||
|
'apiTestManagement.batchModalSubTitle': '(已选 {count} 个接口)',
|
||||||
|
'apiTestManagement.chooseAttr': '选择属性',
|
||||||
|
'apiTestManagement.attrRequired': '属性不能为空',
|
||||||
|
'apiTestManagement.batchUpdate': '批量更新为',
|
||||||
|
'apiTestManagement.valueRequired': '属性值不能为空',
|
||||||
|
'apiTestManagement.batchMoveConfirm': '移动至所选模块',
|
||||||
|
'apiTestManagement.belongModule': '所属模块',
|
||||||
|
'apiTestManagement.importMode': '导入模式',
|
||||||
|
'apiTestManagement.importModeTip1': '覆盖:',
|
||||||
|
'apiTestManagement.importModeTip2': '1.同一接口请求类型+路径一致,请求参数内容不一致则覆盖',
|
||||||
|
'apiTestManagement.importModeTip3': '2.同一接口请求类型+路径一致,请求参数内容一致不做变更',
|
||||||
|
'apiTestManagement.importModeTip4': '3.非同一接口,请求类型+路径一致,则新增',
|
||||||
|
'apiTestManagement.importModeTip5': '不覆盖:',
|
||||||
|
'apiTestManagement.importModeTip6': '1.同一接口请求类型+路径一致,则不做变更',
|
||||||
|
'apiTestManagement.importModeTip7': '2.非同一接口请求类型+路径一致,则新增',
|
||||||
|
'apiTestManagement.cover': '覆盖',
|
||||||
|
'apiTestManagement.uncover': '不覆盖',
|
||||||
|
'apiTestManagement.moreSetting': '更多设置',
|
||||||
|
'apiTestManagement.importType': '导入方式',
|
||||||
|
'apiTestManagement.urlImport': 'URL 导入',
|
||||||
|
'apiTestManagement.syncImportCase': '同步导入接口用例',
|
||||||
|
'apiTestManagement.syncUpdateDirectory': '同步更新接口所在目录',
|
||||||
|
'apiTestManagement.importSwaggerFileTip1': '支持 Swagger 3.0 版本的 json 文件,',
|
||||||
|
'apiTestManagement.importSwaggerFileTip2': '2.0 的文件建议自行在官网转换成 3.0 再进行导入',
|
||||||
|
'apiTestManagement.importSwaggerFileTip3': ',大小不超过 50M',
|
||||||
|
'apiTestManagement.urlImportPlaceholder': '请输入OpenAPI/Swagger URL',
|
||||||
|
'apiTestManagement.swaggerURLRequired': 'SwaggerURL 不能为空',
|
||||||
|
'apiTestManagement.basicAuth': 'Basic Auth 认证',
|
||||||
|
'apiTestManagement.account': '账号',
|
||||||
|
'apiTestManagement.accountRequired': '账号不能为空',
|
||||||
|
'apiTestManagement.password': '密码',
|
||||||
|
'apiTestManagement.passwordRequired': '密码不能为空',
|
||||||
|
'apiTestManagement.taskName': '任务名称',
|
||||||
|
'apiTestManagement.taskNamePlaceholder': '请输入任务名称',
|
||||||
|
'apiTestManagement.taskNameRequired': '任务名称不能为空',
|
||||||
|
'apiTestManagement.syncFrequency': '同步频率',
|
||||||
|
'apiTestManagement.timeTaskList': '定时任务列表',
|
||||||
|
'apiTestManagement.timeTaskHour': '(每小时)',
|
||||||
|
'apiTestManagement.timeTaskSixHour': '(每 6 小时)',
|
||||||
|
'apiTestManagement.timeTaskTwelveHour': '(每 12 小时)',
|
||||||
|
'apiTestManagement.timeTaskDay': '(每天)',
|
||||||
|
'apiTestManagement.customFrequency': '自定义频率',
|
||||||
|
};
|
|
@ -190,7 +190,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileType(type: string) {
|
function getFileType(type: string) {
|
||||||
const fileTypes = type ? getFileEnum(`/${type.toLowerCase()}`) : 'unknown';
|
const fileTypes = type ? getFileEnum(type.toLowerCase()) : 'unknown';
|
||||||
return FileIconMap[fileTypes][UploadStatus.done];
|
return FileIconMap[fileTypes][UploadStatus.done];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div>XXXXXX <span>(101)</span></div>
|
<div>XXXXXX <span>(101)</span></div>
|
||||||
<a-input-search
|
<a-input-search
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
:max-length="250"
|
:max-length="255"
|
||||||
:placeholder="t('project.member.searchMember')"
|
:placeholder="t('project.member.searchMember')"
|
||||||
allow-clear
|
allow-clear
|
||||||
@search="searchHandler"
|
@search="searchHandler"
|
||||||
|
|
|
@ -225,49 +225,7 @@
|
||||||
/>
|
/>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical">
|
<reviewForm ref="dialogFormRef" />
|
||||||
<a-form-item field="reason" :label="t('caseManagement.caseReview.reviewResult')" class="mb-[8px]">
|
|
||||||
<a-radio-group v-model:model-value="caseResultForm.result" @change="() => dialogFormRef?.resetFields()">
|
|
||||||
<a-radio value="PASS">
|
|
||||||
<div class="inline-flex items-center">
|
|
||||||
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
|
|
||||||
{{ t('caseManagement.caseReview.pass') }}
|
|
||||||
</div>
|
|
||||||
</a-radio>
|
|
||||||
<a-radio value="UN_PASS">
|
|
||||||
<div class="inline-flex items-center">
|
|
||||||
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
|
|
||||||
{{ t('caseManagement.caseReview.fail') }}
|
|
||||||
</div>
|
|
||||||
</a-radio>
|
|
||||||
<a-radio value="UNDER_REVIEWED">
|
|
||||||
<div class="inline-flex items-center">
|
|
||||||
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
|
||||||
{{ t('caseManagement.caseReview.suggestion') }}
|
|
||||||
</div>
|
|
||||||
<a-tooltip :content="t('caseManagement.caseReview.suggestionTip')" position="right">
|
|
||||||
<icon-question-circle
|
|
||||||
class="ml-[4px] mt-[2px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</a-tooltip>
|
|
||||||
</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item
|
|
||||||
field="reason"
|
|
||||||
:label="t('caseManagement.caseReview.reason')"
|
|
||||||
:rules="
|
|
||||||
caseResultForm.result === 'UN_PASS' || caseResultForm.result === 'UNDER_REVIEWED'
|
|
||||||
? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }]
|
|
||||||
: []
|
|
||||||
"
|
|
||||||
asterisk-position="end"
|
|
||||||
class="mb-0"
|
|
||||||
>
|
|
||||||
<MsRichText v-model:raw="caseResultForm.reason" class="w-full" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
<a-button type="primary" class="mt-[16px]" :loading="submitReviewLoading" @click="submitReview">
|
<a-button type="primary" class="mt-[16px]" :loading="submitReviewLoading" @click="submitReview">
|
||||||
{{ t('caseManagement.caseReview.submitReview') }}
|
{{ t('caseManagement.caseReview.submitReview') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
@ -292,7 +250,7 @@
|
||||||
* @description 功能测试-用例评审-用例详情
|
* @description 功能测试-用例评审-用例详情
|
||||||
*/
|
*/
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import MSAvatar from '@/components/pure/ms-avatar/index.vue';
|
import MSAvatar from '@/components/pure/ms-avatar/index.vue';
|
||||||
|
@ -302,13 +260,13 @@
|
||||||
import MsEmpty from '@/components/pure/ms-empty/index.vue';
|
import MsEmpty from '@/components/pure/ms-empty/index.vue';
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
import MsPagination from '@/components/pure/ms-pagination/index';
|
import MsPagination from '@/components/pure/ms-pagination/index';
|
||||||
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
|
||||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||||
import caseTemplateDetail from '../caseManagementFeature/components/caseTemplateDetail.vue';
|
import caseTemplateDetail from '../caseManagementFeature/components/caseTemplateDetail.vue';
|
||||||
import caseTabDemand from '../caseManagementFeature/components/tabContent/tabDemand/associatedDemandTable.vue';
|
import caseTabDemand from '../caseManagementFeature/components/tabContent/tabDemand/associatedDemandTable.vue';
|
||||||
import caseTabDetail from '../caseManagementFeature/components/tabContent/tabDetail.vue';
|
import caseTabDetail from '../caseManagementFeature/components/tabContent/tabDetail.vue';
|
||||||
|
import reviewForm from './components/reviewForm.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCaseReviewHistoryList,
|
getCaseReviewHistoryList,
|
||||||
|
@ -325,6 +283,8 @@
|
||||||
import type { DetailCase } from '@/models/caseManagement/featureCase';
|
import type { DetailCase } from '@/models/caseManagement/featureCase';
|
||||||
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
|
import { Instance } from 'tippy.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -529,11 +489,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const autoNext = ref(false);
|
const autoNext = ref(false);
|
||||||
const caseResultForm = ref({
|
const dialogFormRef = ref<InstanceType<typeof reviewForm>>();
|
||||||
result: 'PASS' as ReviewResult,
|
|
||||||
reason: '',
|
|
||||||
});
|
|
||||||
const dialogFormRef = ref<FormInstance>();
|
|
||||||
const demandKeyword = ref('');
|
const demandKeyword = ref('');
|
||||||
const caseDemandRef = ref<InstanceType<typeof caseTabDemand>>();
|
const caseDemandRef = ref<InstanceType<typeof caseTabDemand>>();
|
||||||
|
|
||||||
|
@ -553,47 +509,46 @@
|
||||||
const submitReviewLoading = ref(false);
|
const submitReviewLoading = ref(false);
|
||||||
// 提交评审
|
// 提交评审
|
||||||
function submitReview() {
|
function submitReview() {
|
||||||
dialogFormRef.value?.validate(async (errors) => {
|
dialogFormRef.value?.validateForm(async (caseResultForm: Record<string, any>) => {
|
||||||
if (!errors) {
|
try {
|
||||||
try {
|
submitReviewLoading.value = true;
|
||||||
submitReviewLoading.value = true;
|
const params = {
|
||||||
const params = {
|
projectId: appStore.currentProjectId,
|
||||||
projectId: appStore.currentProjectId,
|
caseId: activeCaseId.value,
|
||||||
caseId: activeCaseId.value,
|
reviewId: route.query.id as string,
|
||||||
reviewId: route.query.id as string,
|
status: caseResultForm.value.result,
|
||||||
status: caseResultForm.value.result,
|
reviewPassRule: reviewDetail.value.reviewPassRule,
|
||||||
reviewPassRule: reviewDetail.value.reviewPassRule,
|
content: caseResultForm.value.reason,
|
||||||
content: caseResultForm.value.reason,
|
notifier: '', // TODO: 通知人
|
||||||
notifier: '', // TODO: 通知人
|
};
|
||||||
};
|
await saveCaseReviewResult(params);
|
||||||
await saveCaseReviewResult(params);
|
Message.success(t('caseManagement.caseReview.reviewSuccess'));
|
||||||
Message.success(t('caseManagement.caseReview.reviewSuccess'));
|
caseResultForm.value = {
|
||||||
caseResultForm.value = {
|
result: 'PASS' as ReviewResult,
|
||||||
result: 'PASS' as ReviewResult,
|
reason: '',
|
||||||
reason: '',
|
fileList: [] as MsFileItem[],
|
||||||
};
|
};
|
||||||
if (autoNext.value) {
|
if (autoNext.value) {
|
||||||
// 自动下一个,更改激活的 id会刷新详情
|
// 自动下一个,更改激活的 id会刷新详情
|
||||||
const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
|
const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
|
||||||
if (index < caseList.value.length - 1) {
|
if (index < caseList.value.length - 1) {
|
||||||
activeCaseId.value = caseList.value[index + 1].caseId;
|
activeCaseId.value = caseList.value[index + 1].caseId;
|
||||||
} else {
|
|
||||||
// 当前是最后一个,刷新数据
|
|
||||||
loadCaseDetail();
|
|
||||||
initReviewHistoryList();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 不自动下一个才请求详情
|
// 当前是最后一个,刷新数据
|
||||||
loadCaseDetail();
|
loadCaseDetail();
|
||||||
initReviewHistoryList();
|
initReviewHistoryList();
|
||||||
}
|
}
|
||||||
loadCaseList();
|
} else {
|
||||||
} catch (error) {
|
// 不自动下一个才请求详情
|
||||||
// eslint-disable-next-line no-console
|
loadCaseDetail();
|
||||||
console.log(error);
|
initReviewHistoryList();
|
||||||
} finally {
|
|
||||||
submitReviewLoading.value = false;
|
|
||||||
}
|
}
|
||||||
|
loadCaseList();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
submitReviewLoading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
:confirm-loading="confirmLoading"
|
:confirm-loading="confirmLoading"
|
||||||
:associated-ids="[]"
|
:associated-ids="[]"
|
||||||
:type="RequestModuleEnum.CASE_MANAGEMENT"
|
:type="RequestModuleEnum.CASE_MANAGEMENT"
|
||||||
|
:table-params="{ reviewId: props.reviewId }"
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
@save="saveHandler"
|
@save="saveHandler"
|
||||||
>
|
>
|
||||||
|
@ -90,7 +91,7 @@
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
project: string;
|
project: string;
|
||||||
// associatedIds: string[];
|
reviewId?: string;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:visible', val: boolean): void;
|
(e: 'update:visible', val: boolean): void;
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
@selected-change="handleTableSelect"
|
@selected-change="handleTableSelect"
|
||||||
@batch-action="handleTableBatch"
|
@batch-action="handleTableBatch"
|
||||||
>
|
>
|
||||||
<template #resultColumn>
|
<template #resultTitle>
|
||||||
<div class="flex items-center text-[var(--color-text-3)]">
|
<div class="flex items-center text-[var(--color-text-3)]">
|
||||||
{{ t('caseManagement.caseReview.reviewResult') }}
|
{{ t('caseManagement.caseReview.reviewResult') }}
|
||||||
<a-tooltip :content="t('caseManagement.caseReview.reviewResultTip')" position="right">
|
<a-tooltip :content="t('caseManagement.caseReview.reviewResultTip')" position="right">
|
||||||
|
@ -96,7 +96,6 @@
|
||||||
</ms-base-table>
|
</ms-base-table>
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="dialogVisible"
|
v-model:visible="dialogVisible"
|
||||||
:on-before-ok="handleDeleteConfirm"
|
|
||||||
class="p-[4px]"
|
class="p-[4px]"
|
||||||
title-align="start"
|
title-align="start"
|
||||||
body-class="p-0"
|
body-class="p-0"
|
||||||
|
@ -158,7 +157,35 @@
|
||||||
asterisk-position="end"
|
asterisk-position="end"
|
||||||
class="mb-0"
|
class="mb-0"
|
||||||
>
|
>
|
||||||
<MsRichText v-model:raw="dialogForm.reason" class="w-full" />
|
<div class="flex w-full items-center">
|
||||||
|
<a-mention
|
||||||
|
v-model:model-value="dialogForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:auto-size="{ minRows: 1 }"
|
||||||
|
:max-length="1000"
|
||||||
|
allow-clear
|
||||||
|
class="flex flex-1 items-center"
|
||||||
|
/>
|
||||||
|
<MsUpload
|
||||||
|
v-model:file-list="dialogForm.fileList"
|
||||||
|
accept="image"
|
||||||
|
size-unit="MB"
|
||||||
|
:auto-upload="false"
|
||||||
|
multiple
|
||||||
|
:limit="10"
|
||||||
|
:disabled="dialogForm.fileList.length >= 10"
|
||||||
|
>
|
||||||
|
<a-button type="outline" class="ml-[8px] p-[8px_6px]" :disabled="dialogForm.fileList.length >= 10">
|
||||||
|
<icon-file-image :size="18" />
|
||||||
|
</a-button>
|
||||||
|
</MsUpload>
|
||||||
|
</div>
|
||||||
|
<MsFileList
|
||||||
|
v-model:file-list="dialogForm.fileList"
|
||||||
|
show-mode="imageList"
|
||||||
|
:show-tab="false"
|
||||||
|
class="mt-[8px]"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item
|
<a-form-item
|
||||||
v-if="dialogShowType === 'changeReviewer'"
|
v-if="dialogShowType === 'changeReviewer'"
|
||||||
|
@ -241,10 +268,12 @@
|
||||||
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 MsPopconfirm from '@/components/pure/ms-popconfirm/index.vue';
|
import MsPopconfirm from '@/components/pure/ms-popconfirm/index.vue';
|
||||||
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
||||||
|
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||||
|
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||||
import MsSelect from '@/components/business/ms-select';
|
import MsSelect from '@/components/business/ms-select';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -255,7 +284,8 @@
|
||||||
getReviewDetailCasePage,
|
getReviewDetailCasePage,
|
||||||
getReviewUsers,
|
getReviewUsers,
|
||||||
} from '@/api/modules/case-management/caseReview';
|
} from '@/api/modules/case-management/caseReview';
|
||||||
import { reviewResultMap, reviewStatusMap } from '@/config/caseManagement';
|
import { getProjectMemberCommentOptions } from '@/api/modules/project-management/projectMember';
|
||||||
|
import { reviewResultMap } from '@/config/caseManagement';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useModal from '@/hooks/useModal';
|
import useModal from '@/hooks/useModal';
|
||||||
import useTableStore from '@/hooks/useTableStore';
|
import useTableStore from '@/hooks/useTableStore';
|
||||||
|
@ -296,6 +326,10 @@
|
||||||
dataIndex: 'num',
|
dataIndex: 'num',
|
||||||
slotName: 'num',
|
slotName: 'num',
|
||||||
sortIndex: 1,
|
sortIndex: 1,
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
|
@ -313,27 +347,23 @@
|
||||||
title: 'caseManagement.caseReview.reviewer',
|
title: 'caseManagement.caseReview.reviewer',
|
||||||
dataIndex: 'reviewNames',
|
dataIndex: 'reviewNames',
|
||||||
slotName: 'reviewNames',
|
slotName: 'reviewNames',
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.reviewResult',
|
title: 'caseManagement.caseReview.reviewResult',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
slotName: 'status',
|
slotName: 'status',
|
||||||
titleSlotName: 'resultColumn',
|
titleSlotName: 'resultTitle',
|
||||||
width: 110,
|
width: 110,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'caseManagement.caseReview.version',
|
// title: 'caseManagement.caseReview.version',
|
||||||
dataIndex: 'versionName',
|
// dataIndex: 'versionName',
|
||||||
width: 90,
|
// width: 90,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.creator',
|
title: 'caseManagement.caseReview.creator',
|
||||||
dataIndex: 'creator',
|
dataIndex: 'createUserName',
|
||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -345,7 +375,6 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const tableStore = useTableStore();
|
const tableStore = useTableStore();
|
||||||
tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, columns, 'drawer');
|
|
||||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, getTableQueryParams } = useTable(
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, getTableQueryParams } = useTable(
|
||||||
getReviewDetailCasePage,
|
getReviewDetailCasePage,
|
||||||
{
|
{
|
||||||
|
@ -442,6 +471,7 @@
|
||||||
reason: '',
|
reason: '',
|
||||||
reviewer: [] as string[],
|
reviewer: [] as string[],
|
||||||
isAppend: false,
|
isAppend: false,
|
||||||
|
fileList: [] as MsFileItem[],
|
||||||
};
|
};
|
||||||
const dialogForm = ref({ ...defaultDialogForm });
|
const dialogForm = ref({ ...defaultDialogForm });
|
||||||
const dialogFormRef = ref<FormInstance>();
|
const dialogFormRef = ref<FormInstance>();
|
||||||
|
@ -488,27 +518,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除拦截
|
|
||||||
* @param done 关闭弹窗
|
|
||||||
*/
|
|
||||||
async function handleDeleteConfirm(done: (closed: boolean) => void) {
|
|
||||||
try {
|
|
||||||
// if (replaceVersion.value !== '') {
|
|
||||||
// await useLatestVersion(replaceVersion.value);
|
|
||||||
// }
|
|
||||||
// await toggleVersionStatus(activeRecord.value.id);
|
|
||||||
// Message.success(t('caseManagement.caseReview.close', { name: activeRecord.value.name }));
|
|
||||||
loadList();
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error);
|
|
||||||
done(false);
|
|
||||||
} finally {
|
|
||||||
done(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleArchive(record: ReviewItem) {
|
function handleArchive(record: ReviewItem) {
|
||||||
openModal({
|
openModal({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -730,72 +739,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await initReviewers();
|
const [, memberRes] = await Promise.all([
|
||||||
|
initReviewers(),
|
||||||
|
getProjectMemberCommentOptions(appStore.currentProjectId, keyword.value),
|
||||||
|
]);
|
||||||
|
const memberOptions = memberRes.map((e) => ({ label: e.name, value: e.id }));
|
||||||
filterConfigList.value = [
|
filterConfigList.value = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: 'ID',
|
dataIndex: 'id',
|
||||||
type: FilterType.INPUT,
|
type: FilterType.INPUT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.name',
|
title: 'caseManagement.caseReview.caseName',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
type: FilterType.INPUT,
|
type: FilterType.INPUT,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.caseCount',
|
|
||||||
dataIndex: 'caseCount',
|
|
||||||
type: FilterType.NUMBER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.status',
|
|
||||||
dataIndex: 'status',
|
|
||||||
type: FilterType.SELECT,
|
|
||||||
selectProps: {
|
|
||||||
mode: 'static',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: t(reviewStatusMap.PREPARED.label),
|
|
||||||
value: 'PREPARED',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t(reviewStatusMap.UNDERWAY.label),
|
|
||||||
value: 'UNDERWAY',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t(reviewStatusMap.COMPLETED.label),
|
|
||||||
value: 'COMPLETED',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t(reviewStatusMap.ARCHIVED.label),
|
|
||||||
value: 'ARCHIVED',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.passRate',
|
|
||||||
dataIndex: 'passRate',
|
|
||||||
type: FilterType.NUMBER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.type',
|
|
||||||
dataIndex: 'reviewPassRule',
|
|
||||||
type: FilterType.SELECT,
|
|
||||||
selectProps: {
|
|
||||||
mode: 'static',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: t('caseManagement.caseReview.single'),
|
|
||||||
value: 'SINGLE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('caseManagement.caseReview.multi'),
|
|
||||||
value: 'MULTIPLE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.reviewer',
|
title: 'caseManagement.caseReview.reviewer',
|
||||||
dataIndex: 'reviewers',
|
dataIndex: 'reviewers',
|
||||||
|
@ -805,54 +764,35 @@
|
||||||
options: reviewersOptions.value,
|
options: reviewersOptions.value,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'caseManagement.caseReview.reviewResult',
|
||||||
|
dataIndex: 'status',
|
||||||
|
type: FilterType.SELECT,
|
||||||
|
selectProps: {
|
||||||
|
mode: 'static',
|
||||||
|
options: Object.keys(reviewResultMap).map((e) => ({
|
||||||
|
label: t(reviewResultMap[e as ReviewResult].label),
|
||||||
|
value: e,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.creator',
|
title: 'caseManagement.caseReview.creator',
|
||||||
dataIndex: 'createUser',
|
dataIndex: 'createUser',
|
||||||
type: FilterType.SELECT,
|
type: FilterType.SELECT,
|
||||||
selectProps: {
|
selectProps: {
|
||||||
mode: 'static',
|
mode: 'static',
|
||||||
options: reviewersOptions.value,
|
options: memberOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.module',
|
|
||||||
dataIndex: 'module',
|
|
||||||
type: FilterType.TREE_SELECT,
|
|
||||||
treeSelectData: props.moduleTree,
|
|
||||||
treeSelectProps: {
|
|
||||||
fieldNames: {
|
|
||||||
title: 'name',
|
|
||||||
key: 'id',
|
|
||||||
children: 'children',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.tag',
|
|
||||||
dataIndex: 'tags',
|
|
||||||
type: FilterType.TAGS_INPUT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.desc',
|
|
||||||
dataIndex: 'description',
|
|
||||||
type: FilterType.INPUT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.startTime',
|
|
||||||
dataIndex: 'startTime',
|
|
||||||
type: FilterType.DATE_PICKER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'caseManagement.caseReview.endTime',
|
|
||||||
dataIndex: 'endTime',
|
|
||||||
type: FilterType.DATE_PICKER,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
searchCase,
|
searchCase,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE, columns, 'drawer');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@ -871,4 +811,3 @@
|
||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/config/caseManagement
|
|
||||||
|
|
|
@ -34,7 +34,9 @@
|
||||||
@popup-visible-change="handleFilterHidden"
|
@popup-visible-change="handleFilterHidden"
|
||||||
>
|
>
|
||||||
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="statusFilterVisible = true">
|
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="statusFilterVisible = true">
|
||||||
{{ t(columnConfig.title as string) }}
|
<div class="font-medium">
|
||||||
|
{{ t(columnConfig.title as string) }}
|
||||||
|
</div>
|
||||||
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
|
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
@ -101,10 +103,10 @@
|
||||||
<MsButton type="text" class="!mr-0" @click="() => editReview(record)">
|
<MsButton type="text" class="!mr-0" @click="() => editReview(record)">
|
||||||
{{ t('common.edit') }}
|
{{ t('common.edit') }}
|
||||||
</MsButton>
|
</MsButton>
|
||||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
<!-- <a-divider direction="vertical" :margin="8"></a-divider>
|
||||||
<MsButton type="text" class="!mr-0">
|
<MsButton type="text" class="!mr-0">
|
||||||
{{ t('common.export') }}
|
{{ t('common.export') }}
|
||||||
</MsButton>
|
</MsButton> -->
|
||||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||||
<MsTableMoreAction :list="getMoreAction(record.status)" @select="handleMoreActionSelect($event, record)" />
|
<MsTableMoreAction :list="getMoreAction(record.status)" @select="handleMoreActionSelect($event, record)" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -169,6 +171,7 @@
|
||||||
import ModuleTree from './moduleTree.vue';
|
import ModuleTree from './moduleTree.vue';
|
||||||
|
|
||||||
import { getReviewList, getReviewUsers } from '@/api/modules/case-management/caseReview';
|
import { getReviewList, getReviewUsers } from '@/api/modules/case-management/caseReview';
|
||||||
|
import { getProjectMemberCommentOptions } from '@/api/modules/project-management/projectMember';
|
||||||
import { reviewStatusMap } from '@/config/caseManagement';
|
import { reviewStatusMap } from '@/config/caseManagement';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useModal from '@/hooks/useModal';
|
import useModal from '@/hooks/useModal';
|
||||||
|
@ -211,8 +214,12 @@
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getReviewUsers(appStore.currentProjectId, keyword.value);
|
const [userRes, memberRes] = await Promise.all([
|
||||||
const userOptions = res.map((e) => ({ label: e.name, value: e.id }));
|
getReviewUsers(appStore.currentProjectId, keyword.value),
|
||||||
|
getProjectMemberCommentOptions(appStore.currentProjectId, keyword.value),
|
||||||
|
]);
|
||||||
|
const userOptions = userRes.map((e) => ({ label: e.name, value: e.id }));
|
||||||
|
const memberOptions = memberRes.map((e) => ({ label: e.name, value: e.id }));
|
||||||
filterConfigList.value = [
|
filterConfigList.value = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
|
@ -293,7 +300,7 @@
|
||||||
type: FilterType.SELECT,
|
type: FilterType.SELECT,
|
||||||
selectProps: {
|
selectProps: {
|
||||||
mode: 'static',
|
mode: 'static',
|
||||||
options: userOptions,
|
options: memberOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -341,6 +348,10 @@
|
||||||
dataIndex: 'num',
|
dataIndex: 'num',
|
||||||
slotName: 'num',
|
slotName: 'num',
|
||||||
sortIndex: 1,
|
sortIndex: 1,
|
||||||
|
sortable: {
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
|
@ -382,16 +393,13 @@
|
||||||
title: 'caseManagement.caseReview.reviewer',
|
title: 'caseManagement.caseReview.reviewer',
|
||||||
slotName: 'reviewers',
|
slotName: 'reviewers',
|
||||||
dataIndex: 'reviewers',
|
dataIndex: 'reviewers',
|
||||||
sortable: {
|
|
||||||
sortDirections: ['ascend', 'descend'],
|
|
||||||
sorter: true,
|
|
||||||
},
|
|
||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.creator',
|
title: 'caseManagement.caseReview.creator',
|
||||||
dataIndex: 'createUser',
|
dataIndex: 'createUserName',
|
||||||
width: 90,
|
showTooltip: true,
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.module',
|
title: 'caseManagement.caseReview.module',
|
||||||
|
@ -402,7 +410,7 @@
|
||||||
title: 'caseManagement.caseReview.tag',
|
title: 'caseManagement.caseReview.tag',
|
||||||
dataIndex: 'tags',
|
dataIndex: 'tags',
|
||||||
isTag: true,
|
isTag: true,
|
||||||
width: 300,
|
width: 170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'caseManagement.caseReview.desc',
|
title: 'caseManagement.caseReview.desc',
|
||||||
|
@ -420,11 +428,11 @@
|
||||||
slotName: 'action',
|
slotName: 'action',
|
||||||
dataIndex: 'operation',
|
dataIndex: 'operation',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
width: 150,
|
width: 110,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const tableStore = useTableStore();
|
const tableStore = useTableStore();
|
||||||
tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW, columns, 'drawer');
|
await tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_REVIEW, columns, 'drawer');
|
||||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||||
getReviewList,
|
getReviewList,
|
||||||
{
|
{
|
||||||
|
@ -438,9 +446,12 @@
|
||||||
...item,
|
...item,
|
||||||
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
|
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
|
||||||
reviewers: item.reviewers.map((e: ReviewDetailReviewersItem) => e.userName),
|
reviewers: item.reviewers.map((e: ReviewDetailReviewersItem) => e.userName),
|
||||||
cycle: `${dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss')} - ${dayjs(item.endTime).format(
|
cycle:
|
||||||
'YYYY-MM-DD HH:mm:ss'
|
item.startTime && item.endTime
|
||||||
)}`,
|
? `${dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss')} - ${dayjs(item.endTime).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss'
|
||||||
|
)}`
|
||||||
|
: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<a-form ref="dialogFormRef" :model="caseResultForm" layout="vertical">
|
||||||
|
<a-form-item field="reason" :label="t('caseManagement.caseReview.reviewResult')" class="mb-[8px]">
|
||||||
|
<a-radio-group v-model:model-value="caseResultForm.result" @change="() => dialogFormRef?.resetFields()">
|
||||||
|
<a-radio value="PASS">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
|
||||||
|
{{ t('caseManagement.caseReview.pass') }}
|
||||||
|
</div>
|
||||||
|
</a-radio>
|
||||||
|
<a-radio value="UN_PASS">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
|
||||||
|
{{ t('caseManagement.caseReview.fail') }}
|
||||||
|
</div>
|
||||||
|
</a-radio>
|
||||||
|
<a-radio value="UNDER_REVIEWED">
|
||||||
|
<div class="inline-flex items-center">
|
||||||
|
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
|
||||||
|
{{ t('caseManagement.caseReview.suggestion') }}
|
||||||
|
</div>
|
||||||
|
<a-tooltip :content="t('caseManagement.caseReview.suggestionTip')" position="right">
|
||||||
|
<icon-question-circle
|
||||||
|
class="ml-[4px] mt-[2px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="reason"
|
||||||
|
:label="t('caseManagement.caseReview.reason')"
|
||||||
|
:rules="
|
||||||
|
caseResultForm.result !== 'PASS'
|
||||||
|
? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }]
|
||||||
|
: []
|
||||||
|
"
|
||||||
|
asterisk-position="end"
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center">
|
||||||
|
<a-mention
|
||||||
|
v-model:model-value="caseResultForm.reason"
|
||||||
|
type="textarea"
|
||||||
|
:auto-size="{ minRows: 1 }"
|
||||||
|
:max-length="1000"
|
||||||
|
allow-clear
|
||||||
|
class="flex flex-1 items-center"
|
||||||
|
/>
|
||||||
|
<MsUpload
|
||||||
|
v-model:file-list="caseResultForm.fileList"
|
||||||
|
accept="image"
|
||||||
|
size-unit="MB"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="10"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<a-button type="outline" class="ml-[8px] p-[8px_6px]">
|
||||||
|
<icon-file-image :size="18" />
|
||||||
|
</a-button>
|
||||||
|
</MsUpload>
|
||||||
|
</div>
|
||||||
|
<MsFileList
|
||||||
|
v-model:file-list="caseResultForm.fileList"
|
||||||
|
show-mode="imageList"
|
||||||
|
:show-tab="false"
|
||||||
|
class="mt-[8px]"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FormInstance } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
||||||
|
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||||
|
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { ReviewResult } from '@/models/caseManagement/caseReview';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const dialogFormRef = ref<FormInstance>();
|
||||||
|
const caseResultForm = ref({
|
||||||
|
result: 'PASS' as ReviewResult,
|
||||||
|
reason: '',
|
||||||
|
fileList: [] as MsFileItem[],
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateForm(cb: (form: Record<string, any>) => void) {
|
||||||
|
dialogFormRef.value?.validate((errors) => {
|
||||||
|
if (!errors && typeof cb === 'function') {
|
||||||
|
cb(caseResultForm.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
validateForm,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.image-preview-container {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -117,6 +117,7 @@
|
||||||
<AssociateDrawer
|
<AssociateDrawer
|
||||||
v-model:visible="associateDrawerVisible"
|
v-model:visible="associateDrawerVisible"
|
||||||
v-model:project="associateDrawerProject"
|
v-model:project="associateDrawerProject"
|
||||||
|
:review-id="reviewId"
|
||||||
@success="writeAssociateCases"
|
@success="writeAssociateCases"
|
||||||
/>
|
/>
|
||||||
<deleteReviewModal v-model:visible="deleteModalVisible" :record="reviewDetail" @success="handleDeleteSuccess" />
|
<deleteReviewModal v-model:visible="deleteModalVisible" :record="reviewDetail" @success="handleDeleteSuccess" />
|
||||||
|
@ -172,11 +173,12 @@
|
||||||
const reviewDetail = ref<ReviewItem>({
|
const reviewDetail = ref<ReviewItem>({
|
||||||
...reviewDefaultDetail,
|
...reviewDefaultDetail,
|
||||||
});
|
});
|
||||||
|
const reviewId = ref(route.query.id as string);
|
||||||
|
|
||||||
async function initDetail() {
|
async function initDetail() {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await getReviewDetail(route.query.id as string);
|
const res = await getReviewDetail(reviewId.value);
|
||||||
reviewDetail.value = res;
|
reviewDetail.value = res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -203,7 +205,7 @@
|
||||||
modulesCount.value = await getReviewDetailModuleCount({
|
modulesCount.value = await getReviewDetailModuleCount({
|
||||||
...params,
|
...params,
|
||||||
viewFlag: onlyMine.value,
|
viewFlag: onlyMine.value,
|
||||||
reviewId: route.query.id as string,
|
reviewId: reviewId.value,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -237,7 +239,7 @@
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await associateReviewCase({
|
await associateReviewCase({
|
||||||
reviewId: route.query.id as string,
|
reviewId: reviewId.value,
|
||||||
projectId: appStore.currentProjectId,
|
projectId: appStore.currentProjectId,
|
||||||
reviewers: params.reviewers,
|
reviewers: params.reviewers,
|
||||||
baseAssociateCaseRequest: params,
|
baseAssociateCaseRequest: params,
|
||||||
|
@ -258,7 +260,7 @@
|
||||||
router.push({
|
router.push({
|
||||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
|
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
|
||||||
query: {
|
query: {
|
||||||
id: route.query.id,
|
id: reviewId.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -267,7 +269,7 @@
|
||||||
router.push({
|
router.push({
|
||||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
|
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
|
||||||
query: {
|
query: {
|
||||||
reviewId: route.query.id,
|
reviewId: reviewId.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -337,7 +339,7 @@
|
||||||
followLoading.value = true;
|
followLoading.value = true;
|
||||||
await followReview({
|
await followReview({
|
||||||
userId: userStore.id || '',
|
userId: userStore.id || '',
|
||||||
caseReviewId: route.query.id as string,
|
caseReviewId: reviewId.value,
|
||||||
});
|
});
|
||||||
Message.success(
|
Message.success(
|
||||||
reviewDetail.value.followFlag
|
reviewDetail.value.followFlag
|
||||||
|
@ -357,7 +359,7 @@
|
||||||
router.push({
|
router.push({
|
||||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
|
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
|
||||||
query: {
|
query: {
|
||||||
copyId: route.query.id,
|
copyId: reviewId.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -381,4 +383,3 @@
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/config/caseManagement
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default {
|
||||||
'caseManagement.caseReview.batchMoveConfirm': 'Move {count} use cases to the selected module',
|
'caseManagement.caseReview.batchMoveConfirm': 'Move {count} use cases to the selected module',
|
||||||
'caseManagement.caseReview.batchMoveTitleSub': '({count} reviews selected)',
|
'caseManagement.caseReview.batchMoveTitleSub': '({count} reviews selected)',
|
||||||
'caseManagement.caseReview.batchMoveSuccess': 'Review moved successfully',
|
'caseManagement.caseReview.batchMoveSuccess': 'Review moved successfully',
|
||||||
'caseManagement.caseReview.folderSearchPlaceholder': 'Please enter a group name',
|
'caseManagement.caseReview.folderSearchPlaceholder': 'Please enter a module name',
|
||||||
'caseManagement.caseReview.allReviews': 'All reviews',
|
'caseManagement.caseReview.allReviews': 'All reviews',
|
||||||
'caseManagement.caseReview.noReviews': 'No matching review data yet',
|
'caseManagement.caseReview.noReviews': 'No matching review data yet',
|
||||||
'caseManagement.caseReview.deleteFolderTipTitle': 'Remove the `{name}` module?',
|
'caseManagement.caseReview.deleteFolderTipTitle': 'Remove the `{name}` module?',
|
||||||
|
|
|
@ -43,10 +43,10 @@ export default {
|
||||||
'caseManagement.caseReview.archiveSuccess': '归档成功',
|
'caseManagement.caseReview.archiveSuccess': '归档成功',
|
||||||
'caseManagement.caseReview.move': '移动到',
|
'caseManagement.caseReview.move': '移动到',
|
||||||
'caseManagement.caseReview.batchMove': '批量移动',
|
'caseManagement.caseReview.batchMove': '批量移动',
|
||||||
'caseManagement.caseReview.batchMoveConfirm': '移动 {count} 个用例至已选模块 ',
|
'caseManagement.caseReview.batchMoveConfirm': '移动 {count} 个评审至已选模块 ',
|
||||||
'caseManagement.caseReview.batchMoveTitleSub': '(已选 {count} 条评审) ',
|
'caseManagement.caseReview.batchMoveTitleSub': '(已选 {count} 条评审) ',
|
||||||
'caseManagement.caseReview.batchMoveSuccess': '评审移动成功',
|
'caseManagement.caseReview.batchMoveSuccess': '评审移动成功',
|
||||||
'caseManagement.caseReview.folderSearchPlaceholder': '请输入分组名称',
|
'caseManagement.caseReview.folderSearchPlaceholder': '请输入模块名称',
|
||||||
'caseManagement.caseReview.allReviews': '全部评审',
|
'caseManagement.caseReview.allReviews': '全部评审',
|
||||||
'caseManagement.caseReview.noReviews': '暂无匹配的评审数据',
|
'caseManagement.caseReview.noReviews': '暂无匹配的评审数据',
|
||||||
'caseManagement.caseReview.deleteFolderTipTitle': '是否删除 `{name}` 模块?',
|
'caseManagement.caseReview.deleteFolderTipTitle': '是否删除 `{name}` 模块?',
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const innerSlogan = computed(() => {
|
const innerSlogan = computed(() => {
|
||||||
return props.isPreview ? props.slogan : appStore.pageConfig.slogan;
|
return props.isPreview ? props.slogan : t(appStore.pageConfig.slogan);
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
|
|
|
@ -20,10 +20,10 @@
|
||||||
:before-change="handleEnableIntercept"
|
:before-change="handleEnableIntercept"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="small"
|
size="small"
|
||||||
class="mr-[4px]"
|
class="mr-[8px]"
|
||||||
type="line"
|
type="line"
|
||||||
/>
|
/>
|
||||||
<a-tooltip :content="t('project.fileManagement.uploadTipSingle')">
|
<a-tooltip v-if="fileType === 'jar'" :content="t('project.fileManagement.uploadTipSingle')">
|
||||||
<MsIcon type="icon-icon-maybe_outlined" class="mr-[8px] cursor-pointer hover:text-[rgb(var(--primary-5))]" />
|
<MsIcon type="icon-icon-maybe_outlined" class="mr-[8px] cursor-pointer hover:text-[rgb(var(--primary-5))]" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<MsButton
|
<MsButton
|
||||||
|
@ -176,6 +176,16 @@
|
||||||
:action-config="caseBatchActions"
|
:action-config="caseBatchActions"
|
||||||
v-on="caseTableEvent"
|
v-on="caseTableEvent"
|
||||||
>
|
>
|
||||||
|
<template #id="{ record }">
|
||||||
|
<a-tooltip :content="`${record.id}`">
|
||||||
|
<a-button type="text" class="px-0" @click="goCaseDetail(record.id)">
|
||||||
|
<div class="one-line-text max-w-[168px]">{{ record.id }}</div>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #sourceType="{ record }">
|
||||||
|
{{ t(associateFileSourceLocaleMap[record.sourceType]) }}
|
||||||
|
</template>
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<MsButton
|
<MsButton
|
||||||
v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']"
|
v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']"
|
||||||
|
@ -215,7 +225,6 @@
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import type { MsPaginationI, MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { MsPaginationI, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
import { getFileEnum } from '@/components/pure/ms-upload/iconMap';
|
|
||||||
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
|
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
|
||||||
import MsThumbnailCard from '@/components/business/ms-thumbnail-card/index.vue';
|
import MsThumbnailCard from '@/components/business/ms-thumbnail-card/index.vue';
|
||||||
import popConfirm from './popConfirm.vue';
|
import popConfirm from './popConfirm.vue';
|
||||||
|
@ -232,13 +241,16 @@
|
||||||
upgradeAssociation,
|
upgradeAssociation,
|
||||||
} from '@/api/modules/project-management/fileManagement';
|
} from '@/api/modules/project-management/fileManagement';
|
||||||
import { CompressImgUrl, OriginImgUrl } from '@/api/requrls/project-management/fileManagement';
|
import { CompressImgUrl, OriginImgUrl } from '@/api/requrls/project-management/fileManagement';
|
||||||
|
import { associateFileSourceLocaleMap } from '@/config/fileManagement';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useLocale from '@/locale/useLocale';
|
import useLocale from '@/locale/useLocale';
|
||||||
|
import router from '@/router';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import useUserStore from '@/store/modules/user';
|
import useUserStore from '@/store/modules/user';
|
||||||
import { downloadByteFile, formatFileSize } from '@/utils';
|
import { downloadByteFile, formatFileSize } from '@/utils';
|
||||||
|
|
||||||
import { AssociationItem, FileDetail } from '@/models/projectManagement/file';
|
import { AssociationItem, FileDetail } from '@/models/projectManagement/file';
|
||||||
|
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -352,10 +364,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileTagClose(tag: string | number, item: Description) {
|
async function handleFileTagClose(tag: string | number, item: Description) {
|
||||||
await updateFile({
|
try {
|
||||||
id: props.fileId,
|
const lastTags = Array.isArray(item.value) ? item.value.filter((e) => e !== tag) : [];
|
||||||
tags: Array.isArray(item.value) ? item.value.filter((e) => e !== tag) : [],
|
await updateFile({
|
||||||
});
|
id: props.fileId,
|
||||||
|
tags: lastTags,
|
||||||
|
});
|
||||||
|
item.value = [...lastTags];
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -379,23 +398,30 @@
|
||||||
|
|
||||||
const caseColumns: MsTableColumn = [
|
const caseColumns: MsTableColumn = [
|
||||||
{
|
{
|
||||||
title: 'project.fileManagement.id',
|
title: 'project.fileManagement.caseId',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
|
slotName: 'id',
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'project.fileManagement.name',
|
title: 'project.fileManagement.caseName',
|
||||||
dataIndex: 'sourceName',
|
dataIndex: 'sourceName',
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
width: 200,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'project.fileManagement.type',
|
title: 'project.fileManagement.caseType',
|
||||||
dataIndex: 'sourceType',
|
dataIndex: 'sourceType',
|
||||||
|
slotName: 'sourceType',
|
||||||
|
width: 100,
|
||||||
|
showTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'project.fileManagement.fileVersion',
|
title: 'project.fileManagement.caseFileVersion',
|
||||||
dataIndex: 'fileVersion',
|
dataIndex: 'fileVersion',
|
||||||
|
width: 140,
|
||||||
|
showTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'common.operation',
|
title: 'common.operation',
|
||||||
|
@ -412,10 +438,10 @@
|
||||||
} = useTable(getAssociationList, {
|
} = useTable(getAssociationList, {
|
||||||
scroll: { x: 800 },
|
scroll: { x: 800 },
|
||||||
columns: caseColumns,
|
columns: caseColumns,
|
||||||
|
heightUsed: 200,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
showSelectAll: true,
|
showSelectAll: true,
|
||||||
showPagination: false,
|
showPagination: false,
|
||||||
size: 'default',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const caseBatchActions = {
|
const caseBatchActions = {
|
||||||
|
@ -445,6 +471,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goCaseDetail(id: string) {
|
||||||
|
router.push({
|
||||||
|
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const versionColumns: MsTableColumn = [
|
const versionColumns: MsTableColumn = [
|
||||||
{
|
{
|
||||||
title: 'project.fileManagement.fileVersion',
|
title: 'project.fileManagement.fileVersion',
|
||||||
|
@ -504,9 +539,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadedFile(detail: FileDetail) {
|
function loadedFile(detail: FileDetail) {
|
||||||
if (detail.fileType) {
|
fileType.value = detail.fileType;
|
||||||
fileType.value = getFileEnum(`/${detail.fileType.toLowerCase()}`);
|
|
||||||
}
|
|
||||||
renameTitle.value = detail.name;
|
renameTitle.value = detail.name;
|
||||||
fileDescriptions.value = [
|
fileDescriptions.value = [
|
||||||
{
|
{
|
||||||
|
@ -543,6 +576,7 @@
|
||||||
showTagAdd: true,
|
showTagAdd: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
key: 'tag',
|
key: 'tag',
|
||||||
|
tagType: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('project.fileManagement.createTime'),
|
label: t('project.fileManagement.createTime'),
|
||||||
|
|
|
@ -196,8 +196,8 @@
|
||||||
|
|
||||||
function reset(val: boolean) {
|
function reset(val: boolean) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
form.value.field = '';
|
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
|
form.value.field = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-[calc(100vh-88px)] flex-col overflow-hidden p-[24px]">
|
<div class="flex h-[calc(100vh-88px)] flex-col p-[24px]">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a-button v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']" type="primary" @click="handleAddClick">{{
|
<a-button v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']" type="primary" @click="handleAddClick">
|
||||||
t('project.fileManagement.addFile')
|
{{ t('project.fileManagement.addFile') }}
|
||||||
}}</a-button>
|
</a-button>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<a-select v-model="tableFileType" class="w-[240px]" :loading="fileTypeLoading" @change="searchList">
|
<a-select v-model="tableFileType" class="w-[240px]" :loading="fileTypeLoading" @change="searchList">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
class="w-[240px]"
|
class="w-[240px]"
|
||||||
@search="searchList"
|
@search="searchList"
|
||||||
@press-enter="searchList"
|
@press-enter="searchList"
|
||||||
|
@clear="searchList"
|
||||||
/>
|
/>
|
||||||
<a-radio-group
|
<a-radio-group
|
||||||
v-if="props.activeFolderType === 'folder'"
|
v-if="props.activeFolderType === 'folder'"
|
||||||
|
@ -113,7 +114,7 @@
|
||||||
:type="item.fileType"
|
:type="item.fileType"
|
||||||
:url="`${CompressImgUrl}/${userStore.id}/${item.id}`"
|
:url="`${CompressImgUrl}/${userStore.id}/${item.id}`"
|
||||||
:footer-text="item.name"
|
:footer-text="item.name"
|
||||||
:more-actions="item.fileType === 'JAR' ? getJarFileActions(item) : normalFileActions"
|
:more-actions="item.fileType === 'jar' ? getJarFileActions(item) : normalFileActions"
|
||||||
@click="openFileDetail(item.id, index)"
|
@click="openFileDetail(item.id, index)"
|
||||||
@action-select="handleMoreActionSelect($event, item)"
|
@action-select="handleMoreActionSelect($event, item)"
|
||||||
/>
|
/>
|
||||||
|
@ -241,6 +242,7 @@
|
||||||
:rules="[{ required: true, message: t('project.fileManagement.gitFilePathNotNull') }]"
|
:rules="[{ required: true, message: t('project.fileManagement.gitFilePathNotNull') }]"
|
||||||
required
|
required
|
||||||
asterisk-position="end"
|
asterisk-position="end"
|
||||||
|
class="mb-0"
|
||||||
>
|
>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:model-value="storageForm.path"
|
v-model:model-value="storageForm.path"
|
||||||
|
@ -356,6 +358,7 @@
|
||||||
import useAsyncTaskStore from '@/store/modules/app/asyncTask';
|
import useAsyncTaskStore from '@/store/modules/app/asyncTask';
|
||||||
import useUserStore from '@/store/modules/user';
|
import useUserStore from '@/store/modules/user';
|
||||||
import { characterLimit, downloadByteFile, formatFileSize } from '@/utils';
|
import { characterLimit, downloadByteFile, formatFileSize } from '@/utils';
|
||||||
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
import type { FileItem, FileListQueryParams, Repository } from '@/models/projectManagement/file';
|
import type { FileItem, FileListQueryParams, Repository } from '@/models/projectManagement/file';
|
||||||
import { RouteEnum } from '@/enums/routeEnum';
|
import { RouteEnum } from '@/enums/routeEnum';
|
||||||
|
@ -444,7 +447,7 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
const normalActions = [
|
||||||
{
|
{
|
||||||
label: 'project.fileManagement.move',
|
label: 'project.fileManagement.move',
|
||||||
eventTag: 'move',
|
eventTag: 'move',
|
||||||
|
@ -458,10 +461,20 @@
|
||||||
danger: true,
|
danger: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
if (showType.value === 'card') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'project.fileManagement.download',
|
||||||
|
eventTag: 'download',
|
||||||
|
},
|
||||||
|
...normalActions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return normalActions;
|
||||||
});
|
});
|
||||||
|
|
||||||
function getJarFileActions(record: FileItem) {
|
function getJarFileActions(record: FileItem) {
|
||||||
const jarFileActions: ActionsItem[] = [
|
let jarFileActions: ActionsItem[] = [
|
||||||
{
|
{
|
||||||
label: 'project.fileManagement.move',
|
label: 'project.fileManagement.move',
|
||||||
eventTag: 'move',
|
eventTag: 'move',
|
||||||
|
@ -483,10 +496,24 @@
|
||||||
danger: true,
|
danger: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (record.enable) {
|
if (showType.value === 'card') {
|
||||||
return jarFileActions.filter((e) => e.label !== 'common.enable');
|
jarFileActions = [
|
||||||
|
{
|
||||||
|
label: 'project.fileManagement.download',
|
||||||
|
eventTag: 'download',
|
||||||
|
},
|
||||||
|
...jarFileActions,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return jarFileActions.filter((e) => e.label !== 'common.disable');
|
if (record.storage === 'GIT') {
|
||||||
|
jarFileActions = jarFileActions.filter((e) => e.eventTag !== 'move');
|
||||||
|
}
|
||||||
|
if (record.enable) {
|
||||||
|
jarFileActions = jarFileActions.filter((e) => e.label !== 'common.enable');
|
||||||
|
} else if (record.enable === false) {
|
||||||
|
jarFileActions = jarFileActions.filter((e) => e.label !== 'common.disable');
|
||||||
|
}
|
||||||
|
return jarFileActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: MsTableColumn = [
|
const columns: MsTableColumn = [
|
||||||
|
@ -494,6 +521,7 @@
|
||||||
title: 'project.fileManagement.name',
|
title: 'project.fileManagement.name',
|
||||||
slotName: 'name',
|
slotName: 'name',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
fixed: 'left',
|
||||||
width: 270,
|
width: 270,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -515,7 +543,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'project.fileManagement.creator',
|
title: 'project.fileManagement.creator',
|
||||||
dataIndex: 'creator',
|
dataIndex: 'createUser',
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
|
@ -544,12 +572,13 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const tableStore = useTableStore();
|
const tableStore = useTableStore();
|
||||||
|
await tableStore.initColumn(TableKeyEnum.FILE_MANAGEMENT_FILE, columns, 'drawer');
|
||||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetPagination } = useTable(
|
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetPagination } = useTable(
|
||||||
getFileList,
|
getFileList,
|
||||||
{
|
{
|
||||||
tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
|
tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
|
||||||
showSetting: true,
|
showSetting: true,
|
||||||
selectable: true,
|
selectable: !!hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+DOWNLOAD+UPDATE+DELETE']),
|
||||||
showSelectAll: true,
|
showSelectAll: true,
|
||||||
},
|
},
|
||||||
(item) => {
|
(item) => {
|
||||||
|
@ -809,6 +838,7 @@
|
||||||
*/
|
*/
|
||||||
async function changeFileType() {
|
async function changeFileType() {
|
||||||
await initFileTypes();
|
await initFileTypes();
|
||||||
|
resetSelector();
|
||||||
setTableParams();
|
setTableParams();
|
||||||
if (showType.value === 'card') {
|
if (showType.value === 'card') {
|
||||||
cardListRef.value?.reload();
|
cardListRef.value?.reload();
|
||||||
|
@ -938,6 +968,9 @@
|
||||||
case 'toggle':
|
case 'toggle':
|
||||||
toggleJarFile(record);
|
toggleJarFile(record);
|
||||||
break;
|
break;
|
||||||
|
case 'download':
|
||||||
|
handleDownload(record);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default {
|
||||||
'project.fileManagement.addStorage': 'Add repository',
|
'project.fileManagement.addStorage': 'Add repository',
|
||||||
'project.fileManagement.rename': 'Rename',
|
'project.fileManagement.rename': 'Rename',
|
||||||
'project.fileManagement.nameNotNull': 'name cannot be empty',
|
'project.fileManagement.nameNotNull': 'name cannot be empty',
|
||||||
'project.fileManagement.namePlaceholder': 'Please enter the group name and press Enter to save',
|
'project.fileManagement.namePlaceholder': 'Please enter the module name and press Enter to save',
|
||||||
'project.fileManagement.renameSuccess': 'Rename successful',
|
'project.fileManagement.renameSuccess': 'Rename successful',
|
||||||
'project.fileManagement.updateDescSuccess': 'File description updated successfully',
|
'project.fileManagement.updateDescSuccess': 'File description updated successfully',
|
||||||
'project.fileManagement.addSubModuleSuccess': 'Added successfully',
|
'project.fileManagement.addSubModuleSuccess': 'Added successfully',
|
||||||
|
@ -132,4 +132,15 @@ export default {
|
||||||
'project.fileManagement.batchMoveConfirm': 'Move to selected module',
|
'project.fileManagement.batchMoveConfirm': 'Move to selected module',
|
||||||
'project.fileManagement.batchMoveSuccess': 'File moved successfully',
|
'project.fileManagement.batchMoveSuccess': 'File moved successfully',
|
||||||
'project.fileManagement.moduleMoveSuccess': 'Module moved successfully',
|
'project.fileManagement.moduleMoveSuccess': 'Module moved successfully',
|
||||||
|
'project.fileManagement.caseId': 'Case ID',
|
||||||
|
'project.fileManagement.caseName': 'Case name',
|
||||||
|
'project.fileManagement.caseType': 'Case type',
|
||||||
|
'project.fileManagement.caseFileVersion': 'Associated file version',
|
||||||
|
'project.fileManagement.caseTypeBug': 'Bug',
|
||||||
|
'project.fileManagement.caseTypeFeature': 'Feature',
|
||||||
|
'project.fileManagement.caseTypeApiDebug': 'Api debug',
|
||||||
|
'project.fileManagement.caseTypeApiScene': 'Api scene',
|
||||||
|
'project.fileManagement.caseTypeApiCase': 'Api case',
|
||||||
|
'project.fileManagement.caseTypeApiDefine': 'Api definition',
|
||||||
|
'project.fileManagement.caseTypeApiMock': 'Api Mock',
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default {
|
||||||
'project.fileManagement.addStorage': '添加存储库',
|
'project.fileManagement.addStorage': '添加存储库',
|
||||||
'project.fileManagement.rename': '重命名',
|
'project.fileManagement.rename': '重命名',
|
||||||
'project.fileManagement.nameNotNull': '名字不能为空',
|
'project.fileManagement.nameNotNull': '名字不能为空',
|
||||||
'project.fileManagement.namePlaceholder': '请输入分组名称,按回车键保存',
|
'project.fileManagement.namePlaceholder': '请输入模块名称,按回车键保存',
|
||||||
'project.fileManagement.renameSuccess': '重命名成功',
|
'project.fileManagement.renameSuccess': '重命名成功',
|
||||||
'project.fileManagement.updateDescSuccess': '文件描述更新成功',
|
'project.fileManagement.updateDescSuccess': '文件描述更新成功',
|
||||||
'project.fileManagement.addSubModuleSuccess': '添加成功',
|
'project.fileManagement.addSubModuleSuccess': '添加成功',
|
||||||
|
@ -124,4 +124,15 @@ export default {
|
||||||
'project.fileManagement.batchMoveConfirm': '移动至所选模块',
|
'project.fileManagement.batchMoveConfirm': '移动至所选模块',
|
||||||
'project.fileManagement.batchMoveSuccess': '文件移动成功',
|
'project.fileManagement.batchMoveSuccess': '文件移动成功',
|
||||||
'project.fileManagement.moduleMoveSuccess': '模块移动成功',
|
'project.fileManagement.moduleMoveSuccess': '模块移动成功',
|
||||||
|
'project.fileManagement.caseId': '用例 ID',
|
||||||
|
'project.fileManagement.caseName': '用例名称',
|
||||||
|
'project.fileManagement.caseType': '用例类型',
|
||||||
|
'project.fileManagement.caseFileVersion': '关联的文件版本',
|
||||||
|
'project.fileManagement.caseTypeBug': '缺陷',
|
||||||
|
'project.fileManagement.caseTypeFeature': '功能用例',
|
||||||
|
'project.fileManagement.caseTypeApiDebug': '接口调试',
|
||||||
|
'project.fileManagement.caseTypeApiScene': '接口场景',
|
||||||
|
'project.fileManagement.caseTypeApiCase': '接口用例',
|
||||||
|
'project.fileManagement.caseTypeApiDefine': '接口定义',
|
||||||
|
'project.fileManagement.caseTypeApiMock': '接口 Mock',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,121 +1,113 @@
|
||||||
<template>
|
<template>
|
||||||
<MsCard ref="fullRef" :special-height="132" :is-fullscreen="isFullScreen" simple>
|
<MsCard
|
||||||
<div id="mscard">
|
ref="fullRef"
|
||||||
<div class="mb-[16px] flex items-center justify-between">
|
:special-height="132"
|
||||||
<div class="font-medium text-[var(--color-text-000)]">{{ t('project.messageManagement.config') }}</div>
|
show-full-screen
|
||||||
<div>
|
hide-back
|
||||||
<MsSelect
|
hide-footer
|
||||||
v-model:model-value="robotFilters"
|
@toggle-full-screen="handleToggleFullScreen"
|
||||||
:options="robotOptions"
|
>
|
||||||
:allow-search="false"
|
<template #headerLeft>
|
||||||
allow-clear
|
<div class="font-medium text-[var(--color-text-000)]">{{ t('project.messageManagement.config') }}</div>
|
||||||
class="mr-[8px] w-[240px]"
|
</template>
|
||||||
:prefix="t('project.messageManagement.robot')"
|
<template #headerRight>
|
||||||
:multiple="true"
|
<MsSelect
|
||||||
:has-all-select="true"
|
v-model:model-value="robotFilters"
|
||||||
:default-all-select="true"
|
:options="robotOptions"
|
||||||
:popup-container="isFullScreen ? '#mscard' : undefined"
|
:allow-search="false"
|
||||||
>
|
allow-clear
|
||||||
<template #footer>
|
class="mr-[8px] !w-[240px]"
|
||||||
<div class="mb-[6px] mt-[4px] p-[3px_8px]">
|
:prefix="t('project.messageManagement.robot')"
|
||||||
<MsButton v-permission="['PROJECT_MESSAGE:READ+ADD']" type="text" @click="emit('createRobot')">
|
:multiple="true"
|
||||||
<MsIcon type="icon-icon_add_outlined" class="mr-[8px] text-[rgb(var(--primary-6))]" size="14" />
|
:has-all-select="true"
|
||||||
{{ t('project.messageManagement.createBot') }}
|
:default-all-select="true"
|
||||||
</MsButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</MsSelect>
|
|
||||||
<a-button type="outline" class="arco-btn-outline--secondary px-[5px]" @click="toggleFullScreen">
|
|
||||||
<template #icon>
|
|
||||||
<MsIcon
|
|
||||||
:type="isFullScreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'"
|
|
||||||
class="text-[var(--color-text-4)]"
|
|
||||||
size="14"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
{{ t(isFullScreen ? 'common.offFullScreen' : 'common.fullScreen') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ms-base-table
|
|
||||||
ref="tableRef"
|
|
||||||
v-bind="propsRes"
|
|
||||||
v-model:expandedKeys="expandedKeys"
|
|
||||||
no-disable
|
|
||||||
:indent-size="0"
|
|
||||||
v-on="propsEvent"
|
|
||||||
>
|
>
|
||||||
<template #name="{ record }">
|
<template #footer>
|
||||||
<span class="font-medium text-[var(--color-text-1)]">{{ record.name }}</span>
|
<div class="mb-[6px] mt-[4px] p-[3px_8px]">
|
||||||
|
<MsButton v-permission="['PROJECT_MESSAGE:READ+ADD']" type="text" @click="emit('createRobot')">
|
||||||
|
<MsIcon type="icon-icon_add_outlined" class="mr-[8px] text-[rgb(var(--primary-6))]" size="14" />
|
||||||
|
{{ t('project.messageManagement.createBot') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #eventName="{ record }">
|
</MsSelect>
|
||||||
<span>{{ record.eventName || '' }}</span>
|
</template>
|
||||||
</template>
|
<ms-base-table
|
||||||
<template #receiver="{ record, dataIndex }">
|
ref="tableRef"
|
||||||
<MsSelect
|
v-bind="propsRes"
|
||||||
v-if="!record.children"
|
v-model:expandedKeys="expandedKeys"
|
||||||
:id="`${record.taskType}-${record.event}`"
|
no-disable
|
||||||
v-model:model-value="record.receivers"
|
:indent-size="0"
|
||||||
v-model:loading="record.loading"
|
v-on="propsEvent"
|
||||||
mode="remote"
|
>
|
||||||
:options="defaultReceivers"
|
<template #name="{ record }">
|
||||||
:search-keys="['label']"
|
<span class="font-medium text-[var(--color-text-1)]">{{ record.name }}</span>
|
||||||
allow-search
|
</template>
|
||||||
:at-least-one="true"
|
<template #eventName="{ record }">
|
||||||
value-key="id"
|
<span>{{ record.eventName || '' }}</span>
|
||||||
label-key="name"
|
</template>
|
||||||
:multiple="true"
|
<template #receiver="{ record, dataIndex }">
|
||||||
:placeholder="t('project.messageManagement.receiverPlaceholder')"
|
<MsSelect
|
||||||
:remote-extra-params="{ projectId: appStore.currentProjectId }"
|
v-if="!record.children"
|
||||||
:remote-func="getMessageUserList"
|
v-model:model-value="record.receivers"
|
||||||
:remote-fields-map="{ label: 'name', value: 'id', id: 'id' }"
|
v-model:loading="record.loading"
|
||||||
:not-auto-init-search="true"
|
class="w-full"
|
||||||
:popup-container="isFullScreen ? '#mscard' : undefined"
|
mode="remote"
|
||||||
:fallback-option="(val) => ({
|
:options="defaultReceivers"
|
||||||
|
:remote-filter-func="(opts) => getReceiverOptions(opts, record.event)"
|
||||||
|
:search-keys="['label']"
|
||||||
|
allow-search
|
||||||
|
:at-least-one="true"
|
||||||
|
value-key="id"
|
||||||
|
label-key="name"
|
||||||
|
:multiple="true"
|
||||||
|
:placeholder="t('project.messageManagement.receiverPlaceholder')"
|
||||||
|
:remote-extra-params="{ projectId: appStore.currentProjectId }"
|
||||||
|
:remote-func="getMessageUserList"
|
||||||
|
:remote-fields-map="{ label: 'name', value: 'id', id: 'id' }"
|
||||||
|
:not-auto-init-search="true"
|
||||||
|
:fallback-option="(val) => ({
|
||||||
label: (val as Record<string, any>).name,
|
label: (val as Record<string, any>).name,
|
||||||
value: val,
|
value: val,
|
||||||
})"
|
})"
|
||||||
:object-value="true"
|
:object-value="true"
|
||||||
@remove="changeMessageReceivers(false, record, dataIndex as string)"
|
@remove="changeMessageReceivers(false, record, dataIndex as string)"
|
||||||
@popup-visible-change="changeMessageReceivers($event, record, dataIndex as string)"
|
@popup-visible-change="changeMessageReceivers($event, record, dataIndex as string)"
|
||||||
|
/>
|
||||||
|
<span v-else></span>
|
||||||
|
</template>
|
||||||
|
<template #robot="{ record, dataIndex }">
|
||||||
|
<div v-if="!record.children && record.projectRobotConfigMap?.[dataIndex as string]" class="flex items-center">
|
||||||
|
<a-switch
|
||||||
|
v-model:model-value="record.projectRobotConfigMap[dataIndex as string].enable"
|
||||||
|
v-permission="['PROJECT_MESSAGE:READ+UPDATE']"
|
||||||
|
:before-change="(val) => handleChangeIntercept(!!val, record, dataIndex as string)"
|
||||||
|
size="small"
|
||||||
|
type="line"
|
||||||
/>
|
/>
|
||||||
<span v-else></span>
|
<a-popover position="right">
|
||||||
</template>
|
<div class="ml-[8px] mr-[4px] cursor-pointer text-[var(--color-text-1)] hover:text-[rgb(var(--primary-6))]">
|
||||||
<template #robot="{ record, dataIndex }">
|
{{ t('common.preview') }}
|
||||||
<div v-if="!record.children && record.projectRobotConfigMap?.[dataIndex as string]" class="flex items-center">
|
</div>
|
||||||
<a-switch
|
<template #content>
|
||||||
v-model:model-value="record.projectRobotConfigMap[dataIndex as string].enable"
|
<MessagePreview
|
||||||
v-permission="['PROJECT_MESSAGE:READ+UPDATE']"
|
:robot="record.projectRobotConfigMap[dataIndex as string]"
|
||||||
:before-change="(val) => handleChangeIntercept(!!val, record, dataIndex as string)"
|
:function-name="record.functionName"
|
||||||
size="small"
|
:event-name="record.eventName"
|
||||||
type="line"
|
/>
|
||||||
/>
|
</template>
|
||||||
<a-popover position="right" :popup-container="isFullScreen ? '#mscard' : undefined">
|
</a-popover>
|
||||||
<div
|
<MsButton
|
||||||
class="ml-[8px] mr-[4px] cursor-pointer text-[var(--color-text-1)] hover:text-[rgb(var(--primary-6))]"
|
v-permission="['PROJECT_MESSAGE:READ+UPDATE']"
|
||||||
>
|
v-xpack
|
||||||
{{ t('common.preview') }}
|
type="button"
|
||||||
</div>
|
@click="editRobot(record, dataIndex as string)"
|
||||||
<template #content>
|
>{{ t('common.setting') }}</MsButton
|
||||||
<MessagePreview
|
>
|
||||||
:robot="record.projectRobotConfigMap[dataIndex as string]"
|
</div>
|
||||||
:function-name="record.functionName"
|
<span v-else></span>
|
||||||
:event-name="record.eventName"
|
</template>
|
||||||
/>
|
</ms-base-table>
|
||||||
</template>
|
|
||||||
</a-popover>
|
|
||||||
<MsButton
|
|
||||||
v-permission="['PROJECT_MESSAGE:READ+UPDATE']"
|
|
||||||
v-xpack
|
|
||||||
type="button"
|
|
||||||
@click="editRobot(record, dataIndex as string)"
|
|
||||||
>{{ t('common.setting') }}</MsButton
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<span v-else></span>
|
|
||||||
</template>
|
|
||||||
</ms-base-table>
|
|
||||||
</div>
|
|
||||||
</MsCard>
|
</MsCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -139,7 +131,6 @@
|
||||||
getRobotList,
|
getRobotList,
|
||||||
saveMessageConfig,
|
saveMessageConfig,
|
||||||
} from '@/api/modules/project-management/messageManagement';
|
} from '@/api/modules/project-management/messageManagement';
|
||||||
import useFullScreen from '@/hooks/useFullScreen';
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
|
@ -157,9 +148,6 @@
|
||||||
|
|
||||||
const robotFilters = ref<string[]>([]);
|
const robotFilters = ref<string[]>([]);
|
||||||
const robotOptions = ref<(SelectOptionData & RobotItem)[]>([]);
|
const robotOptions = ref<(SelectOptionData & RobotItem)[]>([]);
|
||||||
const fullRef = ref<HTMLElement | null>();
|
|
||||||
|
|
||||||
const { isFullScreen, toggleFullScreen } = useFullScreen(fullRef);
|
|
||||||
|
|
||||||
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
|
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
|
||||||
const staticColumns: MsTableColumn = [
|
const staticColumns: MsTableColumn = [
|
||||||
|
@ -219,6 +207,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const expandedKeys = ref<string[]>([]);
|
const expandedKeys = ref<string[]>([]);
|
||||||
|
const heightUsed = ref(428);
|
||||||
|
|
||||||
interface TableMessageChildrenItem {
|
interface TableMessageChildrenItem {
|
||||||
functionName: string;
|
functionName: string;
|
||||||
|
@ -243,7 +232,7 @@
|
||||||
showPagination: false,
|
showPagination: false,
|
||||||
hoverable: false,
|
hoverable: false,
|
||||||
showExpand: true,
|
showExpand: true,
|
||||||
heightUsed: 50,
|
heightUsed: heightUsed.value,
|
||||||
rowKey: 'key',
|
rowKey: 'key',
|
||||||
rowClass: (record: TableMessageItem) => {
|
rowClass: (record: TableMessageItem) => {
|
||||||
if (record.children) {
|
if (record.children) {
|
||||||
|
@ -302,6 +291,10 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handleToggleFullScreen(val: boolean) {
|
||||||
|
propsRes.value.heightUsed = val ? 224 : 428;
|
||||||
|
}
|
||||||
|
|
||||||
function spanMethod(data: {
|
function spanMethod(data: {
|
||||||
record: TableData;
|
record: TableData;
|
||||||
column: TableColumnData | TableOperationColumn;
|
column: TableColumnData | TableOperationColumn;
|
||||||
|
@ -326,6 +319,14 @@
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReceiverOptions(options, event: string) {
|
||||||
|
if (event === 'CREATE') {
|
||||||
|
// 创建事件的接收人不包含操作人、创建人、关注人
|
||||||
|
return options.filter((e) => !['OPERATOR', 'CREATE_USER', 'FOLLOW_PEOPLE'].includes(e.id));
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
async function initRobotList() {
|
async function initRobotList() {
|
||||||
const res = await getRobotList(appStore.currentProjectId);
|
const res = await getRobotList(appStore.currentProjectId);
|
||||||
robotOptions.value = res
|
robotOptions.value = res
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
:ok-loading="drawerLoading"
|
:ok-loading="drawerLoading"
|
||||||
:show-continue="!isEdit"
|
:show-continue="!isEdit"
|
||||||
:ok-text="isEdit ? t('common.update') : t('common.create')"
|
:ok-text="isEdit ? t('common.update') : t('common.create')"
|
||||||
|
:save-continue-text="t('project.messageManagement.saveContinueText')"
|
||||||
@confirm="handleDrawerConfirm"
|
@confirm="handleDrawerConfirm"
|
||||||
@continue="handleDrawerConfirm(true)"
|
@continue="handleDrawerConfirm(true)"
|
||||||
@cancel="handleDrawerCancel"
|
@cancel="handleDrawerCancel"
|
||||||
|
@ -407,8 +408,16 @@
|
||||||
title: t(robot.enable ? 'project.messageManagement.disableTitle' : 'project.messageManagement.enableTitle', {
|
title: t(robot.enable ? 'project.messageManagement.disableTitle' : 'project.messageManagement.enableTitle', {
|
||||||
name: characterLimit(robot.name),
|
name: characterLimit(robot.name),
|
||||||
}),
|
}),
|
||||||
content: t(robot.enable ? 'project.messageManagement.disableContent' : 'project.messageManagement.enableContent'),
|
content: () =>
|
||||||
okText: t(robot.enable ? 'project.messageManagement.disableConfirm' : 'project.messageManagement.enableConfirm'),
|
h('div', {
|
||||||
|
innerHTML: `<div>${t(
|
||||||
|
robot.enable ? 'project.messageManagement.disableContent' : 'project.messageManagement.enableContent',
|
||||||
|
{ robot: robot.name }
|
||||||
|
)}</div><div>${robot.platform === 'MAIL' ? t('project.messageManagement.enableEmailContentTip') : ''}</div>`,
|
||||||
|
}),
|
||||||
|
okText: t(robot.enable ? 'project.messageManagement.disableConfirm' : 'project.messageManagement.enableConfirm', {
|
||||||
|
robot: robot.name,
|
||||||
|
}),
|
||||||
cancelText: t('common.cancel'),
|
cancelText: t('common.cancel'),
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
onBeforeOk: async () => {
|
onBeforeOk: async () => {
|
||||||
|
|
|
@ -54,12 +54,14 @@ export default {
|
||||||
'project.messageManagement.appSecretRequired': 'AppSecret cannot be empty',
|
'project.messageManagement.appSecretRequired': 'AppSecret cannot be empty',
|
||||||
'project.messageManagement.disableTitle': 'Are you sure you want to close {name}?',
|
'project.messageManagement.disableTitle': 'Are you sure you want to close {name}?',
|
||||||
'project.messageManagement.disableContent':
|
'project.messageManagement.disableContent':
|
||||||
'After closing, it will no longer receive in-site message notifications and will not be displayed on the message list page.',
|
'After closing, it will no longer receive {robot} message notifications and will not be displayed on the message list page.',
|
||||||
'project.messageManagement.disableConfirm': 'Confirm close',
|
'project.messageManagement.disableConfirm': 'Confirm close',
|
||||||
'project.messageManagement.disableSuccess': 'Closed successfully',
|
'project.messageManagement.disableSuccess': 'Closed successfully',
|
||||||
'project.messageManagement.enableTitle': 'Turn on {name}',
|
'project.messageManagement.enableTitle': 'Turn on {name}',
|
||||||
'project.messageManagement.enableContent':
|
'project.messageManagement.enableContent':
|
||||||
'After turning it on, site messages will be displayed in the message settings list, and the notification type needs to be manually set.',
|
'After turning it on, {robot} messages will be displayed in the message settings list, and the notification type needs to be manually set.',
|
||||||
|
'project.messageManagement.enableEmailContentTip':
|
||||||
|
'Email notifications can be sent only after configuring the SMTP service in System Settings-Email Settings.',
|
||||||
'project.messageManagement.enableConfirm': 'Confirm to open',
|
'project.messageManagement.enableConfirm': 'Confirm to open',
|
||||||
'project.messageManagement.enableSuccess': 'Opened successfully',
|
'project.messageManagement.enableSuccess': 'Opened successfully',
|
||||||
'project.messageManagement.deleteTitle': 'Confirm to delete {name}?',
|
'project.messageManagement.deleteTitle': 'Confirm to delete {name}?',
|
||||||
|
@ -100,4 +102,5 @@ export default {
|
||||||
'project.messageManagement.receiverNotNull': 'Please set at least one message recipient',
|
'project.messageManagement.receiverNotNull': 'Please set at least one message recipient',
|
||||||
'project.messageManagement.unsetReceiversTip': 'Please set the message recipients before configuring the template.',
|
'project.messageManagement.unsetReceiversTip': 'Please set the message recipients before configuring the template.',
|
||||||
'project.messageManagement.noMatchField': 'No matching fields yet',
|
'project.messageManagement.noMatchField': 'No matching fields yet',
|
||||||
|
'project.messageManagement.saveContinueText': 'Save and continue creating',
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default {
|
||||||
'project.messageManagement.updateAt': '更新于',
|
'project.messageManagement.updateAt': '更新于',
|
||||||
'project.messageManagement.status': '状态',
|
'project.messageManagement.status': '状态',
|
||||||
'project.messageManagement.statusTipOn': '开启:在消息通知列表展示及使用',
|
'project.messageManagement.statusTipOn': '开启:在消息通知列表展示及使用',
|
||||||
'project.messageManagement.statusTipOff': '关闭:暂不使用改机器人',
|
'project.messageManagement.statusTipOff': '关闭:暂不使用该机器人',
|
||||||
'project.messageManagement.choosePlatform': '选择配置平台',
|
'project.messageManagement.choosePlatform': '选择配置平台',
|
||||||
'project.messageManagement.WE_COM': '企业微信',
|
'project.messageManagement.WE_COM': '企业微信',
|
||||||
'project.messageManagement.DING_TALK': '钉钉',
|
'project.messageManagement.DING_TALK': '钉钉',
|
||||||
|
@ -49,11 +49,12 @@ export default {
|
||||||
'project.messageManagement.appSecretPlaceholder': '打开帮助文档可直接获取',
|
'project.messageManagement.appSecretPlaceholder': '打开帮助文档可直接获取',
|
||||||
'project.messageManagement.appSecretRequired': 'AppSecret 不能为空',
|
'project.messageManagement.appSecretRequired': 'AppSecret 不能为空',
|
||||||
'project.messageManagement.disableTitle': '确定关闭 {name} 吗?',
|
'project.messageManagement.disableTitle': '确定关闭 {name} 吗?',
|
||||||
'project.messageManagement.disableContent': '关闭后,将不在接收站内信通知,且不在消息列表页展示',
|
'project.messageManagement.disableContent': '关闭后,将不在接收 {robot} 通知,且不在消息列表页展示',
|
||||||
'project.messageManagement.disableConfirm': '确认关闭',
|
'project.messageManagement.disableConfirm': '确认关闭',
|
||||||
'project.messageManagement.disableSuccess': '关闭成功',
|
'project.messageManagement.disableSuccess': '关闭成功',
|
||||||
'project.messageManagement.enableTitle': '开启 {name}',
|
'project.messageManagement.enableTitle': '开启 {name}',
|
||||||
'project.messageManagement.enableContent': '开启后,站内信则显示在消息设置列表,需要手动设置通知类型',
|
'project.messageManagement.enableContent': '开启后,{robot} 则显示在消息设置列表,需要手动设置通知类型',
|
||||||
|
'project.messageManagement.enableEmailContentTip': '在系统设置-邮件设置中配置 SMTP 服务后才可发送邮件通知',
|
||||||
'project.messageManagement.enableConfirm': '确认开启',
|
'project.messageManagement.enableConfirm': '确认开启',
|
||||||
'project.messageManagement.enableSuccess': '开启成功',
|
'project.messageManagement.enableSuccess': '开启成功',
|
||||||
'project.messageManagement.deleteTitle': '确认删除 {name} ?',
|
'project.messageManagement.deleteTitle': '确认删除 {name} ?',
|
||||||
|
@ -92,4 +93,5 @@ export default {
|
||||||
'project.messageManagement.receiverNotNull': '请最少设置一位消息接收人',
|
'project.messageManagement.receiverNotNull': '请最少设置一位消息接收人',
|
||||||
'project.messageManagement.unsetReceiversTip': '配置模板前请先设置消息接收人',
|
'project.messageManagement.unsetReceiversTip': '配置模板前请先设置消息接收人',
|
||||||
'project.messageManagement.noMatchField': '暂无匹配字段',
|
'project.messageManagement.noMatchField': '暂无匹配字段',
|
||||||
|
'project.messageManagement.saveContinueText': '保存并继续创建',
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,17 +31,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-[40px] flex justify-center">
|
<div class="mt-[40px] flex justify-center">
|
||||||
<a-button type="outline" @click="() => openProjectVersion(true)">
|
<a-button v-permission="['PROJECT_VERSION:READ+UPDATE']" type="outline" @click="() => openProjectVersion(true)">
|
||||||
{{ t('project.projectVersion.openVersion') }}
|
{{ t('project.projectVersion.openVersion') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
<div v-if="projectVersionStatus">
|
<div v-if="projectVersionStatus" class="table-container">
|
||||||
<div class="flex justify-between">
|
<div class="mb-[16px] flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<a-switch
|
<a-switch
|
||||||
v-model:model-value="projectVersionStatus"
|
v-model:model-value="projectVersionStatus"
|
||||||
|
v-permission="['PROJECT_VERSION:READ+UPDATE']"
|
||||||
size="small"
|
size="small"
|
||||||
:before-change="(val) => openProjectVersion(val)"
|
:before-change="(val) => openProjectVersion(val)"
|
||||||
type="line"
|
type="line"
|
||||||
|
@ -56,14 +57,20 @@
|
||||||
allow-clear
|
allow-clear
|
||||||
@search="searchVersion"
|
@search="searchVersion"
|
||||||
@press-enter="searchVersion"
|
@press-enter="searchVersion"
|
||||||
|
@clear="searchVersion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
|
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
|
||||||
<template #statusTitle>
|
<template #statusTitle>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{{ t('project.projectVersion.status') }}
|
<div class="font-medium text-[var(--color-text-3)]">
|
||||||
|
{{ t('project.projectVersion.status') }}
|
||||||
|
</div>
|
||||||
<a-popover position="rt">
|
<a-popover position="rt">
|
||||||
<icon-info-circle class="ml-[4px] hover:text-[rgb(var(--primary-5))]" size="16" />
|
<icon-info-circle
|
||||||
|
class="ml-[4px] text-[var(--color-text-3)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="w-[256px]"> {{ t('project.projectVersion.statusTip') }} </div>
|
<div class="w-[256px]"> {{ t('project.projectVersion.statusTip') }} </div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -93,7 +100,7 @@
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #quickCreate>
|
<template v-if="hasAnyPermission(['PROJECT_VERSION:READ+ADD'])" #quickCreate>
|
||||||
<a-form
|
<a-form
|
||||||
v-if="showQuickCreateForm"
|
v-if="showQuickCreateForm"
|
||||||
ref="quickCreateFormRef"
|
ref="quickCreateFormRef"
|
||||||
|
@ -144,6 +151,7 @@
|
||||||
<template #status="{ record }">
|
<template #status="{ record }">
|
||||||
<a-switch
|
<a-switch
|
||||||
v-model:model-value="record.status"
|
v-model:model-value="record.status"
|
||||||
|
v-permission="['PROJECT_VERSION:READ+UPDATE']"
|
||||||
size="small"
|
size="small"
|
||||||
:before-change="(val) => handleStatusChange(val, record)"
|
:before-change="(val) => handleStatusChange(val, record)"
|
||||||
type="line"
|
type="line"
|
||||||
|
@ -152,6 +160,7 @@
|
||||||
<template #latest="{ record }">
|
<template #latest="{ record }">
|
||||||
<a-switch
|
<a-switch
|
||||||
v-model:model-value="record.latest"
|
v-model:model-value="record.latest"
|
||||||
|
v-permission="['PROJECT_VERSION:READ+UPDATE']"
|
||||||
:disabled="record.latest"
|
:disabled="record.latest"
|
||||||
:before-change="() => handleUseLatestVersionChange(record)"
|
:before-change="() => handleUseLatestVersionChange(record)"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -161,6 +170,7 @@
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-tooltip :content="t('project.projectVersion.latestVersionDeleteTip')" :disabled="!record.latest">
|
<a-tooltip :content="t('project.projectVersion.latestVersionDeleteTip')" :disabled="!record.latest">
|
||||||
<MsButton
|
<MsButton
|
||||||
|
v-permission="['PROJECT_VERSION:READ+DELETE']"
|
||||||
type="text"
|
type="text"
|
||||||
:loading="delLoading"
|
:loading="delLoading"
|
||||||
:disabled="record.latest"
|
:disabled="record.latest"
|
||||||
|
@ -234,6 +244,7 @@
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useModal from '@/hooks/useModal';
|
import useModal from '@/hooks/useModal';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { hasAnyPermission } from '@/utils/permission';
|
||||||
|
|
||||||
import { ProjectItem, ProjectVersionOption } from '@/models/projectManagement/projectVersion';
|
import { ProjectItem, ProjectVersionOption } from '@/models/projectManagement/projectVersion';
|
||||||
import { ColumnEditTypeEnum } from '@/enums/tableEnum';
|
import { ColumnEditTypeEnum } from '@/enums/tableEnum';
|
||||||
|
@ -310,7 +321,7 @@
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
showTooltip: true,
|
showTooltip: true,
|
||||||
editType: ColumnEditTypeEnum.INPUT,
|
editType: ColumnEditTypeEnum.INPUT,
|
||||||
width: 150,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'project.projectVersion.status',
|
title: 'project.projectVersion.status',
|
||||||
|
@ -346,6 +357,7 @@
|
||||||
{
|
{
|
||||||
title: 'project.projectVersion.creator',
|
title: 'project.projectVersion.creator',
|
||||||
dataIndex: 'createUser',
|
dataIndex: 'createUser',
|
||||||
|
showTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'common.operation',
|
title: 'common.operation',
|
||||||
|
@ -358,8 +370,8 @@
|
||||||
const { propsRes, propsEvent, loadList, setKeyword, setLoadListParams } = useTable(
|
const { propsRes, propsEvent, loadList, setKeyword, setLoadListParams } = useTable(
|
||||||
getVersionList,
|
getVersionList,
|
||||||
{
|
{
|
||||||
|
scroll: { x: '100%' },
|
||||||
columns,
|
columns,
|
||||||
size: 'default',
|
|
||||||
},
|
},
|
||||||
(item) => {
|
(item) => {
|
||||||
return {
|
return {
|
||||||
|
@ -585,4 +597,10 @@
|
||||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 10%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 3px 14px 2px rgb(0 0 0 / 5%);
|
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 10%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 3px 14px 2px rgb(0 0 0 / 5%);
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
.table-container {
|
||||||
|
.ms-scroll-bar();
|
||||||
|
@apply overflow-auto;
|
||||||
|
|
||||||
|
min-width: 850px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
<a-input-search
|
<a-input-search
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
:max-length="250"
|
:max-length="255"
|
||||||
allow-clear
|
allow-clear
|
||||||
:placeholder="t('organization.member.searchMember')"
|
:placeholder="t('organization.member.searchMember')"
|
||||||
class="w-[230px]"
|
class="w-[230px]"
|
||||||
|
|
|
@ -198,6 +198,7 @@
|
||||||
Message.success(
|
Message.success(
|
||||||
isEdit.value ? t('system.project.updateProjectSuccess') : t('system.project.createProjectSuccess')
|
isEdit.value ? t('system.project.updateProjectSuccess') : t('system.project.createProjectSuccess')
|
||||||
);
|
);
|
||||||
|
appStore.initProjectList();
|
||||||
handleCancel(true);
|
handleCancel(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
|
@ -664,7 +664,6 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const tableStore = useTableStore();
|
const tableStore = useTableStore();
|
||||||
tableStore.initColumn(TableKeyEnum.SYSTEM_AUTH, columns, 'drawer');
|
|
||||||
const { propsRes, propsEvent, loadList } = useTable(getAuthList, {
|
const { propsRes, propsEvent, loadList } = useTable(getAuthList, {
|
||||||
tableKey: TableKeyEnum.SYSTEM_AUTH,
|
tableKey: TableKeyEnum.SYSTEM_AUTH,
|
||||||
columns,
|
columns,
|
||||||
|
@ -1193,6 +1192,7 @@
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export declare type AuthConfigInstance = InstanceType<typeof _default>;
|
export declare type AuthConfigInstance = InstanceType<typeof _default>;
|
||||||
|
await tableStore.initColumn(TableKeyEnum.SYSTEM_AUTH, columns, 'drawer');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped></style>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
{
|
{
|
||||||
key: 'memoryCleanup',
|
key: 'memoryCleanup',
|
||||||
title: t('system.config.memoryCleanup'),
|
title: t('system.config.memoryCleanup'),
|
||||||
permission: ['SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ'],
|
permission: ['SYSTEM_PARAMETER_SETTING_MEMORY_CLEAN:READ+UPDATE'],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue