feat(全局): 部分组件调整&问题修复

This commit is contained in:
baiqi 2024-01-29 16:15:40 +08:00 committed by 刘瑞斌
parent 2cbeb77956
commit 58f1dda566
74 changed files with 1004 additions and 678 deletions

View File

@ -37,7 +37,7 @@
"dependencies": {
"@7polo/kity": "2.0.8",
"@7polo/kityminder-core": "1.4.53",
"@arco-design/web-vue": "^2.53.3",
"@arco-design/web-vue": "^2.54.3",
"@arco-themes/vue-ms-theme-default": "^0.0.30",
"@form-create/arco-design": "^3.1.23",
"@halo-dev/richtext-editor": "0.0.0-alpha.33",

View File

@ -181,8 +181,8 @@ export const getReviewDetailModuleCount = (data: ReviewDetailCaseListQueryParams
};
// 评审详情-已关联用例模块树
export const getReviewDetailModuleTree = (projectId: string, reviewId: string) => {
return MSR.get({ url: `${GetReviewDetailModuleTreeUrl}/${projectId}/${reviewId}` });
export const getReviewDetailModuleTree = (reviewId: string) => {
return MSR.get({ url: `${GetReviewDetailModuleTreeUrl}/${reviewId}` });
};
// 评审详情-获取用例评审历史

View File

@ -47,7 +47,7 @@ export function login(data: LoginData) {
}
export function isLogin() {
return MSR.get<LoginRes>({ url: isLoginUrl }, { ignoreCancelToken: true });
return MSR.get<LoginRes>({ url: isLoginUrl }, { ignoreCancelToken: true, errorMessageMode: 'none' });
}
export function logout() {
@ -143,18 +143,18 @@ export function updatePsw(data: UpdatePswParams) {
}
// 个人信息-校验第三方平台账号信息
export function validatePlatform(id: string, data: Record<string, any>) {
return MSR.post({ url: `${ValidatePlatformUrl}/${id}`, data });
export function validatePlatform(id: string, orgId: string, data: Record<string, any>) {
return MSR.post({ url: `${ValidatePlatformUrl}/${id}/${orgId}`, data });
}
// 个人信息-保存第三方平台账号信息
export function savePlatform(data: UpdatePswParams) {
export function savePlatform(data: Record<string, any>) {
return MSR.post({ url: SavePlatformUrl, data });
}
// 个人信息-获取第三方平台账号信息
export function getPlatform() {
return MSR.get({ url: GetPlatformUrl });
export function getPlatform(orgId: string) {
return MSR.get({ url: GetPlatformUrl, params: orgId });
}
// 个人信息-获取第三方平台账号信息-插件信息

View File

@ -1,7 +1,7 @@
@font-face {
font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1705549750803') format('woff2'), url('iconfont.woff?t=1705549750803') format('woff'),
url('iconfont.ttf?t=1705549750803') format('truetype'), url('iconfont.svg?t=1705549750803#iconfont') format('svg');
src: url('iconfont.woff2?t=1706424798592') format('woff2'), url('iconfont.woff?t=1706424798592') format('woff'),
url('iconfont.ttf?t=1706424798592') format('truetype'), url('iconfont.svg?t=1706424798592#iconfont') format('svg');
}
.iconfont {
font-size: 16px;
@ -10,6 +10,12 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-icon_swich::before {
content: '\e79c';
}
.icon-icon_split_turn-down_arrow::before {
content: '\e79b';
}
.icon-icon_carriage_return2::before {
content: '\e79a';
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,20 @@
"css_prefix_text": "icon-",
"description": "DE、MS项目icon管理",
"glyphs": [
{
"icon_id": "39108518",
"name": "icon_swich",
"font_class": "icon_swich",
"unicode": "e79c",
"unicode_decimal": 59292
},
{
"icon_id": "39084088",
"name": "icon_split_turn-down_arrow",
"font_class": "icon_split_turn-down_arrow",
"unicode": "e79b",
"unicode_decimal": 59291
},
{
"icon_id": "38923289",
"name": "icon_carriage_return",

View File

@ -14,6 +14,10 @@
/>
<missing-glyph />
<glyph glyph-name="icon_swich" unicode="&#59292;" d="M670.165333 798.165333l256-256c1.536-1.493333 2.901333-3.114667 4.138667-4.778666l3.029333-4.693334 2.304-4.864 1.493334-4.48 1.28-6.314666L938.666667 512l-0.128-3.2-0.725334-5.376-1.28-4.736-1.877333-4.736-2.218667-4.181333-2.858666-4.096-3.413334-3.84a43.050667 43.050667 0 0 0-4.778666-4.138667l-4.693334-3.029333-4.864-2.304-4.48-1.493334-6.357333-1.28L896 469.333333H128a42.666667 42.666667 0 1 0 0 85.333334h664.96l-183.125333 183.168a42.666667 42.666667 0 1 0 60.330666 60.330666zM913.066667 341.333333a42.666667 42.666667 0 0 0 0-85.333333H248.064l183.168-183.168a42.666667 42.666667 0 0 0-60.373333-60.330667l-256 256a43.008 43.008 0 0 0-4.096 4.778667l-3.072 4.693333-2.261334 4.864-1.237333 3.498667-1.152 4.864-0.597333 5.12L102.357333 298.666667l0.128 3.2 0.554667 4.266666 0.512 2.517334 1.877333 5.845333 1.578667 3.541333 1.578667 2.858667 2.858666 4.096 3.413334 3.84 2.346666 2.133333 4.010667 3.114667 3.157333 1.92 6.101334 2.773333 3.2 1.024 3.925333 0.853334 3.584 0.512L145.066667 341.333333h768z" horiz-adv-x="1024" />
<glyph glyph-name="icon_split_turn-down_arrow" unicode="&#59291;" d="M832-86.528a43.434667 43.434667 0 0 0-6.357333 0.426667l-2.218667 0.426666a40.106667 40.106667 0 0 0-13.653333 5.376 44.16 44.16 0 0 0-3.157334 2.133334l-0.938666 0.725333a43.349333 43.349333 0 0 0-3.84 3.413333l-106.666667 106.666667a42.666667 42.666667 0 0 0 60.330667 60.330667l33.834666-33.834667v195.669333a128 128 0 0 1-120.490666 127.786667l-7.509334 0.213333h-341.333333v-323.626666l33.834667 33.792a42.666667 42.666667 0 0 0 56.32 3.541333l4.010666-3.541333a42.666667 42.666667 0 0 0 0-60.330667l-106.666666-106.666667a42.666667 42.666667 0 0 0-60.330667 0l-106.666667 106.666667a42.666667 42.666667 0 0 0 60.330667 60.330667l33.834667-33.834667V560.853333A149.418667 149.418667 0 0 0 277.333333 853.333333a149.333333 149.333333 0 0 0 42.666667-292.48v-92.714666h341.333333a213.333333 213.333333 0 0 0 213.333334-213.333334v-195.626666l33.834666 33.792a42.666667 42.666667 0 0 0 56.32 3.541333l4.010667-3.541333a42.666667 42.666667 0 0 0 0-60.330667l-106.666667-106.666667-3.328-2.901333-1.450666-1.237333a43.946667 43.946667 0 0 0-16.810667-7.509334l-2.261333-0.426666-2.773334-0.256h-0.085333l-0.512-0.042667 0.469333 0.042667-3.413333-0.170667zM277.333333 768a64 64 0 1 1 0-128 64 64 0 0 1 0 128z" horiz-adv-x="1024" />
<glyph glyph-name="icon_carriage_return2" unicode="&#59290;" d="M311.168 499.498667a42.666667 42.666667 0 1 0 60.330667-60.330667L273.706667 341.333333H789.333333a21.333333 21.333333 0 0 1 21.333334 21.333334V640a42.666667 42.666667 0 0 0 85.333333 0v-277.333333a106.666667 106.666667 0 0 0-106.666667-106.666667H273.706667l97.792-97.834667a42.666667 42.666667 0 0 0 3.541333-56.32l-3.541333-4.010666a42.666667 42.666667 0 0 0-60.330667 0l-170.666667 170.666666-0.938666 0.938667a42.922667 42.922667 0 0 0-2.474667 2.901333l3.413333-3.84A43.008 43.008 0 0 0 128 297.813333V299.648c0 0.938667 0.085333 1.877333 0.170667 2.816L128 298.666667a43.008 43.008 0 0 0 9.088 26.325333c1.066667 1.322667 2.176 2.645333 3.413333 3.84l170.666667 170.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_carriage_return1" unicode="&#59289;" d="M896 725.333333a85.333333 85.333333 0 0 0 85.333333-85.333333v-512a85.333333 85.333333 0 0 0-85.333333-85.333333H336.810667a85.333333 85.333333 0 0 0-58.24 22.954666l-4.608 5.077334-222.378667 287.146666a42.666667 42.666667 0 0 0 0 52.266667l222.378667 287.189333 4.608 5.077334A85.333333 85.333333 0 0 0 336.810667 725.333333H896z m0-85.333333H337.493333l-198.229333-256 198.272-256H896V640z m-396.501333-119.168l76.501333-76.458667 76.501333 76.458667a42.666667 42.666667 0 0 0 60.330667-60.330667L636.373333 384l76.458667-76.501333a42.666667 42.666667 0 0 0-60.330667-60.330667L576 323.626667l-76.501333-76.458667a42.666667 42.666667 0 0 0-60.330667 60.330667L515.626667 384l-76.458667 76.501333a42.666667 42.666667 0 0 0 60.330667 60.330667z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 430 KiB

View File

@ -575,12 +575,12 @@
background-color: var(--color-text-input-border);
}
}
.ms-card-container .arco-scrollbar .arco-scrollbar-track-direction-vertical {
right: -10px;
}
.ms-card-container .arco-scrollbar .arco-scrollbar-track-direction-horizontal {
bottom: -10px;
}
// .ms-card-container .arco-scrollbar .arco-scrollbar-track-direction-vertical {
// right: -10px;
// }
// .ms-card-container .arco-scrollbar .arco-scrollbar-track-direction-horizontal {
// bottom: -10px;
// }
.ms-base-table .arco-scrollbar .arco-scrollbar-track-direction-vertical {
right: 0;
}

View File

@ -215,10 +215,11 @@
// TODO: arco-design cascader path-mode - v-model
function getInputLabel(data: CascaderOption) {
const isTagCount = data[props.labelKey].includes('+');
if (!props.pathMode) {
return t(data[props.labelKey].split('-').pop());
return isTagCount ? data[props.labelKey] : t(data[props.labelKey].split('-').pop());
}
return t(data[props.labelKey]);
return isTagCount ? data[props.labelKey] : t(data[props.labelKey]);
}
function clearValues() {

View File

@ -2,11 +2,11 @@
<div
class="mr-[4px] h-[8px] w-[8px] rounded-full"
:style="{
backgroundColor: caseLevelMap[props.caseLevel].bgColor,
border: `1px solid ${caseLevelMap[props.caseLevel].borderColor}`,
backgroundColor: caseLevel.bgColor,
border: `1px solid ${caseLevel.borderColor}`,
}"
></div>
{{ caseLevelMap[props.caseLevel].label }}
{{ caseLevel.label }}
</template>
<script setup lang="ts">
@ -37,7 +37,9 @@
bgColor: 'var(--color-text-n8)',
borderColor: 'var(--color-text-brand)',
},
} as const;
};
const caseLevel = computed(() => caseLevelMap[props.caseLevel] || {});
</script>
<style lang="less" scoped></style>

View File

@ -6,7 +6,7 @@
</a-button>
</div>
<div class="mb-[16px] flex items-center">
<MsAvatar :avatar="userStore.avatar || 'default'" class="mb-[4px]" />
<MsAvatar :avatar="userStore.avatar || 'default'" :size="58" class="mb-[4px]" />
<a-button
type="outline"
class="arco-btn-outline--secondary ml-[8px] p-[2px_8px]"
@ -304,4 +304,7 @@
bottom: 22px;
}
:deep(.ms-description-item-value) {
-webkit-line-clamp: unset !important;
}
</style>

View File

@ -1,7 +1,14 @@
<template>
<div class="flex h-full flex-col overflow-hidden">
<a-spin :loading="loading" class="flex h-full flex-col overflow-hidden">
<div class="mb-[16px] flex items-center justify-between">
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.tripartite') }}</div>
<MsSelect
v-model:model-value="currentOrg"
:options="orgOptions"
:loading="orgLoading"
class="w-[300px]"
@change="handleOrgChange"
/>
</div>
<div class="platform-card-container">
<div v-for="config of dynamicForm" :key="config.key" class="platform-card">
@ -30,7 +37,7 @@
</a-button>
</div>
</div>
</div>
</a-spin>
</template>
<script setup lang="ts">
@ -38,10 +45,14 @@
import MsFormCreate from '@/components/pure/ms-form-create/ms-form-create.vue';
import MsTag, { TagType } from '@/components/pure/ms-tag/ms-tag.vue';
import MsSelect from '@/components/business/ms-select';
import { getSystemOrgOption } from '@/api/modules/setting/organizationAndProject';
import { getPlatform, getPlatformAccount, savePlatform, validatePlatform } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
const appStore = useAppStore();
const { t } = useI18n();
type Status = 0 | 1 | 2;
@ -81,10 +92,33 @@
'validate-trigger': ['change'],
},
});
const currentOrg = ref(appStore.currentOrgId);
const orgOptions = ref([]);
const orgLoading = ref(false);
async function initOrgOptions() {
try {
orgLoading.value = true;
const res = await getSystemOrgOption();
orgOptions.value = res.map((e) => ({
label: e.name,
value: e.id,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
orgLoading.value = false;
}
}
const loading = ref(false);
async function initPlatformAccountInfo() {
try {
loading.value = true;
dynamicForm.value = {};
const res = await getPlatformAccount();
//
Object.keys(res).forEach((key) => {
dynamicForm.value[key] = {
key,
@ -97,22 +131,48 @@
};
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
async function initPlatformInfo() {
try {
const res = await getPlatform();
loading.value = true;
const res = await getPlatform(currentOrg.value);
//
Object.keys(dynamicForm.value).forEach((configKey: any) => {
const config = dynamicForm.value[configKey].formModel.form;
//
Object.keys(config).forEach((key) => {
const value = res[configKey][key];
config[key] = value || config[key];
dynamicForm.value[configKey].status = value !== undefined ? 1 : 0;
});
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
async function validate(config: any) {
try {
config.validateLoading = true;
await validatePlatform(config.key, config.formModel.form);
const configForms: Record<string, any> = {};
Object.keys(dynamicForm.value).forEach((key) => {
configForms[key] = {
...dynamicForm.value[key].formModel.form,
};
});
await validatePlatform(config.key, currentOrg.value, config.formModel.form);
await savePlatform({
[currentOrg.value]: configForms,
});
Message.success(t('ms.personal.validPass'));
config.status = 1;
} catch (error) {
@ -124,8 +184,14 @@
}
}
onBeforeMount(() => {
initPlatformAccountInfo();
async function handleOrgChange() {
await initPlatformAccountInfo();
initPlatformInfo();
}
onBeforeMount(async () => {
initOrgOptions();
await initPlatformAccountInfo();
initPlatformInfo();
});
</script>

View File

@ -72,7 +72,7 @@ export default {
'ms.personal.validPass': 'Verification passed',
'ms.personal.validFail': 'Verification failed',
'ms.personal.unValid': 'Not verified',
'ms.personal.valid': 'Verify',
'ms.personal.valid': 'Verify and save',
'ms.personal.authType': 'Authentication',
'ms.personal.platformAccount': 'Account',
'ms.personal.platformAccountPlaceholder': 'Please enter {type} account',

View File

@ -66,7 +66,7 @@ export default {
'ms.personal.validPass': '校验通过',
'ms.personal.validFail': '校验失败',
'ms.personal.unValid': '未校验',
'ms.personal.valid': '校验',
'ms.personal.valid': '校验并保存',
'ms.personal.authType': '认证方式',
'ms.personal.platformAccount': '平台账号',
'ms.personal.platformAccountPlaceholder': '请输入 {type} 账号',

View File

@ -69,7 +69,6 @@ export default defineComponent(
selectRef,
selectVal: innerValue,
isCascade: true,
options: props.options,
valueKey: props.valueKey,
labelKey: props.labelKey,
});
@ -79,7 +78,7 @@ export default defineComponent(
(val) => {
innerValue.value = val;
if (Array.isArray(val) && val.length > 0 && props.multiple) {
calculateMaxTag();
calculateMaxTag(remoteOriginOptions.value);
}
},
{
@ -239,6 +238,7 @@ export default defineComponent(
handleSelectAllChange(true);
}
}
calculateMaxTag(val);
}
);
@ -357,9 +357,15 @@ export default defineComponent(
popup-container={props.popupContainer || document.body}
trigger-props={props.triggerProps}
fallback-option={props.fallbackOption}
onChange={(value: ModelType) => emit('update:modelValue', value)}
onChange={(value: ModelType) => {
emit('update:modelValue', value);
emit('change', value);
}}
onSearch={handleSearch}
onPopupVisibleChange={(val: boolean) => emit('popupVisibleChange', val)}
onPopupVisibleChange={(val: boolean) => {
handleSearch('', true);
emit('popupVisibleChange', val);
}}
onRemove={(val: string | number | boolean | Record<string, any> | undefined) => emit('remove', val)}
onKeyup={(e: KeyboardEvent) => {
// 阻止组件在回车时自动触发的事件
@ -416,6 +422,6 @@ export default defineComponent(
'atLeastOne',
'objectValue',
],
emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove'],
emits: ['update:modelValue', 'remoteSearch', 'popupVisibleChange', 'update:loading', 'remove', 'change'],
}
);

View File

@ -19,7 +19,7 @@
:type="FileIconMap[fileType][UploadStatus.done]"
class="absolute top-0 h-full w-full p-[24px] text-[var(--color-text-4)]"
/>
<a-tooltip :content="props.footerText" :mouse-enter-delay="300" position="bl" mini>
<a-tooltip v-if="props.footerText" :content="props.footerText" :mouse-enter-delay="300" position="bl" mini>
<div class="ms-thumbnail-card-footer one-line-text">
{{ props.footerText }}
</div>

View File

@ -113,4 +113,12 @@
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
:deep(.arco-menu-inner) {
overflow-y: hidden;
padding: 9px 20px;
.arco-menu-selected-label {
bottom: -8px !important;
}
}
</style>

View File

@ -66,6 +66,7 @@
<script setup lang="ts">
import { h, nextTick, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
import { useVModel } from '@vueuse/core';
import { debounce } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -302,28 +303,14 @@
emit('check', checkedKeys);
}
const innerFocusNodeKey = ref(props.focusNodeKey || ''); //
watch(
() => props.focusNodeKey,
(val) => {
innerFocusNodeKey.value = val || '';
}
);
watch(
() => innerFocusNodeKey.value,
(val) => {
emit('update:focusNodeKey', val);
}
);
const innerFocusNodeKey = useVModel(props, 'focusNodeKey', emit); //
const focusEl = ref<HTMLElement | null>(); //
watch(
() => innerFocusNodeKey.value,
(val) => {
if (val.toString() !== '') {
if (val?.toString() !== '') {
focusEl.value = treeRef.value?.$el.querySelector(`[data-key="${val}"]`);
if (focusEl.value) {
focusEl.value.style.backgroundColor = 'rgb(var(--primary-1))';
@ -355,21 +342,7 @@
}
);
const innerSelectedKeys = ref(props.selectedKeys || []);
watch(
() => props.selectedKeys,
(val) => {
innerSelectedKeys.value = val || [];
}
);
watch(
() => innerSelectedKeys.value,
(val) => {
emit('update:selectedKeys', val);
}
);
const innerSelectedKeys = useVModel(props, 'selectedKeys', emit);
</script>
<style lang="less">
@ -381,6 +354,14 @@
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
.arco-tree-node-title {
background-color: rgb(var(--primary-1));
&:not([draggable='false']) {
.arco-tree-node-title-text {
width: calc(100% - 22px);
}
}
}
}
.arco-tree-node-indent-block {
width: 1px;
@ -409,7 +390,7 @@
&:hover {
background-color: rgb(var(--primary-1));
+ .ms-tree-node-extra {
@apply block;
@apply visible w-auto;
}
}
.arco-tree-node-title-text {
@ -418,7 +399,7 @@
.arco-tree-node-drag-icon {
@apply cursor-move;
right: 4px;
right: 6px;
.arco-icon {
font-size: 14px;
}
@ -428,9 +409,9 @@
width: 80%;
}
.ms-tree-node-extra {
@apply relative hidden;
@apply invisible relative w-0;
&:hover {
@apply block;
@apply visible w-auto;
}
.ms-tree-node-extra__btn,
.ms-tree-node-extra__more {
@ -448,7 +429,7 @@
}
}
.ms-tree-node-extra--focus {
@apply block;
@apply visible w-auto;
}
.arco-tree-node-custom-icon {
@apply hidden;

View File

@ -1,6 +1,6 @@
<template>
<MsIcon
v-if="props.avatar === 'default'"
v-if="props.avatar === 'default' || props.avatar === null"
type="icon-icon_that_person"
:size="props.size"
class="text-[var(--color-text-4)]"

View File

@ -1,18 +1,19 @@
<template>
<a-spin class="!block h-full" :loading="props.loading" :size="28">
<div
ref="fullRef"
:class="[
'ms-card',
'relative',
'h-full',
props.isFullscreen ? 'ms-card--no-radius' : '',
props.isFullscreen || isFullScreen ? 'ms-card--no-radius' : '',
props.autoHeight ? '' : 'min-h-[500px]',
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
props.noBottomRadius ? 'ms-card--noBottomRadius' : '',
]"
>
<a-scrollbar v-if="!props.simple" :style="{ overflow: 'auto' }">
<div class="card-header" :style="props.minWidth ? { minWidth: `${props.minWidth}px` } : {}">
<div class="ms-card-header" :style="props.headerMinWidth ? { minWidth: `${props.headerMinWidth}px` } : {}">
<div v-if="!props.hideBack" class="back-btn" @click="back"><icon-arrow-left /></div>
<slot name="headerLeft">
<div class="font-medium text-[var(--color-text-000)]">{{ props.title }}</div>
@ -20,6 +21,15 @@
</slot>
<div class="ml-auto flex items-center">
<slot name="headerRight"></slot>
<div
v-if="props.showFullScreen"
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="toggleFullScreen"
>
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
<MsIcon v-else type="icon-icon_magnify_outlined" />
{{ t('msCodeEditor.fullScreen') }}
</div>
</div>
<div v-if="$slots.subHeader" class="basis-full">
<slot name="subHeader"></slot>
@ -30,7 +40,7 @@
<a-divider v-if="!props.simple && !props.hideDivider" class="mb-[16px] mt-0" />
</div>
<div class="ms-card-container">
<a-scrollbar :class="props.noContentPadding ? '' : 'pr-[5px]'" :style="getComputedContentStyle">
<a-scrollbar :class="['h-full', props.noContentPadding ? '' : 'pr-[5px]']" :style="getComputedContentStyle">
<div class="relative h-full w-full" :style="{ minWidth: `${props.minWidth || 1000}px` }">
<slot></slot>
</div>
@ -38,7 +48,7 @@
</div>
<div
v-if="!props.hideFooter && !props.simple"
class="fixed bottom-0 right-[16px] z-10 flex items-center bg-white p-[24px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]"
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)]"
:style="{ width: `calc(100% - ${menuWidth + 16}px)` }"
>
<div class="ml-0 mr-auto">
@ -64,6 +74,7 @@
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -81,7 +92,8 @@
hideBack: boolean; //
autoHeight: boolean; //
otherWidth: number; //
minWidth: number; //
headerMinWidth: number; //
minWidth: number; //
hasBreadcrumb: boolean; //
noContentPadding: boolean; // padding
noBottomRadius?: boolean; //
@ -89,6 +101,7 @@
hideDivider?: boolean; // 线
handleBack: () => void; //
dividerHasPX: boolean; // 线padding;
showFullScreen: boolean; //
}>
>(),
{
@ -117,6 +130,10 @@
return appStore.menuCollapse ? collapsedWidth : appStore.menuWidth;
});
// ref
const fullRef = ref<HTMLElement | null>();
const { isFullScreen, toggleFullScreen } = useFullScreen(fullRef);
const _specialHeight = props.hasBreadcrumb ? 32 + props.specialHeight : props.specialHeight; // 32
const cardOverHeight = computed(() => {
@ -132,6 +149,13 @@
});
const getComputedContentStyle = computed(() => {
if (props.isFullscreen || isFullScreen.value) {
return {
overflow: 'auto',
width: 'auto',
height: 'auto',
};
}
if (props.noContentPadding) {
return {
overflow: 'auto',
@ -139,13 +163,6 @@
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight.value}px)`,
};
}
if (props.isFullscreen) {
return {
overflow: 'auto',
width: 'calc(100vw - 58px)',
height: 'auto',
};
}
return {
overflow: 'auto',
width: props.otherWidth
@ -172,7 +189,7 @@
box-shadow: 0 0 10px rgb(120 56 135 / 5%);
&--noContentPadding {
border-radius: var(--border-radius-large);
.card-header {
.ms-card-header {
padding: 24px 24px 16px;
}
.arco-divider {
@ -182,7 +199,7 @@
&--noBottomRadius {
border-radius: var(--border-radius-large) var(--border-radius-large) 0 0;
}
.card-header {
.ms-card-header {
@apply flex flex-wrap items-center;
padding-bottom: 16px;
@ -200,6 +217,9 @@
}
}
}
.ms-card-container {
@apply h-full;
}
}
.ms-card--no-radius {
border-radius: 0;

View File

@ -34,9 +34,9 @@
<div
v-if="showFullScreen"
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="toggle"
@click="toggleFullScreen"
>
<MsIcon v-if="isFullscreen" type="icon-icon_minify_outlined" />
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
<MsIcon v-else type="icon-icon_magnify_outlined" />
{{ t('msCodeEditor.fullScreen') }}
</div>
@ -44,7 +44,7 @@
</div>
<!-- 这里的 40px 是顶部标题的 40px -->
<div :class="`flex ${showTitleLine ? 'h-[calc(100%-40px)]' : 'h-full'} w-full flex-row`">
<div ref="codeContainerRef" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
<div ref="codeContainerRef" :class="['ms-code-editor', isFullScreen ? 'ms-code-editor-full-screen' : '']"></div>
<slot name="rightBox"> </slot>
</div>
</div>
@ -52,9 +52,9 @@
<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { codeCharset } from '@/config/apiTest';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import { decodeStringToCharset } from '@/utils';
@ -183,7 +183,7 @@
}
};
const { isFullscreen, toggle } = useFullscreen(fullRef);
const { isFullScreen, toggleFullScreen } = useFullScreen(fullRef);
//
const insertContent = (text: string) => {
@ -257,7 +257,7 @@
return {
codeContainerRef,
fullRef,
isFullscreen,
isFullScreen,
currentTheme,
themeOptions,
currentLanguage,
@ -265,7 +265,7 @@
currentCharset,
charsetOptions,
showTitleLine,
toggle,
toggleFullScreen,
t,
handleThemeChange,
handleLanguageChange,

View File

@ -10,6 +10,7 @@
'ms-drawer',
props.mask ? '' : 'ms-drawer-no-mask',
props.noContentPadding ? 'ms-drawer-no-content-padding' : '',
props.noTitle ? 'ms-drawer-no-title' : '',
]"
@cancel="handleCancel"
@close="handleClose"
@ -20,15 +21,15 @@
<div class="flex items-center">
{{ props.title }}
<slot name="headerLeft"></slot>
<a-tag v-if="titleTag" :color="props.titleTagColor" class="ml-[8px] mr-auto">{{
props.titleTag
}}</a-tag></div
>
<a-tag v-if="titleTag" :color="props.titleTagColor" class="ml-[8px] mr-auto">
{{ props.titleTag }}
</a-tag>
</div>
<slot name="tbutton"></slot>
</div>
</slot>
</template>
<div v-if="!props.disabledWidthDrag" class="handle" @mousedown="startResize">
<div v-if="!props.disabledWidthDrag && typeof drawerWidth === 'number'" class="handle" @mousedown="startResize">
<icon-drag-dot-vertical class="absolute left-[-3px] top-[50%] w-[14px]" size="14" />
</div>
<a-scrollbar class="h-full overflow-y-auto">
@ -110,10 +111,13 @@
cancelText?: string;
saveContinueText?: string;
showContinue?: boolean;
width: number;
width: string | number; //
noContentPadding?: boolean; //
popupContainer?: string;
disabledWidthDrag?: boolean; //
closable?: boolean; //
noTitle?: boolean; //
drawerStyle?: Record<string, string>; //
}
const props = withDefaults(defineProps<DrawerProps>(), {
@ -164,33 +168,35 @@
* 鼠标单击开始监听拖拽移动
*/
const startResize = (event: MouseEvent) => {
resizing.value = true;
const startX = event.clientX;
const initialWidth = drawerWidth.value;
if (typeof drawerWidth.value === 'number') {
resizing.value = true;
const startX = event.clientX;
const initialWidth = drawerWidth.value;
//
const handleMouseMove = (_event: MouseEvent) => {
if (resizing.value) {
const newWidth = initialWidth + (startX - _event.clientX); // +
if (newWidth >= (props.width || 480) && newWidth <= window.innerWidth * 0.9) {
// width48090%
drawerWidth.value = newWidth;
//
const handleMouseMove = (_event: MouseEvent) => {
if (resizing.value) {
const newWidth = initialWidth + (startX - _event.clientX); // +
if (newWidth >= (props.width || 480) && newWidth <= window.innerWidth * 0.9) {
// width48090%
drawerWidth.value = newWidth;
}
}
}
};
};
//
const handleMouseUp = () => {
if (resizing.value) {
//
resizing.value = false;
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
};
//
const handleMouseUp = () => {
if (resizing.value) {
//
resizing.value = false;
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
};
</script>
@ -232,6 +238,11 @@
right: -12px;
}
}
.ms-drawer-no-title {
.arco-drawer-header {
@apply hidden;
}
}
.ms-drawer-no-mask {
left: auto;
.arco-drawer {

View File

@ -19,11 +19,11 @@
:class="{ active: innerActiveTab === tab.id }"
@click="handleTabClick(tab)"
>
<div class="flex items-center">
<div :draggable="!!tab.draggable" class="flex items-center">
<slot name="label" :tab="tab">{{ tab.label }}</slot>
<div v-if="tab.unSaved" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
<MsButton
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable"
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable !== false"
type="icon"
status="secondary"
class="ms-editable-tab-close-button"

View File

@ -3,5 +3,6 @@ export interface TabItem {
label: string;
closable?: boolean;
unSaved?: boolean; // 未保存
draggable?: boolean;
[key: string]: any;
}

View File

@ -17,8 +17,9 @@
:style="{
'border-top': item.level === 1 && index !== 0 ? '1px solid var(--color-border-2)' : 'none',
}"
@click.stop="toggleMenu(item)"
>
<div @click="toggleMenu(item.name)">{{ item.title }}</div>
<div>{{ item.title }}</div>
</div>
</div>
</div>
@ -28,15 +29,15 @@
<script setup lang="ts">
import { hasAnyPermission } from '@/utils/permission';
interface MenuItem {
title: string;
level: number;
name: string;
}
const props = defineProps<{
title?: string;
defaultKey?: string;
menuList: {
title: string;
level: number;
name: string;
permission?: string[];
}[];
menuList: MenuItem[];
activeClass?: string;
}>();
const emit = defineEmits<{
@ -56,10 +57,10 @@
}
);
const toggleMenu = (itemName: string) => {
if (itemName) {
currentKey.value = itemName;
emit('toggleMenu', itemName);
const toggleMenu = (item: MenuItem) => {
if (item.level !== 1 && item.name !== currentKey.value) {
currentKey.value = item.name;
emit('toggleMenu', item.name);
}
};
</script>

View File

@ -174,11 +174,13 @@
</template>
<template #empty>
<slot name="empty">
<div class="flex h-[20px] flex-col items-center justify-center">
<span class="text-[14px] text-[var(--color-text-4)]">{{ t('msTable.empty') }}</span>
</div>
</slot>
<div class="w-full">
<slot name="empty">
<div class="flex h-[20px] flex-col items-center justify-center">
<span class="text-[14px] text-[var(--color-text-4)]">{{ t('msTable.empty') }}</span>
</div>
</slot>
</div>
</template>
<template #expand-icon="{ expanded }">
<MsIcon v-if="!expanded" :size="8" type="icon-icon_right_outlined" class="text-[var(--color-text-4)]" />
@ -258,6 +260,7 @@
MsTableProps,
} from './type';
import type { TableColumnData, TableData } from '@arco-design/web-vue';
import type { TableOperationColumn } from '@arco-design/web-vue/es/table/interface';
const batchLeft = ref('10px');
const { t } = useI18n();
@ -275,7 +278,15 @@
noDisable?: boolean;
showSetting?: boolean;
columns: MsTableColumn;
spanMethod?: (params: { record: TableData; rowIndex: number; columnIndex: number }) => void;
spanMethod?: (params: {
record: TableData;
column: TableColumnData | TableOperationColumn;
rowIndex: number;
columnIndex: number;
}) => void | {
rowspan?: number | undefined;
colspan?: number | undefined;
};
expandedKeys?: string[];
rowClass?: string | any[] | Record<string, any> | ((record: TableData, rowIndex: number) => any);
spanAll?: boolean;

View File

@ -276,6 +276,8 @@ export default function useTableProps<T>(
} catch (err) {
setTableErrorStatus('error');
propsRes.value.data = [];
// eslint-disable-next-line no-console
console.log(err);
} finally {
setLoading(false);
}

View File

@ -73,8 +73,8 @@ export const FileIconMap: FileIconMapping = {
[UploadStatus.done]: 'icon-icon_file-unknow_colorful1',
},
json: {
[UploadStatus.init]: 'icon-icon_file-json_colorful_ash',
[UploadStatus.done]: 'icon-icon_file-json_colorful1',
[UploadStatus.init]: 'icon-a-icon_file-json',
[UploadStatus.done]: 'icon-a-icon_file-json',
},
};

View File

@ -34,12 +34,14 @@
{{ t(props.mainText || 'ms.upload.importModalDragText') }}
</div>
<div v-if="showSubText" class="ms-upload-sub-text">
{{
t(props.subText || 'ms.upload.importModalFileTip', {
type: UploadAcceptEnum[props.accept],
size: props.maxSize || defaultMaxSize,
})
}}
<slot name="subText">
{{
t(props.subText || 'ms.upload.importModalFileTip', {
type: UploadAcceptEnum[props.accept],
size: props.maxSize || defaultMaxSize,
})
}}
</slot>
</div>
</template>
<template v-else>

View File

@ -3,8 +3,8 @@
<div class="left-side">
<a-space>
<div class="one-line-text flex max-w-[145px] items-center">
<img :src="props.logo" class="mr-[4px] h-[32px] w-[32px]" />
{{ props.name }}
<img :src="props.logo" class="mr-[4px] h-[34px] w-[32px]" />
<div class="font-['Helvetica_Neue'] text-[16px] font-bold text-[rgb(var(--primary-5))]">{{ props.name }}</div>
</div>
</a-space>
</div>
@ -135,7 +135,7 @@
import type { ProjectListItem } from '@/models/setting/project';
import { IconCompass, IconFile, IconInfoCircle, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
import { IconInfoCircle, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
const props = defineProps<{
isPreview?: boolean;
@ -207,21 +207,21 @@
}
const helpCenterList = [
{
name: 'settings.help.guide',
icon: IconCompass,
route: '/help-center/guide',
},
// {
// name: 'settings.help.guide',
// icon: IconCompass,
// route: '/help-center/guide',
// },
{
name: 'settings.help.doc',
icon: IconQuestionCircle,
route: '/help-center/guide',
},
{
name: 'settings.help.APIDoc',
icon: IconFile,
route: '/help-center/guide',
},
// {
// name: 'settings.help.APIDoc',
// icon: IconFile,
// route: '/help-center/guide',
// },
{
name: 'settings.help.version',
icon: IconInfoCircle,

View File

@ -9,7 +9,7 @@ export type PathMapRoute = (typeof RouteEnum)[PathMapKey];
export interface PathMapItem {
key: PathMapKey | string; // 系统设置
locale: string;
route: PathMapRoute;
route: PathMapRoute | string;
permission?: [];
level: (typeof MENU_LEVEL)[number]; // 系统设置里有系统级别也有组织级别,按最低权限级别配置
children?: PathMapItem[];
@ -35,12 +35,18 @@ export const pathMap: PathMapItem[] = [
level: MENU_LEVEL[2],
children: [
{
key: 'API_TEST_DEBUG', // 接口测试
key: 'API_TEST_DEBUG', // 接口测试-接口调试
locale: 'menu.apiTest.debug',
route: RouteEnum.API_TEST_DEBUG,
permission: [],
level: MENU_LEVEL[2],
children: [],
},
{
key: 'API_TEST_MANAGEMENT', // 接口测试-接口管理
locale: 'menu.apiTest.management',
route: RouteEnum.API_TEST_MANAGEMENT,
permission: [],
level: MENU_LEVEL[2],
},
],
},
@ -87,13 +93,6 @@ export const pathMap: PathMapItem[] = [
route: RouteEnum.CASE_MANAGEMENT_CASE,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_REVIEW', // 功能测试-功能用例-用例评审
locale: 'menu.caseManagement.caseManagementReview',
route: RouteEnum.CASE_MANAGEMENT_REVIEW,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例详情
@ -116,6 +115,22 @@ export const pathMap: PathMapItem[] = [
permission: [],
level: MENU_LEVEL[2],
},
],
},
{
key: 'CASE_MANAGEMENT_REVIEW', // 功能测试-功能用例-用例评审
locale: 'menu.caseManagement.caseManagementReview',
route: RouteEnum.CASE_MANAGEMENT_REVIEW,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'CASE_MANAGEMENT_REVIEW_LIST', // 功能测试-功能用例-用例评审列表
locale: 'menu.caseManagement.caseManagementReview',
route: RouteEnum.CASE_MANAGEMENT_REVIEW,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_REVIEW_CREATE', // 功能测试-功能用例-创建评审
locale: 'menu.caseManagement.caseManagementReviewCreate',
@ -123,6 +138,13 @@ export const pathMap: PathMapItem[] = [
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_REVIEW_UPDATE', // 功能测试-功能用例-更新评审
locale: 'menu.caseManagement.caseManagementCaseReviewEdit',
route: RouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'CASE_MANAGEMENT_REVIEW_DETAIL', // 功能测试-功能用例-评审详情
locale: 'menu.caseManagement.caseManagementReviewDetail',
@ -484,4 +506,48 @@ export const pathMap: PathMapItem[] = [
},
],
},
{
key: 'PERSONAL_INFORMATION', // 个人信息
locale: 'ms.personal',
route: '',
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'PERSONAL_INFORMATION_BASE_INFO', // 个人信息-基本信息
locale: 'ms.personal.baseInfo',
route: '',
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'PERSONAL_INFORMATION_PSW', // 个人信息-密码设置
locale: 'ms.personal.setPsw',
route: '',
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'PERSONAL_INFORMATION_APIKEYS', // 个人信息-ApiKeys
locale: 'APIKEY',
route: '',
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'PERSONAL_INFORMATION_LOCAL_EXECUTE', // 个人信息-本地执行
locale: 'ms.personal.localExecution',
route: '',
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'PERSONAL_INFORMATION_TRIPARTITE', // 个人信息-三方平台账号
locale: 'ms.personal.tripartite',
route: '',
permission: [],
level: MENU_LEVEL[2],
},
],
},
];

View File

@ -52,3 +52,14 @@ export enum ResponseComposition {
EXTRACT = 'EXTRACT',
ASSERTION = 'ASSERTION',
}
// 接口定义状态
export enum RequestDefinitionStatus {
DEPRECATED = 'DEPRECATED',
PROCESSING = 'PROCESSING',
DEBUGGING = 'DEBUGGING',
DONE = 'DONE',
}
// 接口导入支持格式
export enum RequestImportFormat {
SWAGGER = 'SWAGGER',
}

View File

@ -1,6 +1,7 @@
export enum ApiTestRouteEnum {
API_TEST = 'apiTest',
API_TEST_DEBUG = 'apiTestDebug',
API_TEST_MANAGEMENT = 'apiTestManagement',
}
export enum BugManagementRouteEnum {

View File

@ -0,0 +1,30 @@
/**
* hook
* @param domRef dom ref
*/
export default function useFullScreen(domRef: Ref<HTMLElement | null | undefined>) {
const isFullScreen = ref(false);
function enterFullScreen() {
domRef.value?.setAttribute('style', 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100;');
isFullScreen.value = true;
}
function exitFullscreen() {
domRef.value?.setAttribute('style', '');
isFullScreen.value = false;
}
function toggleFullScreen() {
if (isFullScreen.value) {
exitFullscreen();
} else {
enterFullScreen();
}
}
return {
isFullScreen,
toggleFullScreen,
};
}

View File

@ -28,8 +28,9 @@ export default function useSelect(config: UseSelectOption) {
/**
*
* @param options
*/
function calculateMaxTag() {
function calculateMaxTag(options?: CascaderOption[] | SelectOptionData[]) {
nextTick(() => {
if (config.selectRef.value && selectViewInner.value && Array.isArray(config.selectVal.value)) {
if (maxTagCount.value >= 1 && config.selectVal.value.length > maxTagCount.value) return; // 已经超过最大数量的展示,不需要再计算
@ -38,7 +39,9 @@ export default function useSelect(config: UseSelectOption) {
let tagCount = 0;
const values = Object.values(config.selectVal.value);
for (let i = 0; i < values.length; i++) {
const tagWidth = (values[i][config.labelKey || 'label']?.length || 0) * 12; // 计算每个标签渲染出来的宽度文字大小在12px时宽度也是 12px
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
if (lastWidth > tagWidth + 36) {
tagCount += 1;
lastWidth -= tagWidth + 36; // 36px是标签的边距、边框等宽度

View File

@ -6,11 +6,11 @@ import { useAppStore, useUserStore } from '@/store';
export default function useUser() {
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const { t } = useI18n();
const logout = async (logoutTo?: string) => {
const appStore = useAppStore();
const userStore = useUserStore();
await userStore.logout();
const currentRoute = router.currentRoute.value;
// 清空顶部菜单

View File

@ -102,4 +102,10 @@ export default {
'common.noResource': 'No resource, please contact the administrator',
'common.noSelectProject': 'No optional items available',
'common.noPermission': 'No permission',
'common.batchEdit': 'Batch Edit',
'common.tagsInputPlaceholder': 'Enter the content and press Enter to directly add tags',
'common.move': 'Move',
'common.batchMove': 'Batch move',
'common.batchMoveSuccess': 'Batch move successful',
'common.importSuccess': 'Import successful',
};

View File

@ -25,6 +25,7 @@ export default {
'menu.caseManagement': 'Case Management',
'menu.apiTest': 'API Test',
'menu.apiTest.debug': 'API debug',
'menu.apiTest.management': 'API Management',
'menu.uiTest': 'UI Test',
'menu.performanceTest': 'Performance Test',
'menu.projectManagement': 'Project',
@ -40,6 +41,7 @@ export default {
'menu.caseManagement.caseManagementReview': 'Feature Case Review',
'menu.caseManagement.caseManagementReviewCreate': 'Create Review',
'menu.caseManagement.caseManagementReviewDetail': 'Review Detail',
'menu.caseManagement.caseManagementReviewDetailCaseDetail': 'Review Case Detail',
'menu.caseManagement.caseManagementCaseReviewEdit': 'Update Review',
'menu.caseManagement.caseManagementCaseDetail': 'Case Detail',
'menu.workstation': 'Workstation',

View File

@ -105,4 +105,10 @@ export default {
'common.noResource': '暂无资源权限,请联系管理员',
'common.noSelectProject': '无可选项目',
'common.noPermission': '无权限',
'common.batchEdit': '批量编辑',
'common.tagsInputPlaceholder': '输入内容后回车可直接添加标签',
'common.move': '移动',
'common.batchMove': '批量移动',
'common.batchMoveSuccess': '批量移动成功',
'common.importSuccess': '导入成功',
};

View File

@ -24,6 +24,7 @@ export default {
'menu.caseManagement': '功能测试',
'menu.apiTest': '接口测试',
'menu.apiTest.debug': '接口调试',
'menu.apiTest.management': '接口管理',
'menu.uiTest': 'UI测试',
'menu.workstation': '工作台',
'menu.loadTest': '性能测试',
@ -44,6 +45,7 @@ export default {
'menu.caseManagement.caseManagementReview': '用例评审',
'menu.caseManagement.caseManagementReviewCreate': '创建评审',
'menu.caseManagement.caseManagementReviewDetail': '评审详情',
'menu.caseManagement.caseManagementReviewDetailCaseDetail': '评审用例详情',
'menu.caseManagement.caseManagementCaseReviewEdit': '更新评审',
'menu.caseManagement.caseManagementCaseDetail': '用例详情',
'menu.projectManagement.projectPermission': '项目与权限',

View File

@ -29,7 +29,7 @@ const defaultLoginConfig = {
icon: [],
loginLogo: [],
loginImage: [],
slogan: '一站式开源持续测试平台',
slogan: 'login.form.title',
};
const defaultPlatformConfig = {
logoPlatform: [],

View File

@ -10,9 +10,11 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router';
import { WorkbenchRouteEnum } from '@/enums/routeEnum';
const router = useRouter();
const back = () => {
// warning Go to the node that has the permission
router.push({ name: 'workstation' });
router.push({ name: WorkbenchRouteEnum.WORKBENCH });
};
</script>

View File

@ -66,8 +66,13 @@
</template>
</a-dropdown>
</MsButton>
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]" @click="toggle">
<MsIcon :type="isFullscreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'" class="mr-1" size="16" />
<MsButton
type="icon"
status="secondary"
class="!rounded-[var(--border-radius-small)]"
@click="toggleFullScreen"
>
<MsIcon :type="isFullScreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'" class="mr-1" size="16" />
{{ t('caseManagement.featureCase.fullScreen') }}
</MsButton>
</div>
@ -150,7 +155,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
@ -169,6 +173,7 @@
import CommentTab from './commentTab.vue';
import { createOrUpdateComment, deleteSingleBug, followBug, getBugDetail } from '@/api/modules/bug-management/index';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store';
@ -181,7 +186,7 @@
const router = useRouter();
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
const wrapperRef = ref();
const { isFullscreen, toggle } = useFullscreen(wrapperRef);
const { isFullScreen, toggleFullScreen } = useFullScreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore();
const { t } = useI18n();
const { openModal } = useModal();

View File

@ -77,8 +77,13 @@
</template>
</a-dropdown>
</MsButton>
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]" @click="toggle">
<MsIcon :type="isFullscreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'" class="mr-1" size="16" />
<MsButton
type="icon"
status="secondary"
class="!rounded-[var(--border-radius-small)]"
@click="toggleFullScreen"
>
<MsIcon :type="isFullScreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'" class="mr-1" size="16" />
{{ t('caseManagement.featureCase.fullScreen') }}
</MsButton>
</div>
@ -194,7 +199,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
@ -226,6 +230,7 @@
getCaseDetail,
getCaseModuleTree,
} from '@/api/modules/case-management/featureCase';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store';
@ -244,7 +249,7 @@
const router = useRouter();
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
const wrapperRef = ref();
const { isFullscreen, toggle } = useFullscreen(wrapperRef);
const { isFullScreen, toggleFullScreen } = useFullScreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore();
const userStore = useUserStore();
const { t } = useI18n();

View File

@ -7,14 +7,14 @@
>
</template>
<template #operation="{ record }">
<MsButton v-if="record.demandPlatform !== pageConfig.platformName" @click="emit('update', record)">{{
t('caseManagement.featureCase.cancelAssociation')
}}</MsButton>
<MsButton v-if="record.demandPlatform === pageConfig.platformName" @click="emit('update', record)">{{
t('common.edit')
}}</MsButton>
<MsButton v-if="record.demandPlatform !== pageConfig.platformName" @click="emit('update', record)">
{{ t('caseManagement.featureCase.cancelAssociation') }}
</MsButton>
<MsButton v-if="record.demandPlatform === pageConfig.platformName" @click="emit('update', record)">
{{ t('common.edit') }}
</MsButton>
</template>
<template v-if="(props.funParams.keyword || '').trim() === ''" #empty>
<template v-if="(props.funParams.keyword || '').trim() === '' && props.showEmpty" #empty>
<div class="flex w-full items-center justify-center">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="emit('create')">
@ -53,6 +53,7 @@
}; //
isShowOperation?: boolean; //
highlightName?: boolean; //
showEmpty?: boolean; //
}>(),
{
isShowOperation: true,

View File

@ -98,11 +98,8 @@
{{ t('common.save') }}
</a-button></div
>
<a-form-item
field="attachment"
:label="props.allowEdit ? t('caseManagement.featureCase.attachment') : '附件列表'"
>
<div v-if="props.allowEdit" class="flex flex-col">
<a-form-item v-if="props.allowEdit" field="attachment" :label="t('caseManagement.featureCase.attachment')">
<div class="flex flex-col">
<div class="mb-1">
<a-dropdown position="tr" trigger="hover">
<a-button v-permission="['FUNCTIONAL_CASE:READ+UPDATE']" type="outline">
@ -119,26 +116,28 @@
>
<template #upload-button>
<a-button type="text" class="!text-[var(--color-text-1)]">
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}</a-button
>
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}
</a-button>
</template>
</a-upload>
<a-button type="text" class="!text-[var(--color-text-1)]" @click="associatedFile">
<MsIcon type="icon-icon_link-copy_outlined" size="16" />{{
t('caseManagement.featureCase.associatedFile')
}}</a-button
>
<MsIcon type="icon-icon_link-copy_outlined" size="16" />
{{ t('caseManagement.featureCase.associatedFile') }}
</a-button>
</template>
</a-dropdown>
</div>
<div class="!hover:bg-[rgb(var(--primary-1))] !text-[var(--color-text-4)]">{{
t('system.orgTemplate.addAttachmentTip')
}}</div>
<div class="!hover:bg-[rgb(var(--primary-1))] !text-[var(--color-text-4)]">
{{ t('system.orgTemplate.addAttachmentTip') }}
</div>
</div>
</a-form-item>
</a-form>
<!-- 文件列表开始 -->
<div class="w-[90%]">
<div v-if="!props.allowEdit" class="mb-[16px] font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.featureCase.attachment') }}
</div>
<MsFileList
ref="fileListRef"
v-model:file-list="fileList"

View File

@ -153,7 +153,7 @@ export default {
'caseManagement.featureCase.fullScreen': 'Full screen',
'caseManagement.featureCase.more': 'More',
'caseManagement.featureCase.basicInfo': 'Basic Info',
'caseManagement.featureCase.attachment': 'attachment',
'caseManagement.featureCase.attachment': 'Attachment',
'caseManagement.featureCase.contentEdit': 'Content Edit',
'caseManagement.featureCase.followSuccess': 'Followed Success',
'caseManagement.featureCase.cancelFollowSuccess': 'Cancel success',

View File

@ -1,5 +1,5 @@
<template>
<MsCard :loading="loading" :min-width="1100" has-breadcrumb hide-footer no-content-padding hide-divider>
<MsCard :min-width="1100" has-breadcrumb hide-footer no-content-padding hide-divider show-full-screen>
<template #headerLeft>
<a-tooltip :content="reviewDetail.name">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
@ -21,275 +21,258 @@
{{ t('caseManagement.caseReview.myReview') }}
</div>
</template>
<div class="h-full px-[24px]">
<a-divider class="my-0" />
<div class="flex h-[calc(100%-1px)] w-full">
<div class="h-full w-[356px] border-r border-[var(--color-text-n8)] pr-[16px] pt-[16px]">
<div class="mb-[16px] flex">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.caseReview.searchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadCaseList"
@press-enter="loadCaseList"
/>
<a-select v-model:model-value="type" :options="typeOptions" class="w-[92px]" @change="loadCaseList">
</a-select>
</div>
<a-spin :loading="caseListLoading" class="w-full">
<div class="case-list">
<div
v-for="item of caseList"
:key="item.caseId"
:class="['case-item', caseDetail.id === item.caseId ? 'case-item--active' : '']"
@click="changeActiveCase(item)"
>
<div class="mb-[4px] flex items-center justify-between">
<div>{{ item.num }}</div>
<div class="flex items-center gap-[4px] leading-[22px]">
<MsIcon
:type="reviewResultMap[item.status]?.icon"
:style="{ color: reviewResultMap[item.status]?.color }"
/>
{{ t(reviewResultMap[item.status]?.label) }}
</div>
</div>
<a-tooltip :content="item.name">
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div>
<MsEmpty v-if="caseList.length === 0" />
</div>
<MsPagination
v-model:page-size="pageNation.pageSize"
v-model:current="pageNation.current"
:total="pageNation.total"
size="mini"
simple
@change="loadCaseList"
@page-size-change="loadCaseList"
/>
</a-spin>
<div class="flex h-full w-full border-t border-[var(--color-text-n8)]">
<div class="h-full w-[356px] border-r border-[var(--color-text-n8)] py-[16px] pl-[24px] pr-[16px]">
<div class="mb-[16px] flex">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.caseReview.searchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadCaseList"
@press-enter="loadCaseList"
/>
<a-select v-model:model-value="type" :options="typeOptions" class="w-[92px]" @change="loadCaseList">
</a-select>
</div>
<div class="relative flex w-[calc(100%-356px)] flex-col">
<div class="pl-[16px] pt-[16px]">
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]">
<div class="mb-[12px] flex items-center justify-between">
<a-tooltip :content="`【${caseDetail.num}】${caseDetail.name}`">
<div
class="one-line-text cursor-pointer font-medium text-[rgb(var(--primary-5))]"
@click="goCaseDetail"
>
{{ caseDetail.num }}{{ caseDetail.name }}
</div>
</a-tooltip>
<a-button
type="outline"
size="mini"
class="arco-btn-outline--secondary"
@click="editCaseVisible = true"
>
{{ t('common.edit') }}
</a-button>
</div>
<div class="flex items-center">
<MsIcon type="icon-icon_folder_filled1" class="mr-[4px] text-[var(--color-text-4)]" />
<a-tooltip :content="caseDetail.moduleName || t('common.root')">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
{{ caseDetail.moduleName || t('common.root') }}
</div>
</a-tooltip>
<div class="case-detail-label">
{{ t('caseManagement.caseReview.caseLevel') }}
</div>
<div class="case-detail-value">
<caseLevel :case-level="caseDetailLevel" />
</div>
<div class="case-detail-label">
{{ t('caseManagement.caseReview.caseVersion') }}
</div>
<div class="case-detail-value">
<MsIcon type="icon-icon_version" size="13" class="mr-[4px]" />
{{ caseDetail.versionName }}
</div>
<div class="case-detail-label">
{{ t('caseManagement.caseReview.reviewResult') }}
</div>
<div class="case-detail-value">
<div
v-if="reviewResultMap[activeCaseReviewStatus as ReviewResult]"
class="flex items-center gap-[4px]"
>
<MsIcon
:type="reviewResultMap[activeCaseReviewStatus as ReviewResult].icon"
:style="{
color: reviewResultMap[activeCaseReviewStatus as ReviewResult].color,
}"
/>
{{ t(reviewResultMap[activeCaseReviewStatus as ReviewResult].label) }}
</div>
<a-spin :loading="caseListLoading" class="h-[calc(100%-46px)] w-full">
<div class="case-list">
<div
v-for="item of caseList"
:key="item.caseId"
:class="['case-item', caseDetail.id === item.caseId ? 'case-item--active' : '']"
@click="changeActiveCase(item)"
>
<div class="mb-[4px] flex items-center justify-between">
<div>{{ item.num }}</div>
<div class="flex items-center gap-[4px] leading-[22px]">
<MsIcon
:type="reviewResultMap[item.status]?.icon"
:style="{ color: reviewResultMap[item.status]?.color }"
/>
{{ t(reviewResultMap[item.status]?.label) }}
</div>
</div>
</div>
<a-tabs v-model:active-key="showTab" class="no-content">
<a-tab-pane :key="tabList[0].key" :title="tabList[0].title" />
<a-tab-pane :key="tabList[1].key" :title="tabList[1].title" />
<a-tab-pane :key="tabList[2].key">
<template #title>
<div class="flex items-center">
{{ tabList[2].title }}
<div
:class="`ml-[4px] rounded-full ${
showTab === tabList[2].key ? 'bg-[rgb(var(--primary-5))]' : 'bg-[var(--color-text-brand)]'
} px-[4px] text-[12px] text-white`"
>
{{ caseDetail.demandCount > 99 ? '99+' : caseDetail.demandCount }}
</div>
</div>
</template>
</a-tab-pane>
</a-tabs>
</div>
<a-divider class="my-0" />
<div class="content-center">
<MsDescription v-if="showTab === 'baseInfo'" :descriptions="descriptions" label-width="90px" />
<div v-else-if="showTab === 'detail'" class="h-full">
<MsSplitBox :size="0.8" direction="vertical" min="0" :max="0.99">
<template #first>
<caseTabDetail :form="caseDetail" :allow-edit="false" />
</template>
<template #second>
<div class="flex h-full flex-col overflow-hidden">
<div class="my-[8px] font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.reviewHistory') }}
</div>
<div class="review-history-list">
<a-spin :loading="reviewHistoryListLoading" class="h-full w-full">
<div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]">
<div class="flex items-center">
<MSAvatar :avatar="item.userLogo" />
<div class="ml-[8px] flex items-center">
<div class="font-medium text-[var(--color-text-1)]">{{ item.userName }}</div>
<a-divider direction="vertical" margin="8px"></a-divider>
<div v-if="item.status === 'PASS'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }}
</div>
<div v-else-if="item.status === 'UN_PASS'" class="flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.caseReview.fail') }}
</div>
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.caseReview.suggestion') }}
</div>
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
<MsIcon
type="icon-icon_resubmit_filled"
class="mr-[4px] text-[rgb(var(--warning-6))]"
/>
{{ t('caseManagement.caseReview.reReview') }}
</div>
</div>
</div>
<div class="ml-[48px] text-[var(--color-text-2)]" v-html="item.contentText"></div>
<div class="ml-[48px] mt-[8px] text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
<MsEmpty v-if="reviewHistoryList.length === 0" />
</a-spin>
</div>
</div>
</template>
</MsSplitBox>
</div>
<div v-else>
<div class="flex items-center justify-between">
{{ t('caseManagement.caseReview.demandCases') }}
<a-input-search
v-model="demandKeyword"
:placeholder="t('caseManagement.caseReview.demandSearchPlaceholder')"
allow-clear
class="w-[300px]"
@press-enter="searchDemand"
@search="searchDemand"
/>
</div>
<caseTabDemand
ref="caseDemandRef"
:fun-params="{ caseId: route.query.caseId as string, keyword: demandKeyword,projectId:appStore.currentProjectId }"
/>
</div>
</div>
<div class="content-footer">
<div class="mb-[16px] flex items-center">
<div class="font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.startReview') }}
</div>
<a-switch v-model:model-value="autoNext" class="mx-[8px]" size="small" type="line" />
<div class="text-[var(--color-text-4)]">{{ t('caseManagement.caseReview.autoNext') }}</div>
<a-tooltip position="right">
<template #content>
<div>{{ t('caseManagement.caseReview.autoNextTip1') }}</div>
<div>{{ t('caseManagement.caseReview.autoNextTip2') }}</div>
</template>
<icon-question-circle
class="mb-[2px] ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<a-tooltip :content="item.name">
<div class="one-line-text">{{ item.name }}</div>
</a-tooltip>
</div>
<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') }}
<MsEmpty v-if="caseList.length === 0" />
</div>
<MsPagination
v-model:page-size="pageNation.pageSize"
v-model:current="pageNation.current"
:total="pageNation.total"
size="mini"
simple
@change="loadCaseList"
@page-size-change="loadCaseList"
/>
</a-spin>
</div>
<a-spin :loading="caseDetailLoading" class="relative flex flex-1 flex-col">
<div class="content-center">
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[16px]">
<div class="mb-[12px] flex items-center justify-between">
<a-tooltip :content="`【${caseDetail.num}】${caseDetail.name}`">
<div
class="one-line-text cursor-pointer font-medium text-[rgb(var(--primary-5))]"
@click="goCaseDetail"
>
{{ caseDetail.num }}{{ caseDetail.name }}
</div>
</a-tooltip>
<a-button type="outline" size="mini" class="arco-btn-outline--secondary" @click="editCaseVisible = true">
{{ t('common.edit') }}
</a-button>
</div>
<div class="flex items-center">
<MsIcon type="icon-icon_folder_filled1" class="mr-[4px] text-[var(--color-text-4)]" />
<a-tooltip :content="caseDetail.moduleName || t('common.root')">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
{{ caseDetail.moduleName || t('common.root') }}
</div>
</a-tooltip>
<div class="case-detail-label">
{{ t('caseManagement.caseReview.caseLevel') }}
</div>
<div class="case-detail-value">
<caseLevel :case-level="caseDetailLevel" />
</div>
<div class="case-detail-label">
{{ t('caseManagement.caseReview.caseVersion') }}
</div>
<div class="case-detail-value">
<MsIcon type="icon-icon_version" size="13" class="mr-[4px]" />
{{ caseDetail.versionName }}
</div>
<div class="case-detail-label">
{{ t('caseManagement.caseReview.reviewResult') }}
</div>
<div class="case-detail-value">
<div v-if="reviewResultMap[activeCaseReviewStatus as ReviewResult]" class="flex items-center gap-[4px]">
<MsIcon
:type="reviewResultMap[activeCaseReviewStatus as ReviewResult].icon"
:style="{
color: reviewResultMap[activeCaseReviewStatus as ReviewResult].color,
}"
/>
{{ t(reviewResultMap[activeCaseReviewStatus as ReviewResult].label) }}
</div>
</div>
</div>
</div>
<a-tabs v-model:active-key="showTab" class="no-content">
<a-tab-pane :key="tabList[0].key" :title="tabList[0].title" />
<a-tab-pane :key="tabList[1].key" :title="tabList[1].title" />
<a-tab-pane :key="tabList[2].key">
<template #title>
<div class="flex items-center">
{{ tabList[2].title }}
<div
v-if="caseDetail.demandCount > 0"
:class="`ml-[4px] rounded-full ${
showTab === tabList[2].key ? 'bg-[rgb(var(--primary-5))]' : 'bg-[var(--color-text-brand)]'
} px-[4px] text-[12px] text-white`"
>
{{ caseDetail.demandCount > 99 ? '99+' : caseDetail.demandCount }}
</div>
</div>
</template>
</a-tab-pane>
<a-tab-pane :key="tabList[3].key" :title="tabList[3].title" />
</a-tabs>
<a-divider class="my-0" />
<MsDescription
v-if="showTab === 'baseInfo'"
:descriptions="descriptions"
label-width="90px"
class="mt-[16px]"
/>
<div v-else-if="showTab === 'detail'" class="mt-[16px] h-full">
<caseTabDetail :form="caseDetail" :allow-edit="false" />
</div>
<div v-else-if="showTab === 'demand'">
<div class="mt-[16px] flex items-center justify-between">
{{ t('caseManagement.caseReview.demandCases') }}
<a-input-search
v-model="demandKeyword"
:placeholder="t('caseManagement.caseReview.demandSearchPlaceholder')"
allow-clear
class="w-[300px]"
@press-enter="searchDemand"
@search="searchDemand"
/>
</div>
<caseTabDemand
ref="caseDemandRef"
:fun-params="{ projectId: appStore.currentProjectId, caseId: route.query.caseId as string, keyword: demandKeyword }"
:show-empty="false"
/>
</div>
<div v-else class="flex h-full flex-col overflow-hidden">
<div class="review-history-list">
<a-spin :loading="reviewHistoryListLoading" class="h-full w-full">
<div v-for="item of reviewHistoryList" :key="item.id" class="mb-[16px]">
<div class="flex items-center">
<MSAvatar :avatar="item.userLogo" />
<div class="ml-[8px] flex items-center">
<div class="font-medium text-[var(--color-text-1)]">{{ item.userName }}</div>
<a-divider direction="vertical" margin="8px"></a-divider>
<div v-if="item.status === 'PASS'" class="flex items-center">
<MsIcon type="icon-icon_succeed_filled" class="mr-[4px] text-[rgb(var(--success-6))]" />
{{ t('caseManagement.caseReview.pass') }}
</div>
<div v-else-if="item.status === 'UN_PASS'" class="flex items-center">
<MsIcon type="icon-icon_close_filled" class="mr-[4px] text-[rgb(var(--danger-6))]" />
{{ t('caseManagement.caseReview.fail') }}
</div>
<div v-else-if="item.status === 'UNDER_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_warning_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.caseReview.suggestion') }}
</div>
<div v-else-if="item.status === 'RE_REVIEWED'" class="flex items-center">
<MsIcon type="icon-icon_resubmit_filled" class="mr-[4px] text-[rgb(var(--warning-6))]" />
{{ t('caseManagement.caseReview.reReview') }}
</div>
</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'
? [{ required: true, message: t('caseManagement.caseReview.reasonRequired') }]
: []
"
asterisk-position="end"
class="mb-0"
>
<MsRichText v-model:modelValue="caseResultForm.reason" class="w-full" />
</a-form-item>
</a-form>
<a-button type="primary" class="mt-[16px]" :loading="submitReviewLoading" @click="submitReview">
{{ t('caseManagement.caseReview.submitReview') }}
</a-button>
</div>
<div class="ml-[48px] text-[var(--color-text-2)]" v-html="item.contentText"></div>
<div class="ml-[48px] mt-[8px] text-[var(--color-text-4)]">
{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
<MsEmpty v-if="reviewHistoryList.length === 0" />
</a-spin>
</div>
</div>
</div>
</div>
<div class="content-footer">
<div class="mb-[16px] flex items-center">
<div class="font-medium text-[var(--color-text-1)]">
{{ t('caseManagement.caseReview.startReview') }}
</div>
<a-switch v-model:model-value="autoNext" class="mx-[8px]" size="small" type="line" />
<div class="text-[var(--color-text-4)]">{{ t('caseManagement.caseReview.autoNext') }}</div>
<a-tooltip position="right">
<template #content>
<div>{{ t('caseManagement.caseReview.autoNextTip1') }}</div>
<div>{{ t('caseManagement.caseReview.autoNextTip2') }}</div>
</template>
<icon-question-circle
class="mb-[2px] ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<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 === '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">
{{ t('caseManagement.caseReview.submitReview') }}
</a-button>
</div>
</a-spin>
</div>
</MsCard>
<MsDrawer
@ -455,9 +438,11 @@
activeCaseId.value = item.caseId;
}
}
const caseDetailLoading = ref(false);
//
async function loadCaseDetail() {
try {
caseDetailLoading.value = true;
const res = await getCaseDetail(activeCaseId.value);
caseDetail.value = res;
descriptions.value = [
@ -467,12 +452,19 @@
},
//
...res.customFields.map((e) => {
const val =
typeof e.defaultValue === 'string' && e.defaultValue !== '' ? JSON.parse(e.defaultValue) : e.defaultValue;
return {
label: e.fieldName,
value: Array.isArray(val) ? val.join('、') : val,
};
try {
const val =
typeof e.defaultValue === 'string' && e.defaultValue !== '' ? JSON.parse(e.defaultValue) : e.defaultValue;
return {
label: e.fieldName,
value: Array.isArray(val) ? val.join('、') : val,
};
} catch (error) {
return {
label: e.fieldName,
value: e.defaultValue,
};
}
}),
{
label: t('caseManagement.caseReview.creator'),
@ -486,6 +478,8 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
caseDetailLoading.value = false;
}
}
@ -503,6 +497,10 @@
key: 'demand',
title: t('caseManagement.caseReview.caseDemand'),
},
{
key: 'reviewHistory',
title: t('caseManagement.caseReview.reviewHistory'),
},
]);
const reviewHistoryListLoading = ref(false);
@ -526,9 +524,7 @@
() => activeCaseId.value,
() => {
loadCaseDetail();
if (showTab.value === 'detail') {
initReviewHistoryList();
}
initReviewHistoryList();
}
);
@ -577,14 +573,21 @@
reason: '',
};
if (autoNext.value) {
// id
const index = caseList.value.findIndex((e) => e.caseId === activeCaseId.value);
if (index < caseList.value.length - 1) {
activeCaseId.value = caseList.value[index + 1].caseId;
} else {
//
loadCaseDetail();
initReviewHistoryList();
}
} else {
//
loadCaseDetail();
initReviewHistoryList();
}
loadCaseList();
loadCaseDetail();
initReviewHistoryList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -654,8 +657,10 @@
.case-list {
.ms-scroll-bar();
overflow-y: auto;
margin-bottom: 16px;
padding: 16px;
height: calc(100% - 46px);
border-radius: var(--border-radius-small);
background: var(--color-text-n9);
.case-item {
@ -691,21 +696,18 @@
@apply flex-1 overflow-auto;
.ms-scroll-bar();
padding: 16px 0 16px 16px;
padding: 16px;
.review-history-list {
@apply overflow-auto;
.ms-scroll-bar();
@apply h-full;
padding: 16px 0 16px 16px;
}
}
.content-footer {
padding: 16px;
width: calc(100% + 32px);
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%);
:deep(.arco-radio-label) {
@apply inline-flex;
}
}
</style>
@/config/caseManagement

View File

@ -7,7 +7,7 @@
:get-modules-func="getCaseModuleTree"
:get-table-func="getCaseList"
:confirm-loading="confirmLoading"
:associated-ids="associatedIds"
:associated-ids="[]"
:type="RequestModuleEnum.CASE_MANAGEMENT"
@close="emit('close')"
@save="saveHandler"
@ -90,10 +90,12 @@
const props = defineProps<{
visible: boolean;
project: string;
// associatedIds: string[];
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:project', val: string): void;
(e: 'update:associatedIds', val: string[]): void;
(e: 'success', val: BaseAssociateCaseRequest & { reviewers: string[] }): void;
(e: 'close'): void;
}>();
@ -136,7 +138,7 @@
const currentSelectCase = ref<string | number | Record<string, any> | undefined>('');
const associatedIds = ref<string[]>([]);
// const associatedIds = useVModel(props, 'associatedIds', emit);
const confirmLoading = ref<boolean>(false);
function saveHandler(params: BaseAssociateCaseRequest) {
@ -144,7 +146,7 @@
if (!errors) {
try {
confirmLoading.value = true;
associatedIds.value = [...params.selectIds];
// associatedIds.value = [...params.selectIds];
emit('success', { ...params, reviewers: associateForm.value.reviewers });
innerVisible.value = false;
} catch (error) {

View File

@ -44,10 +44,10 @@
</a-tooltip>
</div>
</template>
<template #name="{ record }">
<a-tooltip :content="record.name">
<template #num="{ record }">
<a-tooltip :content="record.num">
<a-button type="text" class="px-0" @click="review(record)">
<div class="one-line-text max-w-[168px]">{{ record.name }}</div>
<div class="one-line-text max-w-[168px]">{{ record.num }}</div>
</a-button>
</a-tooltip>
</template>
@ -86,7 +86,7 @@
</MsPopconfirm>
</template>
<template v-if="keyword.trim() === ''" #empty>
<div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]">
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="createCase">
{{ t('caseManagement.caseReview.crateCase') }}
@ -158,11 +158,7 @@
asterisk-position="end"
class="mb-0"
>
<!-- <a-input
v-model:model-value="dialogForm.reason"
:placeholder="t('caseManagement.caseReview.reasonPlaceholder')"
/> -->
<MsRichText v-model:modelValue="dialogForm.reason" class="w-full" />
<MsRichText v-model:raw="dialogForm.reason" class="w-full" />
</a-form-item>
<a-form-item
v-if="dialogShowType === 'changeReviewer'"
@ -298,17 +294,19 @@
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
showTooltip: true,
width: 100,
},
{
title: 'caseManagement.caseReview.caseName',
slotName: 'name',
dataIndex: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showTooltip: true,
width: 200,
},
{
@ -317,6 +315,7 @@
slotName: 'reviewNames',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
@ -352,6 +351,7 @@
{
scroll: { x: '100%' },
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
heightUsed: 484,
showSetting: true,
selectable: true,
showSelectAll: true,

View File

@ -56,7 +56,6 @@
import { getReviewDetailModuleTree } from '@/api/modules/case-management/caseReview';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/projectManagement/file';
@ -70,7 +69,6 @@
const emit = defineEmits(['init', 'folderNodeSelect']);
const route = useRoute();
const appStore = useAppStore();
const { t } = useI18n();
const virtualListProps = computed(() => {
@ -111,7 +109,7 @@
async function initModules() {
try {
loading.value = true;
const res = await getReviewDetailModuleTree(appStore.currentProjectId, route.query.id as string);
const res = await getReviewDetailModuleTree(route.query.id as string);
folderTree.value = mapTree<ModuleTreeNode>(res, (node) => {
return {
...node,

View File

@ -31,7 +31,7 @@
<a-button
type="primary"
status="danger"
:disabled="confirmReviewName !== props.record.name"
:disabled="confirmReviewName !== props.record.name || loading"
class="ml-[12px]"
@click="handleDeleteConfirm"
>
@ -39,6 +39,7 @@
</a-button>
<a-button
v-if="props.record.status === 'COMPLETED'"
:loading="loading"
type="primary"
class="ml-[12px]"
@click="handleDeleteConfirm"
@ -79,6 +80,7 @@
const dialogVisible = useVModel(props, 'visible', emit);
const confirmReviewName = ref('');
const loading = ref(false);
function handleDialogCancel() {
dialogVisible.value = false;
@ -90,6 +92,7 @@
*/
async function handleDeleteConfirm() {
try {
loading.value = true;
await deleteReview(props.record.id, appStore.currentProjectId);
Message.success(t('common.deleteSuccess'));
dialogVisible.value = false;
@ -97,9 +100,10 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
</script>
<style lang="less" scoped></style>
@/config/caseManagement

View File

@ -33,7 +33,7 @@
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<a-button type="text" class="arco-btn-text--secondary" @click="statusFilterVisible = true">
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="statusFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
@ -67,10 +67,10 @@
</a-tooltip>
</div>
</template>
<template #name="{ record }">
<a-tooltip :content="record.name">
<template #num="{ record }">
<a-tooltip :content="`${record.num}`">
<a-button type="text" class="px-0" @click="openDetail(record.id)">
<div class="one-line-text max-w-[168px]">{{ record.name }}</div>
<div class="one-line-text max-w-[168px]">{{ record.num }}</div>
</a-button>
</a-tooltip>
</template>
@ -109,7 +109,7 @@
<MsTableMoreAction :list="getMoreAction(record.status)" @select="handleMoreActionSelect($event, record)" />
</template>
<template v-if="keyword.trim() === ''" #empty>
<div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]">
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('caseManagement.caseReview.tableNoData') }}
<MsButton class="ml-[8px]" @click="() => emit('goCreate')">
{{ t('caseManagement.caseReview.create') }}
@ -339,17 +339,19 @@
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
showTooltip: true,
width: 100,
},
{
title: 'caseManagement.caseReview.name',
slotName: 'name',
dataIndex: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showTooltip: true,
width: 200,
},
{
@ -382,6 +384,7 @@
dataIndex: 'reviewers',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
@ -677,4 +680,3 @@
</script>
<style lang="less" scoped></style>
@/config/caseManagement

View File

@ -40,7 +40,7 @@
<div>{{ t('caseManagement.caseReview.reReview') }}</div>
</td>
<td class="popover-value-td">
{{ props.reviewDetail.reviewedCount }}
{{ props.reviewDetail.reReviewedCount }}
</td>
</tr>
<tr>
@ -66,7 +66,7 @@
reviewDetail: {
passCount: number;
unPassCount: number;
reviewedCount: number;
reReviewedCount: number;
underReviewedCount: number;
caseCount: number;
[key: string]: any;
@ -81,7 +81,7 @@
props.reviewDetail.status === 'PREPARED' ||
(props.reviewDetail.passCount === 0 &&
props.reviewDetail.unPassCount === 0 &&
props.reviewDetail.reviewedCount === 0 &&
props.reviewDetail.reReviewedCount === 0 &&
props.reviewDetail.underReviewedCount === 0)
) {
return [
@ -101,7 +101,7 @@
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.reviewDetail.reviewedCount / props.reviewDetail.caseCount) * 100,
percentage: (props.reviewDetail.reReviewedCount / props.reviewDetail.caseCount) * 100,
color: 'rgb(var(--warning-6))',
},
{

View File

@ -1,5 +1,14 @@
<template>
<MsCard :loading="loading" :min-width="1100" auto-height hide-footer no-bottom-radius no-content-padding hide-divider>
<MsCard
:loading="loading"
:header-min-width="1100"
:min-width="150"
auto-height
hide-footer
no-bottom-radius
no-content-padding
hide-divider
>
<template #headerLeft>
<a-tooltip :content="reviewDetail.name">
<div class="one-line-text mr-[8px] max-w-[260px] font-medium text-[var(--color-text-000)]">
@ -284,11 +293,11 @@
eventTag: 'createCase',
icon: 'icon-icon_add_outlined-1',
},
{
label: t('caseManagement.caseReview.createTestPlan'),
eventTag: 'createTestPlan',
icon: 'icon-icon_add_outlined-1',
},
// {
// label: t('caseManagement.caseReview.createTestPlan'),
// eventTag: 'createTestPlan',
// icon: 'icon-icon_add_outlined-1',
// },
{
isDivider: true,
},

View File

@ -13,10 +13,10 @@
<!-- TOTO 第一版本暂时只考虑普通登录 -->
<!-- <a-form-item class="login-form-item" field="radio" hide-label>
<a-radio-group v-model="userInfo.authenticate" type="button">
<a-radio value="LOCAL">普通登陆</a-radio>
<a-radio v-xpack value="LDAP">LDAP</a-radio>
<a-radio v-xpack value="OAuth2">OAuth2 测试</a-radio>
<a-radio v-xpack value="OIDC 90">OIDC 90</a-radio>
<a-radio value="LOCAL">{{ t('login.form.normalLogin') }}</a-radio>
<a-radio value="LDAP">LDAP</a-radio>
<a-radio value="OAuth2">{{ t('login.form.oauth2Test') }}</a-radio>
<a-radio value="OIDC 90">OIDC 90</a-radio>
</a-radio-group>
</a-form-item> -->
<a-form-item

View File

@ -1,5 +1,5 @@
export default {
'login.form.title': 'Login to MeterSphere',
'login.form.title': 'One-stop open source continuous testing platform',
'login.form.userName.errMsg': 'Username cannot be empty',
'login.form.password.errMsg': 'Password cannot be empty',
'login.form.login.errMsg': 'Login error, refresh and try again',
@ -10,5 +10,6 @@ export default {
'login.form.forgetPassword': 'Forgot password',
'login.form.login': 'login',
'login.form.register': 'register account',
'login.banner.slogan1': 'Out-of-the-box high-quality template',
'login.form.normalLogin': 'Normal login',
'login.form.oauth2Test': 'OAuth2 Test',
};

View File

@ -1,14 +1,15 @@
export default {
'login.form.title': '一站式开源持续测试平台',
'login.form.userName.errMsg': '用户名不能为空',
'login.form.userName.errMsg': '邮箱不能为空',
'login.form.password.errMsg': '密码不能为空',
'login.form.login.errMsg': '登录出错,请刷新重试',
'login.form.login.success': '欢迎使用',
'login.form.userName.placeholder': '用户名',
'login.form.userName.placeholder': '请输入邮箱登录',
'login.form.password.placeholder': '密码',
'login.form.rememberPassword': '记住密码',
'login.form.forgetPassword': '忘记密码',
'login.form.login': '登录',
'login.form.register': '注册账号',
'login.banner.slogan': '开箱即用的高质量模板',
'login.form.normalLogin': '普通登录',
'login.form.oauth2Test': 'OAuth2 测试',
};

View File

@ -67,11 +67,17 @@
<div class="mb-[16px] h-[102px] w-[102px]">
<a-spin class="h-full w-full" :loading="fileLoading">
<MsThumbnailCard
mode="hover"
:mode="detail.storage === 'GIT' ? 'default' : 'hover'"
:type="detail.fileType || ''"
:url="`${CompressImgUrl}/${userStore.id}/${detail.id}`"
:footer-text="t('project.fileManagement.replaceFile')"
@click="handleFileIconClick"
:footer-text="detail.storage === 'GIT' ? undefined : t('project.fileManagement.replaceFile')"
@click="
() => {
if (detail.storage !== 'GIT') {
handleFileIconClick();
}
}
"
/>
</a-spin>
</div>

View File

@ -31,8 +31,8 @@
>
<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-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
<div class="one-line-text 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">

View File

@ -83,7 +83,7 @@
/>
</template>
<template v-if="keyword.trim() === ''" #empty>
<div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]">
<div class="flex w-full items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('project.fileManagement.tableNoFile') }}
<MsButton class="ml-[8px]" @click="handleAddClick">
{{ t('project.fileManagement.addFile') }}

View File

@ -10,8 +10,8 @@
<div class="folder-count">({{ myFileCount }})</div>
</div>
</div>
<div class="folder">
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
<div class="folder" @click="setActiveFolder('all')">
<div :class="getFolderClass('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('project.fileManagement.allFile') }}</div>
<div class="folder-count">({{ allFileCount }})</div>

View File

@ -1,5 +1,5 @@
<template>
<MsCard ref="fullRef" :special-height="132" :is-fullscreen="isFullscreen" simple>
<MsCard ref="fullRef" :special-height="132" :is-fullscreen="isFullScreen" simple>
<div id="mscard">
<div class="mb-[16px] flex items-center justify-between">
<div class="font-medium text-[var(--color-text-000)]">{{ t('project.messageManagement.config') }}</div>
@ -14,7 +14,7 @@
:multiple="true"
:has-all-select="true"
:default-all-select="true"
:popup-container="isFullscreen ? '#mscard' : undefined"
:popup-container="isFullScreen ? '#mscard' : undefined"
>
<template #footer>
<div class="mb-[6px] mt-[4px] p-[3px_8px]">
@ -25,15 +25,15 @@
</div>
</template>
</MsSelect>
<a-button type="outline" class="arco-btn-outline--secondary px-[5px]" @click="toggle">
<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'"
: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') }}
{{ t(isFullScreen ? 'common.offFullScreen' : 'common.fullScreen') }}
</a-button>
</div>
</div>
@ -42,7 +42,6 @@
v-bind="propsRes"
v-model:expandedKeys="expandedKeys"
no-disable
span-all
:indent-size="0"
v-on="propsEvent"
>
@ -71,7 +70,7 @@
:remote-func="getMessageUserList"
:remote-fields-map="{ label: 'name', value: 'id', id: 'id' }"
:not-auto-init-search="true"
:popup-container="isFullscreen ? '#mscard' : undefined"
:popup-container="isFullScreen ? '#mscard' : undefined"
:fallback-option="(val) => ({
label: (val as Record<string, any>).name,
value: val,
@ -91,7 +90,7 @@
size="small"
type="line"
/>
<a-popover position="right" :popup-container="isFullscreen ? '#mscard' : undefined">
<a-popover position="right" :popup-container="isFullScreen ? '#mscard' : undefined">
<div
class="ml-[8px] mr-[4px] cursor-pointer text-[var(--color-text-1)] hover:text-[rgb(var(--primary-6))]"
>
@ -123,7 +122,6 @@
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -141,6 +139,7 @@
getRobotList,
saveMessageConfig,
} from '@/api/modules/project-management/messageManagement';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -160,7 +159,7 @@
const robotOptions = ref<(SelectOptionData & RobotItem)[]>([]);
const fullRef = ref<HTMLElement | null>();
const { isFullscreen, toggle } = useFullscreen(fullRef);
const { isFullScreen, toggleFullScreen } = useFullScreen(fullRef);
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
const staticColumns: MsTableColumn = [
@ -287,7 +286,7 @@
functionName: (item as unknown as MessageItem).name,
taskType: child.taskType,
name: child.taskTypeName,
rowspan: child.messageTaskDetailDTOList.length,
rowspan: child.messageTaskDetailDTOList.length || 1,
...grandson,
});
}

View File

@ -20,7 +20,7 @@
<div class="flex gap-[12px] p-[16px]">
<a-avatar>MS</a-avatar>
<div class="flex flex-1 flex-col">
<div class="font-medium text-[var(--color-text-1)]">{{ subject || '-' }}</div>
<div class="font-medium text-[var(--color-text-1)]" v-html="subject || '-'"></div>
<div class="mt-[4px] text-[var(--color-text-2)]" v-html="template || '-'"></div>
<div class="text-[var(--color-text-4)]">{{ dayjs().format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
@ -31,8 +31,7 @@
v-else-if="props.robot.platform === 'MAIL'"
class="preview-rounded w-[400px] bg-[var(--color-text-n9)] p-[12px] text-[14px]"
>
<div class="mb-[4px] text-[16px] font-medium leading-[24px] text-[var(--color-text-1)]">
{{ subject || '-' }}
<div class="mb-[4px] text-[16px] font-medium leading-[24px] text-[var(--color-text-1)]" v-html="subject || '-'">
</div>
<div class="mb-[8px] flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
@ -216,9 +215,6 @@
// 使 {{name}}
function replacePreviewName(str: string) {
return str
.replace(/<|>/g, (match) => {
return match === '<' ? '&lt;' : '&gt;';
})
.replace(/{{(.*?)}}/g, `<span style='color: rgb(var(--primary-6))'>&lt;$1&gt;</span>`)
.replace(/\n/g, '<br>');
}

View File

@ -329,6 +329,7 @@
dataIndex: 'publishTime',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},
@ -337,6 +338,7 @@
dataIndex: 'createTime',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 180,
},

View File

@ -1,6 +1,6 @@
<template>
<div>
<MsCard :loading="loading" simple>
<MsCard :loading="loading" auto-height simple>
<div class="mb-4 flex items-center justify-between">
<a-button v-permission="['SYSTEM_PARAMETER_SETTING_AUTH:READ+ADD']" type="primary" @click="createAuth">
{{ t('system.config.auth.add') }}
@ -100,7 +100,7 @@
<a-input
v-model:model-value="activeAuthForm.configuration.casUrl"
:max-length="250"
:placeholder="t('system.config.auth.serviceUrlPlaceholder')"
:placeholder="t('system.config.auth.commonUrlPlaceholder', { url: 'http://<casurl>' })"
allow-clear
></a-input>
</a-form-item>
@ -114,7 +114,7 @@
<a-input
v-model:model-value="activeAuthForm.configuration.loginUrl"
:max-length="250"
:placeholder="t('system.config.auth.loginUrlPlaceholder')"
:placeholder="t('system.config.auth.commonUrlPlaceholder', { url: 'http://<casurl>/login' })"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.loginUrlTip')" :show-fill-icon="false" />
@ -129,7 +129,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.redirectUrl"
:max-length="250"
:placeholder="t('system.config.auth.callbackUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<meteresphere-endpoint>/sso/callback/cas/suthld',
})
"
allow-clear
></a-input>
</a-form-item>
@ -143,7 +147,7 @@
<a-input
v-model:model-value="activeAuthForm.configuration.validateUrl"
:max-length="250"
:placeholder="t('system.config.auth.verifyUrlPlaceholder')"
:placeholder="t('system.config.auth.commonUrlPlaceholder', { url: 'http://<casurl>/serviceValidate' })"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.verifyUrlTip')" :show-fill-icon="false" />
@ -160,7 +164,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.authUrl"
:max-length="250"
:placeholder="t('system.config.auth.authUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/auth',
})
"
allow-clear
></a-input>
</a-form-item>
@ -174,7 +182,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.tokenUrl"
:max-length="250"
:placeholder="t('system.config.auth.tokenUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/token',
})
"
allow-clear
></a-input>
</a-form-item>
@ -188,7 +200,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.userInfoUrl"
:max-length="250"
:placeholder="t('system.config.auth.userInfoUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/userinfo',
})
"
allow-clear
></a-input>
</a-form-item>
@ -202,7 +218,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.redirectUrl"
:max-length="250"
:placeholder="t('system.config.auth.OIDCCallbackUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<metersphere-endpoint>/sso/callback or http://<metersphere-endpoint>/sso/callback/authld',
})
"
allow-clear
></a-input>
</a-form-item>
@ -244,7 +264,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.logoutUrl"
:max-length="250"
:placeholder="t('system.config.auth.logoutSessionUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>/auth/realms/<metersphere>/protocol/openid-connect/logout',
})
"
allow-clear
></a-input>
</a-form-item>
@ -252,7 +276,7 @@
<a-input
v-model:model-value="activeAuthForm.configuration.loginUrl"
:max-length="250"
:placeholder="t('system.config.auth.loginUrlPlaceholder')"
:placeholder="t('system.config.auth.commonUrlPlaceholder', { url: 'http://<casurl>/login' })"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.loginUrlTip')" :show-fill-icon="false" />
@ -269,7 +293,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.authUrl"
:max-length="250"
:placeholder="t('system.config.auth.authUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/auth',
})
"
allow-clear
></a-input>
</a-form-item>
@ -283,7 +311,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.tokenUrl"
:max-length="250"
:placeholder="t('system.config.auth.tokenUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/token',
})
"
allow-clear
></a-input>
</a-form-item>
@ -297,7 +329,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.userInfoUrl"
:max-length="250"
:placeholder="t('system.config.auth.userInfoUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/userinfo',
})
"
allow-clear
></a-input>
</a-form-item>
@ -311,7 +347,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.redirectUrl"
:max-length="250"
:placeholder="t('system.config.auth.OIDCCallbackUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<meteresphere-endpoint>/sso/callback/cas/suthld',
})
"
allow-clear
></a-input>
</a-form-item>
@ -365,7 +405,11 @@
<a-input
v-model:model-value="activeAuthForm.configuration.logoutUrl"
:max-length="250"
:placeholder="t('system.config.auth.logoutSessionUrlPlaceholder')"
:placeholder="
t('system.config.auth.commonUrlPlaceholder', {
url: 'http://<keyclock>/auth/realms/<metersphere>/protocol/openid-connect/logout',
})
"
allow-clear
></a-input>
</a-form-item>
@ -381,7 +425,7 @@
<a-input
v-model:model-value="activeAuthForm.configuration.loginUrl"
:max-length="250"
:placeholder="t('system.config.auth.loginUrlPlaceholder')"
:placeholder="t('system.config.auth.commonUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.loginUrlTip')" :show-fill-icon="false" />

View File

@ -1,57 +1,61 @@
<template>
<MsCard class="mb-[16px]" :loading="loading" simple auto-height>
<div class="mb-[16px] flex justify-between">
<div class="font-medium text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup') }}</div>
</div>
<a-radio-group v-model:model-value="activeType" type="button">
<a-radio value="log">{{ t('system.config.memoryCleanup.log') }}</a-radio>
<a-radio value="history">{{ t('system.config.memoryCleanup.history') }}</a-radio>
</a-radio-group>
<template v-if="activeType === 'log'">
<div class="mb-[8px] mt-[16px] flex items-center">
<div class="text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup.keepTime') }}</div>
<a-tooltip :content="t('system.config.memoryCleanup.keepTimeTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
<div>
<MsCard class="mb-[16px]" :loading="loading" simple auto-height>
<div class="mb-[16px] flex justify-between">
<div class="font-medium text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup') }}</div>
</div>
<a-input-number
v-model:model-value="timeCount"
class="w-[130px]"
:disabled="saveLoading || !userStore.isAdmin"
@blur="() => saveConfig()"
>
<template #append>
<a-select
v-model:model-value="activeTime"
:options="timeOptions"
class="select-input-append"
:loading="saveLoading"
@change="() => saveConfig()"
/>
</template>
</a-input-number>
</template>
<template v-else>
<div class="mb-[8px] mt-[16px] flex items-center">
<div class="text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup.saveCount') }}</div>
<a-tooltip :content="t('system.config.memoryCleanup.saveCountTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<a-input-number
v-model:model-value="historyCount"
class="w-[130px]"
:disabled="saveLoading"
@blur="() => saveConfig()"
/>
</template>
</MsCard>
<a-radio-group v-model:model-value="activeType" type="button">
<a-radio value="log">{{ t('system.config.memoryCleanup.log') }}</a-radio>
<a-radio value="history">{{ t('system.config.memoryCleanup.history') }}</a-radio>
</a-radio-group>
<template v-if="activeType === 'log'">
<div class="mb-[8px] mt-[16px] flex items-center">
<div class="text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup.keepTime') }}</div>
<a-tooltip :content="t('system.config.memoryCleanup.keepTimeTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<a-input-number
v-model:model-value="timeCount"
class="w-[130px]"
:disabled="saveLoading"
:min="0"
@blur="() => saveConfig()"
>
<template #append>
<a-select
v-model:model-value="activeTime"
:options="timeOptions"
class="select-input-append"
:loading="saveLoading"
@change="() => saveConfig()"
/>
</template>
</a-input-number>
</template>
<template v-else>
<div class="mb-[8px] mt-[16px] flex items-center">
<div class="text-[var(--color-text-000)]">{{ t('system.config.memoryCleanup.saveCount') }}</div>
<a-tooltip :content="t('system.config.memoryCleanup.saveCountTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
<a-input-number
v-model:model-value="historyCount"
class="w-[130px]"
:disabled="saveLoading"
:min="0"
@blur="() => saveConfig()"
/>
</template>
</MsCard>
</div>
</template>
<script setup lang="ts">
@ -88,20 +92,26 @@
const historyCount = ref(10);
onBeforeMount(async () => {
loading.value = true;
const res = await getCleanupConfig();
if (res.operationLog) {
const matches = res.operationLog.match(/(\d+)([MDY])$/);
if (matches) {
const [, number, letter] = matches;
timeCount.value = Number(number);
activeTime.value = letter;
try {
loading.value = true;
const res = await getCleanupConfig();
if (res.operationLog) {
const matches = res.operationLog.match(/(\d+)([MDY])$/);
if (matches) {
const [, number, letter] = matches;
timeCount.value = Number(number);
activeTime.value = letter;
}
}
if (res.operationHistory) {
historyCount.value = Number(res.operationHistory);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
if (res.operationHistory) {
historyCount.value = Number(res.operationHistory);
}
loading.value = false;
});
const saveLoading = ref(false);

View File

@ -230,7 +230,7 @@
<div :class="['config-preview', '!h-[290px]', currentLocale === 'en-US' ? '!h-[340px]' : '']">
<div ref="platformPageFullRef" class="login-preview">
<div
class="absolute right-[18px] top-[16px] z-[999] w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
class="absolute right-[12px] top-[8px] z-[999] w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="platformFullscreenToggle"
>
<MsIcon v-if="isPlatformPageFullscreen" type="icon-icon_off_screen" />
@ -341,7 +341,6 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -354,6 +353,7 @@
import loginForm from '@/views/login/components/login-form.vue';
import { savePageConfig } from '@/api/modules/setting/config';
import useFullScreen from '@/hooks/useFullScreen';
import { useI18n } from '@/hooks/useI18n';
import useLocale from '@/locale/useLocale';
import useAppStore from '@/store/modules/app';
@ -377,9 +377,10 @@
const pageConfig = ref({ ...appStore.pageConfig });
const loginPageFullRef = ref<HTMLElement | null>(null);
const platformPageFullRef = ref<HTMLElement | null>(null);
const { isFullscreen: isLoginPageFullscreen, toggle: loginFullscreenToggle } = useFullscreen(loginPageFullRef);
const { isFullscreen: isPlatformPageFullscreen, toggle: platformFullscreenToggle } =
useFullscreen(platformPageFullRef);
const { isFullScreen: isLoginPageFullscreen, toggleFullScreen: loginFullscreenToggle } =
useFullScreen(loginPageFullRef);
const { isFullScreen: isPlatformPageFullscreen, toggleFullScreen: platformFullscreenToggle } =
useFullScreen(platformPageFullRef);
const loginConfigFormRef = ref<FormInstance>();
const platformConfigFormRef = ref<FormInstance>();

View File

@ -130,34 +130,21 @@ export default {
'system.config.auth.addResource': 'Add resource',
'system.config.auth.serviceUrl': 'Server address',
'system.config.auth.serviceUrlRequired': 'Server address cannot be empty',
'system.config.auth.serviceUrlPlaceholder': 'eg: http://<casurl>',
'system.config.auth.commonUrlPlaceholder': 'eg: {url}',
'system.config.auth.loginUrl': 'Login address',
'system.config.auth.loginUrlRequired': 'Login address cannot be empty',
'system.config.auth.loginUrlPlaceholder': 'eg: http://<casurl>/login',
'system.config.auth.loginUrlTip': 'When authentication fails, redirect to this login page',
'system.config.auth.verifyUrl': 'Verify address',
'system.config.auth.verifyUrlRequired': 'Verification address cannot be empty',
'system.config.auth.verifyUrlPlaceholder': 'eg: http://<casurl>/serviceValidate',
'system.config.auth.verifyUrlTip': 'The information used to verify the login is correct',
'system.config.auth.callbackUrl': 'Callback address',
'system.config.auth.callbackUrlRequired': 'Callback address cannot be empty',
// eslint-disable-next-line no-template-curly-in-string
'system.config.auth.callbackUrlPlaceholder': 'eg: http://<meteresphere-endpoint>/sso/callback/cas/suthld',
'system.config.auth.OIDCCallbackUrlPlaceholder':
// eslint-disable-next-line no-template-curly-in-string
'eg: http://<metersphere-endpoint>/sso/callback or http://<metersphere-endpoint>/sso/callback/authld',
'system.config.auth.authUrl': 'Authorized end address',
'system.config.auth.authUrlRequired': 'Authorization end address cannot be empty',
'system.config.auth.authUrlPlaceholder':
'eg: http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/auth',
'system.config.auth.tokenUrl': 'Token endpoint address',
'system.config.auth.tokenUrlRequired': 'Token endpoint address cannot be empty',
'system.config.auth.tokenUrlPlaceholder':
'eg: http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/token',
'system.config.auth.userInfoUrl': 'User information endpoint address',
'system.config.auth.userInfoUrlRequired': 'User information endpoint address cannot be empty',
'system.config.auth.userInfoUrlPlaceholder':
'eg: http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/userinfo',
'system.config.auth.clientId': 'Client ID',
'system.config.auth.clientIdRequired': 'Client ID cannot be empty',
'system.config.auth.clientIdPlaceholder': 'eg: metersphere',
@ -166,8 +153,6 @@ export default {
'system.config.auth.clientSecretPlaceholder': 'OIDC client secret',
'system.config.auth.logoutSessionUrl': 'Logout session address',
'system.config.auth.logoutSessionUrlRequired': 'Logout session address cannot be empty',
'system.config.auth.logoutSessionUrlPlaceholder':
'eg: http://<keyclock>/auth/realms/<metersphere>/protocol/openid-connect/logout',
'system.config.auth.password': 'Password',
'system.config.auth.passwordRequired': 'Password cannot be empty',
'system.config.auth.passwordPlaceholder': 'OIDC client secret',

View File

@ -126,34 +126,21 @@ export default {
'system.config.auth.addResource': '添加资源',
'system.config.auth.serviceUrl': '服务端地址',
'system.config.auth.serviceUrlRequired': '服务端地址不能为空',
'system.config.auth.serviceUrlPlaceholder': '例如http://<casurl>',
'system.config.auth.commonUrlPlaceholder': '例如:{url}',
'system.config.auth.loginUrl': '登录地址',
'system.config.auth.loginUrlRequired': '登录地址不能为空',
'system.config.auth.loginUrlPlaceholder': '例如http://<casurl>/login',
'system.config.auth.loginUrlTip': '当身份认证失败,会重新定向到该登录页',
'system.config.auth.verifyUrl': '验证地址',
'system.config.auth.verifyUrlRequired': '验证地址不能为空',
'system.config.auth.verifyUrlPlaceholder': '例如http://<casurl>/serviceValidate',
'system.config.auth.verifyUrlTip': '用于验证登录的信息是否正确',
'system.config.auth.callbackUrl': '回调地址',
'system.config.auth.callbackUrlRequired': '回调地址不能为空',
// eslint-disable-next-line no-template-curly-in-string
'system.config.auth.callbackUrlPlaceholder': '例如http://<meteresphere-endpoint>/sso/callback/cas/suthld',
'system.config.auth.OIDCCallbackUrlPlaceholder':
// eslint-disable-next-line no-template-curly-in-string
'例如http://<metersphere-endpoint>/sso/callback or http://<metersphere-endpoint>/sso/callback/authld',
'system.config.auth.authUrl': '授权端地址',
'system.config.auth.authUrlRequired': '授权端地址不能为空',
'system.config.auth.authUrlPlaceholder':
'例如http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/auth',
'system.config.auth.tokenUrl': 'Token 端点地址',
'system.config.auth.tokenUrlRequired': 'Token 端点地址不能为空',
'system.config.auth.tokenUrlPlaceholder':
'例如http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/token',
'system.config.auth.userInfoUrl': '用户信息端点地址',
'system.config.auth.userInfoUrlRequired': '用户信息端点地址不能为空',
'system.config.auth.userInfoUrlPlaceholder':
'例如http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/userinfo',
'system.config.auth.clientId': '客户端 ID',
'system.config.auth.clientIdRequired': '客户端 ID不能为空',
'system.config.auth.clientIdPlaceholder': '例如metersphere',
@ -162,8 +149,6 @@ export default {
'system.config.auth.clientSecretPlaceholder': 'OIDC client secret',
'system.config.auth.logoutSessionUrl': '注销会话端地址',
'system.config.auth.logoutSessionUrlRequired': '注销会话端地址不能为空',
'system.config.auth.logoutSessionUrlPlaceholder':
'例如http://<keyclock>/auth/realms/<metersphere>/protocol/openid-connect/logout',
'system.config.auth.password': '密码',
'system.config.auth.passwordRequired': '密码不能为空',
'system.config.auth.passwordPlaceholder': 'OIDC client secret',

View File

@ -457,6 +457,7 @@
width: 180,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
},
];