feat: 缺陷管理评论组件
This commit is contained in:
parent
3a42e95796
commit
8b54ddab04
|
@ -60,6 +60,7 @@
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-dompurify-html": "^4.1.4",
|
||||||
"vue-draggable-plus": "^0.2.7",
|
"vue-draggable-plus": "^0.2.7",
|
||||||
"vue-echarts": "^6.6.1",
|
"vue-echarts": "^6.6.1",
|
||||||
"vue-i18n": "^9.3.0",
|
"vue-i18n": "^9.3.0",
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="h-[40px] w-[40px] gap-[8px] rounded-full">
|
||||||
|
<img
|
||||||
|
src="https://p6-passport.byteacctimg.com/img/user-avatar/9a6e39ea689600e70175649a8cd14913~200x200.awebp"
|
||||||
|
alt="User avatar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ props.element.createUser }}</div>
|
||||||
|
<div v-dompurify-html="props.element.content" class="mt-[4px]"></div>
|
||||||
|
<div class="mt-[16px] flex flex-row items-center">
|
||||||
|
<div class="text-[var(--color-text-4)]">{{
|
||||||
|
dayjs(props.element.updateTime).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}}</div>
|
||||||
|
<div class="ml-[24px] flex flex-row gap-[16px]">
|
||||||
|
<div class="comment-btn" @click="expendChange">
|
||||||
|
<MsIconfont type="icon-icon_comment_outlined" />
|
||||||
|
<span>{{ !expendComment ? t('comment.expendComment') : t('comment.collapseComment') }}</span>
|
||||||
|
<span class="text-[var(--color-text-4)]">({{ element.children?.length }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="comment-btn" @click="replyClick">
|
||||||
|
<MsIconfont type="icon-icon_reply" />
|
||||||
|
<span>{{ t('comment.reply') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="comment-btn" @click="editClick">
|
||||||
|
<MsIconfont type="icon-icon_edit_outlined" />
|
||||||
|
<span>{{ t('comment.edit') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="comment-btn" @click="deleteClick">
|
||||||
|
<MsIconfont type="icon-icon_delete-trash_outlined" />
|
||||||
|
<span>{{ t('comment.delete') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import MsIconfont from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
export interface CommentItem {
|
||||||
|
id: string; // 评论id
|
||||||
|
bugId: string; // bug id
|
||||||
|
createUser: string; // 创建人
|
||||||
|
updateTime: number; // 更新时间
|
||||||
|
content: string;
|
||||||
|
replyUser?: string; // 回复人
|
||||||
|
notifier?: string; // 通知人
|
||||||
|
children?: CommentItem[];
|
||||||
|
}
|
||||||
|
// 仅评论: ’COMMENT‘; 评论并@: ’AT‘; 回复评论/回复并@: ’REPLAY‘;)
|
||||||
|
export type commentEvent = 'COMMENT' | 'AT' | 'REPLAY';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
element: CommentItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'reply'): void;
|
||||||
|
(event: 'edit'): void;
|
||||||
|
(event: 'delete'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const expendComment = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
|
||||||
|
const expendChange = () => {
|
||||||
|
expendComment.value = !expendComment.value;
|
||||||
|
};
|
||||||
|
const replyClick = () => {
|
||||||
|
emit('reply');
|
||||||
|
};
|
||||||
|
|
||||||
|
const editClick = () => {
|
||||||
|
isEdit.value = true;
|
||||||
|
emit('edit');
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteClick = () => {
|
||||||
|
emit('delete');
|
||||||
|
};
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.comment-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
:hover {
|
||||||
|
background-color: var(--color-bg-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
'comment.expendComment': 'Expand comment',
|
||||||
|
'comment.collapseComment': 'Collapse comment',
|
||||||
|
'comment.reply': 'Reply',
|
||||||
|
'comment.delete': 'Delete',
|
||||||
|
'comment.edit': 'Edit',
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
'comment.expendComment': '展开评论',
|
||||||
|
'comment.collapseComment': '收起评论',
|
||||||
|
'comment.edit': '编辑',
|
||||||
|
'comment.reply': '回复',
|
||||||
|
'comment.delete': '删除',
|
||||||
|
};
|
|
@ -132,6 +132,7 @@
|
||||||
const uiTestSpan = ref(1);
|
const uiTestSpan = ref(1);
|
||||||
const apiTestSpan = ref(1);
|
const apiTestSpan = ref(1);
|
||||||
const loadTestSpan = ref(1);
|
const loadTestSpan = ref(1);
|
||||||
|
const personalSpan = ref(1);
|
||||||
|
|
||||||
// 表格的总全选
|
// 表格的总全选
|
||||||
const allChecked = ref(false);
|
const allChecked = ref(false);
|
||||||
|
@ -203,6 +204,11 @@
|
||||||
rowspan: loadTestSpan.value,
|
rowspan: loadTestSpan.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (record.isPersonal) {
|
||||||
|
return {
|
||||||
|
rowspan: personalSpan.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,7 +241,7 @@
|
||||||
permissions: child?.permissions,
|
permissions: child?.permissions,
|
||||||
indeterminate,
|
indeterminate,
|
||||||
perChecked,
|
perChecked,
|
||||||
ability: index === 0 ? t(`system.userGroup.${type}`) : undefined,
|
ability: index === 0 ? item.name : undefined,
|
||||||
operationObject: t(child.name),
|
operationObject: t(child.name),
|
||||||
isSystem: index === 0 && type === 'SYSTEM',
|
isSystem: index === 0 && type === 'SYSTEM',
|
||||||
isOrganization: index === 0 && type === 'ORGANIZATION',
|
isOrganization: index === 0 && type === 'ORGANIZATION',
|
||||||
|
@ -247,6 +253,7 @@
|
||||||
isUiTest: index === 0 && type === 'UI_TEST',
|
isUiTest: index === 0 && type === 'UI_TEST',
|
||||||
isLoadTest: index === 0 && type === 'LOAD_TEST',
|
isLoadTest: index === 0 && type === 'LOAD_TEST',
|
||||||
isApiTest: index === 0 && type === 'API_TEST',
|
isApiTest: index === 0 && type === 'API_TEST',
|
||||||
|
isPersonal: index === 0 && type === 'PERSONAL',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
@ -255,26 +262,28 @@
|
||||||
const transformData = (data: UserGroupAuthSetting[]) => {
|
const transformData = (data: UserGroupAuthSetting[]) => {
|
||||||
const result: AuthTableItem[] = [];
|
const result: AuthTableItem[] = [];
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
if (item.type === 'SYSTEM') {
|
if (item.id === 'SYSTEM') {
|
||||||
systemSpan.value = item.children?.length || 0;
|
systemSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'PROJECT') {
|
} else if (item.id === 'PROJECT') {
|
||||||
projectSpan.value = item.children?.length || 0;
|
projectSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'ORGANIZATION') {
|
} else if (item.id === 'ORGANIZATION') {
|
||||||
organizationSpan.value = item.children?.length || 0;
|
organizationSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'WORKSTATION') {
|
} else if (item.id === 'WORKSTATION') {
|
||||||
workstationSpan.value = item.children?.length || 0;
|
workstationSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'TEST_PLAN') {
|
} else if (item.id === 'TEST_PLAN') {
|
||||||
testPlanSpan.value = item.children?.length || 0;
|
testPlanSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'BUG_MANAGEMENT') {
|
} else if (item.id === 'BUG_MANAGEMENT') {
|
||||||
bugManagementSpan.value = item.children?.length || 0;
|
bugManagementSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'CASE_MANAGEMENT') {
|
} else if (item.id === 'CASE_MANAGEMENT') {
|
||||||
caseManagementSpan.value = item.children?.length || 0;
|
caseManagementSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'UI_TEST') {
|
} else if (item.id === 'UI_TEST') {
|
||||||
uiTestSpan.value = item.children?.length || 0;
|
uiTestSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'API_TEST') {
|
} else if (item.id === 'API_TEST') {
|
||||||
apiTestSpan.value = item.children?.length || 0;
|
apiTestSpan.value = item.children?.length || 0;
|
||||||
} else if (item.type === 'LOAD_TEST') {
|
} else if (item.id === 'LOAD_TEST') {
|
||||||
loadTestSpan.value = item.children?.length || 0;
|
loadTestSpan.value = item.children?.length || 0;
|
||||||
|
} else if (item.id === 'PERSONAL') {
|
||||||
|
personalSpan.value = item.children?.length || 0;
|
||||||
}
|
}
|
||||||
result.push(...makeData(item, item.id));
|
result.push(...makeData(item, item.id));
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:value="item.key"
|
:value="item.key"
|
||||||
class="mt-[8px] w-[95px] pl-[0px]"
|
class="mt-[8px] w-[95px] pl-[0px]"
|
||||||
|
:disabled="item.key === 'name'"
|
||||||
>
|
>
|
||||||
<a-tooltip :content="item.text">
|
<a-tooltip :content="item.text">
|
||||||
<span class="one-line-text">{{ item.text }}</span>
|
<span class="one-line-text">{{ item.text }}</span>
|
||||||
|
@ -77,10 +78,10 @@
|
||||||
</a-checkbox-group>
|
</a-checkbox-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[270px]">
|
<div>
|
||||||
<div class="optional-header">
|
<div class="optional-header min-w-[270px]">
|
||||||
<div class="font-medium">{{ t('system.orgTemplate.selectedField') }}</div>
|
<div class="font-medium">{{ t('system.orgTemplate.selectedField') }}</div>
|
||||||
<MsButton @click="clearHandler">{{ t('system.orgTemplate.clear') }}</MsButton>
|
<MsButton @click="handleReset">{{ t('system.orgTemplate.clear') }}</MsButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-[16px]">
|
<div class="p-[16px]">
|
||||||
<VueDraggable v-model="selectedList" ghost-class="ghost">
|
<VueDraggable v-model="selectedList" ghost-class="ghost">
|
||||||
|
@ -94,6 +95,7 @@
|
||||||
<div class="one-line-text ml-2 w-[178px]">{{ element.text }}</div>
|
<div class="one-line-text ml-2 w-[178px]">{{ element.text }}</div>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<icon-close
|
<icon-close
|
||||||
|
v-if="element.key !== 'name'"
|
||||||
:style="{ 'font-size': '14px' }"
|
:style="{ 'font-size': '14px' }"
|
||||||
class="cursor-pointer text-[var(--color-text-3)]"
|
class="cursor-pointer text-[var(--color-text-3)]"
|
||||||
@click="removeSelectedField(element.key)"
|
@click="removeSelectedField(element.key)"
|
||||||
|
@ -132,9 +134,14 @@
|
||||||
interface MsExportDrawerProps {
|
interface MsExportDrawerProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
allData: MsExportDrawerMap;
|
allData: MsExportDrawerMap;
|
||||||
|
// 默认选中的字段 keys
|
||||||
|
defaultSelectedKeys?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<MsExportDrawerProps>();
|
const props = withDefaults(defineProps<MsExportDrawerProps>(), {
|
||||||
|
visible: false,
|
||||||
|
defaultSelectedKeys: () => ['name', 'id', 'title', 'status', 'handle_user', 'content'],
|
||||||
|
});
|
||||||
|
|
||||||
const selectedList = ref<MsExportDrawerOption[]>([]); // 已选字段
|
const selectedList = ref<MsExportDrawerOption[]>([]); // 已选字段
|
||||||
const selectedIds = ref<string[]>([]); // 已选字段id
|
const selectedIds = ref<string[]>([]); // 已选字段id
|
||||||
|
@ -199,13 +206,17 @@
|
||||||
return [...systemList.value, ...customList.value, ...otherList.value];
|
return [...systemList.value, ...customList.value, ...otherList.value];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
selectedList.value = allList.value.filter((item) => props.defaultSelectedKeys.includes(item.key));
|
||||||
|
};
|
||||||
|
|
||||||
const handleDrawerConfirm = () => {
|
const handleDrawerConfirm = () => {
|
||||||
emit('confirm', selectedList.value);
|
emit('confirm', selectedList.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrawerCancel = () => {
|
const handleDrawerCancel = () => {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
selectedList.value = [];
|
handleReset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCheckedAll = computed(() => {
|
const isCheckedAll = computed(() => {
|
||||||
|
@ -216,10 +227,6 @@
|
||||||
return selectedList.value.length > 0 && selectedList.value.length < allList.value.length;
|
return selectedList.value.length > 0 && selectedList.value.length < allList.value.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearHandler = () => {
|
|
||||||
selectedList.value = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeAll = (value: boolean | (string | number | boolean)[]) => {
|
const handleChangeAll = (value: boolean | (string | number | boolean)[]) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
selectedList.value = allList.value;
|
selectedList.value = allList.value;
|
||||||
|
@ -239,6 +246,10 @@
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
selectedIds.value = selectedList.value.map((item) => item.key);
|
selectedIds.value = selectedList.value.map((item) => item.key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
handleReset();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
|
@ -16,6 +16,7 @@ import store from './store';
|
||||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
||||||
import '@/assets/style/global.less';
|
import '@/assets/style/global.less';
|
||||||
import localforage from 'localforage';
|
import localforage from 'localforage';
|
||||||
|
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
@ -26,6 +27,7 @@ async function bootstrap() {
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(ArcoVue);
|
app.use(ArcoVue);
|
||||||
app.use(ArcoVueIcon);
|
app.use(ArcoVueIcon);
|
||||||
|
app.use(VueDOMPurifyHTML);
|
||||||
app.component('SvgIcon', SvgIcon);
|
app.component('SvgIcon', SvgIcon);
|
||||||
app.component('MsIcon', MsIcon);
|
app.component('MsIcon', MsIcon);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@ export type AuthScopeType =
|
||||||
| 'CASE_MANAGEMENT'
|
| 'CASE_MANAGEMENT'
|
||||||
| 'API_TEST'
|
| 'API_TEST'
|
||||||
| 'UI_TEST'
|
| 'UI_TEST'
|
||||||
| 'LOAD_TEST';
|
| 'LOAD_TEST'
|
||||||
|
| 'PERSONAL';
|
||||||
|
|
||||||
export interface UserGroupItem {
|
export interface UserGroupItem {
|
||||||
// 组ID
|
// 组ID
|
||||||
|
@ -121,7 +122,7 @@ export interface AuthTableItem {
|
||||||
isApiTest?: boolean;
|
isApiTest?: boolean;
|
||||||
isUiTest?: boolean;
|
isUiTest?: boolean;
|
||||||
isLoadTest?: boolean;
|
isLoadTest?: boolean;
|
||||||
|
isPersonal?: boolean;
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
}
|
}
|
||||||
export interface SavePermissions {
|
export interface SavePermissions {
|
||||||
|
|
|
@ -0,0 +1,417 @@
|
||||||
|
<template>
|
||||||
|
<MsDetailDrawer
|
||||||
|
ref="detailDrawerRef"
|
||||||
|
v-model:visible="showDrawerVisible"
|
||||||
|
:width="1200"
|
||||||
|
:footer="false"
|
||||||
|
:title="t('caseManagement.featureCase.caseDetailTitle', { id: detailInfo?.id, name: detailInfo?.name })"
|
||||||
|
:detail-id="props.detailId"
|
||||||
|
:detail-index="props.detailIndex"
|
||||||
|
:get-detail-func="getCaseDetail"
|
||||||
|
:pagination="props.pagination"
|
||||||
|
:table-data="props.tableData"
|
||||||
|
:page-change="props.pageChange"
|
||||||
|
@loaded="loadedCase"
|
||||||
|
>
|
||||||
|
<template #titleRight="{ loading }">
|
||||||
|
<div class="rightButtons flex items-center">
|
||||||
|
<MsButton
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="mr-4 !rounded-[var(--border-radius-small)]"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="editLoading"
|
||||||
|
@click="updateHandler('edit')"
|
||||||
|
>
|
||||||
|
<MsIcon type="icon-icon_edit_outlined" class="mr-1 font-[16px]" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</MsButton>
|
||||||
|
<MsButton
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="mr-4 !rounded-[var(--border-radius-small)]"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="shareLoading"
|
||||||
|
@click="shareHandler"
|
||||||
|
>
|
||||||
|
<MsIcon type="icon-icon_share1" class="mr-1 font-[16px]" />
|
||||||
|
{{ t('caseManagement.featureCase.share') }}
|
||||||
|
</MsButton>
|
||||||
|
<MsButton
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="mr-4 !rounded-[var(--border-radius-small)]"
|
||||||
|
:disabled="loading"
|
||||||
|
:loading="followLoading"
|
||||||
|
@click="followHandler"
|
||||||
|
>
|
||||||
|
<MsIcon
|
||||||
|
:type="detailInfo.followFlag ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||||
|
class="mr-1 font-[16px]"
|
||||||
|
:class="[detailInfo.followFlag ? 'text-[rgb(var(--warning-6))]' : '']"
|
||||||
|
/>
|
||||||
|
{{ t('caseManagement.featureCase.follow') }}
|
||||||
|
</MsButton>
|
||||||
|
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]">
|
||||||
|
<a-dropdown position="br" :hide-on-select="false">
|
||||||
|
<div>
|
||||||
|
<icon-more class="mr-1" />
|
||||||
|
<span> {{ t('caseManagement.featureCase.more') }}</span>
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<a-doption class="error-6 text-[rgb(var(--danger-6))]" @click="deleteHandler()">
|
||||||
|
<MsIcon type="icon-icon_delete-trash_outlined" class="font-[16px] text-[rgb(var(--danger-6))]" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</a-doption>
|
||||||
|
</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" />
|
||||||
|
{{ t('caseManagement.featureCase.fullScreen') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div ref="wrapperRef" class="h-full bg-white">
|
||||||
|
<MsSplitBox ref="wrapperRef" expand-direction="right" :max="0.7" :min="0.7" :size="900">
|
||||||
|
<template #left>
|
||||||
|
<div class="leftWrapper h-full">
|
||||||
|
<div class="header h-[50px]">
|
||||||
|
<a-tabs>
|
||||||
|
<a-tab-pane key="detail">
|
||||||
|
<BugDetailTab :detail-info="detailInfo" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="case">
|
||||||
|
<BugCaseTab :detail-info="detailInfo" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="comment">
|
||||||
|
<BugCommentTab :detail-info="detailInfo" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="history">
|
||||||
|
<BugHistoryTab :detail-info="detailInfo" />
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<div class="rightWrapper p-[24px]">
|
||||||
|
<div class="mb-4 font-medium">{{ t('caseManagement.featureCase.basicInfo') }}</div>
|
||||||
|
<div class="baseItem">
|
||||||
|
<span class="label"> {{ t('caseManagement.featureCase.tableColumnModule') }}</span>
|
||||||
|
<span>{{ moduleName }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 自定义字段开始 -->
|
||||||
|
<MsFormCreate
|
||||||
|
v-if="formRules.length"
|
||||||
|
ref="formCreateRef"
|
||||||
|
class="w-full"
|
||||||
|
:option="options"
|
||||||
|
:form-rule="formRules"
|
||||||
|
:form-create-key="FormCreateKeyEnum.CASE_CUSTOM_ATTRS_DETAIL"
|
||||||
|
/>
|
||||||
|
<!-- 自定义字段结束 -->
|
||||||
|
<div class="baseItem">
|
||||||
|
<span class="label"> {{ t('caseManagement.featureCase.tableColumnCreateUser') }}</span>
|
||||||
|
<span>{{ detailInfo?.createUser }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="baseItem">
|
||||||
|
<span class="label"> {{ t('caseManagement.featureCase.tableColumnCreateTime') }}</span>
|
||||||
|
<span>{{ dayjs(detailInfo?.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsSplitBox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsDetailDrawer>
|
||||||
|
<SettingDrawer v-model:visible="showSettingDrawer" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
|
||||||
|
import type { FormItem } from '@/components/pure/ms-form-create/types';
|
||||||
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
|
import type { MsPaginationI } from '@/components/pure/ms-table/type';
|
||||||
|
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
|
||||||
|
import BugDetailTab from './bugDetailTab.vue';
|
||||||
|
|
||||||
|
import { deleteCaseRequest, followerCaseRequest, getCaseDetail } from '@/api/modules/case-management/featureCase';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import useFeatureCaseStore from '@/store/modules/case/featureCase';
|
||||||
|
import useUserStore from '@/store/modules/user';
|
||||||
|
import { characterLimit, findNodeByKey } from '@/utils';
|
||||||
|
|
||||||
|
import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
|
||||||
|
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
|
||||||
|
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
|
||||||
|
const wrapperRef = ref();
|
||||||
|
const { isFullscreen, toggle } = useFullscreen(wrapperRef);
|
||||||
|
const featureCaseStore = useFeatureCaseStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
detailId: string; // 详情 id
|
||||||
|
detailIndex: number; // 详情 下标
|
||||||
|
tableData: any[]; // 表格数据
|
||||||
|
pagination: MsPaginationI; // 分页器对象
|
||||||
|
pageChange: (page: number) => Promise<void>; // 分页变更函数
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible']);
|
||||||
|
|
||||||
|
const userId = computed(() => userStore.userInfo.id);
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const currentProjectId = computed(() => appStore.currentProjectId);
|
||||||
|
|
||||||
|
const showDrawerVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
const showSettingDrawer = ref<boolean>(false);
|
||||||
|
function showMenuSetting() {
|
||||||
|
showSettingDrawer.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabSettingList = computed(() => {
|
||||||
|
return featureCaseStore.tabSettingList;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabSetting = ref<TabItemType[]>([...tabSettingList.value]);
|
||||||
|
const activeTab = ref<string | number>('detail');
|
||||||
|
function changeTabs(key: string | number) {
|
||||||
|
activeTab.value = key;
|
||||||
|
switch (activeTab.value) {
|
||||||
|
case 'setting':
|
||||||
|
showMenuSetting();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailInfo = ref<Record<string, any>>({});
|
||||||
|
const customFields = ref<CustomAttributes[]>([]);
|
||||||
|
|
||||||
|
function loadedCase(detail: CaseManagementTable) {
|
||||||
|
detailInfo.value = { ...detail };
|
||||||
|
customFields.value = detailInfo.value.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleName = computed(() => {
|
||||||
|
return findNodeByKey<Record<string, any>>(featureCaseStore.caseTree, detailInfo.value?.moduleId as string, 'id')
|
||||||
|
?.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const editLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
function updateSuccess() {
|
||||||
|
detailDrawerRef.value?.initDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHandler(type: string) {
|
||||||
|
router.push({
|
||||||
|
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
|
||||||
|
query: {
|
||||||
|
id: detailInfo.value.id,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
mode: type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
function shareHandler() {}
|
||||||
|
|
||||||
|
const followLoading = ref<boolean>(false);
|
||||||
|
// 关注
|
||||||
|
async function followHandler() {
|
||||||
|
followLoading.value = true;
|
||||||
|
try {
|
||||||
|
await followerCaseRequest({ userId: userId.value as string, functionalCaseId: detailInfo.value.id });
|
||||||
|
updateSuccess();
|
||||||
|
Message.success(
|
||||||
|
detailInfo.value.followFlag
|
||||||
|
? t('caseManagement.featureCase.cancelFollowSuccess')
|
||||||
|
: t('caseManagement.featureCase.followSuccess')
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
followLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用例
|
||||||
|
function deleteHandler() {
|
||||||
|
const { id, name } = detailInfo.value;
|
||||||
|
openModal({
|
||||||
|
type: 'error',
|
||||||
|
title: t('caseManagement.featureCase.deleteCaseTitle', { name: characterLimit(name) }),
|
||||||
|
content: t('caseManagement.featureCase.beforeDeleteCase'),
|
||||||
|
okText: t('common.confirmDelete'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
id,
|
||||||
|
deleteAll: false,
|
||||||
|
projectId: currentProjectId.value,
|
||||||
|
};
|
||||||
|
await deleteCaseRequest(params);
|
||||||
|
Message.success(t('common.deleteSuccess'));
|
||||||
|
updateSuccess();
|
||||||
|
detailDrawerRef.value?.openPrevDetail();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const formRules = ref<FormItem[]>([]);
|
||||||
|
|
||||||
|
const isDisabled = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 表单配置项
|
||||||
|
const options = {
|
||||||
|
resetBtn: false, // 不展示默认配置的重置和提交
|
||||||
|
submitBtn: false,
|
||||||
|
on: false, // 取消绑定on事件
|
||||||
|
form: {
|
||||||
|
layout: 'horizontal',
|
||||||
|
labelAlign: 'left',
|
||||||
|
labelColProps: {
|
||||||
|
span: 9,
|
||||||
|
},
|
||||||
|
wrapperColProps: {
|
||||||
|
span: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 暂时默认
|
||||||
|
row: {
|
||||||
|
gutter: 0,
|
||||||
|
},
|
||||||
|
wrap: {
|
||||||
|
'asterisk-position': 'end',
|
||||||
|
'validate-trigger': ['change'],
|
||||||
|
'hide-asterisk': true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化表单
|
||||||
|
function initForm() {
|
||||||
|
formRules.value = customFields.value.map((item: any) => {
|
||||||
|
return {
|
||||||
|
type: item.type,
|
||||||
|
name: item.fieldId,
|
||||||
|
label: item.fieldName,
|
||||||
|
value: JSON.parse(item.defaultValue),
|
||||||
|
required: item.required,
|
||||||
|
options: item.options || [],
|
||||||
|
props: {
|
||||||
|
modelValue: JSON.parse(item.defaultValue),
|
||||||
|
disabled: isDisabled.value,
|
||||||
|
options: item.options || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) as FormItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => customFields.value,
|
||||||
|
() => {
|
||||||
|
initForm();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
showDrawerVisible.value = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => showDrawerVisible.value,
|
||||||
|
(val) => {
|
||||||
|
emit('update:visible', val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tabSettingList.value,
|
||||||
|
() => {
|
||||||
|
tabSetting.value = featureCaseStore.getTab();
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.leftWrapper {
|
||||||
|
.header {
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid var(--color-text-n8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rightWrapper {
|
||||||
|
.baseItem {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
@apply flex;
|
||||||
|
.label {
|
||||||
|
width: 38%;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.arco-form-item-layout-horizontal) {
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
}
|
||||||
|
:deep(.arco-form-item-label-col > .arco-form-item-label) {
|
||||||
|
color: var(--color-text-3) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rightButtons {
|
||||||
|
:deep(.ms-button--secondary):hover,
|
||||||
|
:hover > .arco-icon {
|
||||||
|
color: rgb(var(--primary-5)) !important;
|
||||||
|
background: var(--color-bg-3);
|
||||||
|
.arco-icon:hover {
|
||||||
|
color: rgb(var(--primary-5)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error-6 {
|
||||||
|
color: rgb(var(--danger-6));
|
||||||
|
&:hover {
|
||||||
|
color: rgb(var(--danger-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.active .arco-badge-number) {
|
||||||
|
background: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,186 @@
|
||||||
|
<template>
|
||||||
|
<MsDrawer
|
||||||
|
:width="680"
|
||||||
|
:visible="currentVisible"
|
||||||
|
unmount-on-close
|
||||||
|
:footer="false"
|
||||||
|
:title="t('system.organization.addMember')"
|
||||||
|
:mask="false"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<a-dropdown trigger="hover">
|
||||||
|
<template #content>
|
||||||
|
<a-doption @click="showRelatedDrawer('api')">{{ t('bugManagement.detail.apiCase') }}</a-doption>
|
||||||
|
<a-doption @click="showRelatedDrawer('scenario')">{{ t('bugManagement.detail.scenarioCase') }}</a-doption>
|
||||||
|
<a-doption @click="showRelatedDrawer('ui')">{{ t('bugManagement.detail.uiCase') }}</a-doption>
|
||||||
|
<a-doption @click="showRelatedDrawer('performance')">{{
|
||||||
|
t('bugManagement.detail.performanceCase')
|
||||||
|
}}</a-doption>
|
||||||
|
</template>
|
||||||
|
<a-button type="primary">{{ t('bugManagement.edit.linkCase') }}</a-button>
|
||||||
|
</a-dropdown>
|
||||||
|
<a-input-search
|
||||||
|
v-model:model-value="keyword"
|
||||||
|
allow-clear
|
||||||
|
:placeholder="t('bugManagement.detail.searchCase')"
|
||||||
|
class="w-[230px]"
|
||||||
|
@search="searchUser"
|
||||||
|
@press-enter="searchUser"
|
||||||
|
></a-input-search>
|
||||||
|
</div>
|
||||||
|
<ms-base-table class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
|
||||||
|
<template #name="{ record }">
|
||||||
|
<span>{{ record.name }}</span>
|
||||||
|
<span v-if="record.adminFlag" class="ml-[4px] text-[var(--color-text-4)]">{{
|
||||||
|
`(${t('common.admin')})`
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
<template #operation="{ record }">
|
||||||
|
<MsRemoveButton
|
||||||
|
:title="t('system.organization.removeName', { name: record.name })"
|
||||||
|
:sub-title-tip="t('system.organization.removeTip')"
|
||||||
|
@ok="handleRemove(record)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ms-base-table>
|
||||||
|
</div>
|
||||||
|
<MsDrawer
|
||||||
|
:width="680"
|
||||||
|
:visible="relatedVisible"
|
||||||
|
unmount-on-close
|
||||||
|
:footer="false"
|
||||||
|
:mask="false"
|
||||||
|
@cancel="relatedVisible = false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex flex-row items-center gap-[4px]">
|
||||||
|
<div>{{ t('bugManagement.detail.relatedCase') }}</div>
|
||||||
|
<a-select>
|
||||||
|
<a-option></a-option>
|
||||||
|
<a-option></a-option>
|
||||||
|
<a-option></a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsDrawer>
|
||||||
|
</MsDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { Message, TableData } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
|
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
import MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
|
||||||
|
|
||||||
|
import { deleteProjectMemberByOrg, postProjectMemberByProjectId } from '@/api/modules/setting/organizationAndProject';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
export interface projectDrawerProps {
|
||||||
|
visible: boolean;
|
||||||
|
organizationId?: string;
|
||||||
|
projectId?: string;
|
||||||
|
}
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps<projectDrawerProps>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'cancel'): void;
|
||||||
|
(e: 'requestFetchData'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const relatedVisible = ref(false);
|
||||||
|
const relatedType = ref('api');
|
||||||
|
|
||||||
|
const showRelatedDrawer = (type: string) => {
|
||||||
|
relatedType.value = type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentVisible = ref(props.visible);
|
||||||
|
|
||||||
|
const keyword = ref('');
|
||||||
|
|
||||||
|
const projectColumn: MsTableColumn = [
|
||||||
|
{
|
||||||
|
title: 'system.organization.userName',
|
||||||
|
slotName: 'name',
|
||||||
|
dataIndex: 'name',
|
||||||
|
showTooltip: true,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'system.organization.email',
|
||||||
|
dataIndex: 'email',
|
||||||
|
showTooltip: true,
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'system.organization.phone',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
},
|
||||||
|
{ title: 'system.organization.operation', slotName: 'operation' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(postProjectMemberByProjectId, {
|
||||||
|
heightUsed: 240,
|
||||||
|
columns: projectColumn,
|
||||||
|
scroll: { x: '100%' },
|
||||||
|
selectable: false,
|
||||||
|
noDisable: false,
|
||||||
|
pageSimple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function searchUser() {
|
||||||
|
setKeyword(keyword.value);
|
||||||
|
await loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
keyword.value = '';
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
await loadList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async (record: TableData) => {
|
||||||
|
try {
|
||||||
|
if (props.projectId) {
|
||||||
|
await deleteProjectMemberByOrg(props.projectId, record.id);
|
||||||
|
}
|
||||||
|
Message.success(t('common.removeSuccess'));
|
||||||
|
fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(
|
||||||
|
() => props.projectId,
|
||||||
|
() => {
|
||||||
|
setLoadListParams({ projectId: props.projectId });
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
currentVisible.value = visible;
|
||||||
|
if (visible) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.custom-height) {
|
||||||
|
height: 100vh !important;
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div class="bug-comment">
|
||||||
|
<div class="header-icon">
|
||||||
|
<img :src="comment.user.avatar" alt="User avatar" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h3>{{ comment.user.name }}</h3>
|
||||||
|
<MsRichText :text="comment.content" />
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<span>{{ comment.date }}</span>
|
||||||
|
<button @click="toggleShowComment">{{ showComment ? '收起评论' : '展开评论' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
||||||
|
|
||||||
|
const showComment = ref(false);
|
||||||
|
|
||||||
|
const toggleShowComment = () => {
|
||||||
|
showComment.value = !showComment.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bug-comment {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-[16px]">
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
|
||||||
|
<div class="header-action">
|
||||||
|
<a-button>
|
||||||
|
<template #icon> <MsIconfont type="icon-icon_edit_outlined" /> </template>
|
||||||
|
{{ t('bugManagement.edit.contentEdit') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[8]" :class="{ 'max-h-[260px]': contentEditAble }">
|
||||||
|
<MsRichText
|
||||||
|
v-if="form.content"
|
||||||
|
v-model:model-value="form.content"
|
||||||
|
:disabled="!contentEditAble"
|
||||||
|
:placeholder="t('bugManagement.edit.contentPlaceholder')"
|
||||||
|
/>
|
||||||
|
<div v-else>-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-dropdown trigger="hover">
|
||||||
|
<template #content>
|
||||||
|
<MsUpload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:auto-upload="false"
|
||||||
|
multiple
|
||||||
|
draggable
|
||||||
|
accept="unknown"
|
||||||
|
is-limit
|
||||||
|
size-unit="MB"
|
||||||
|
:max-size="500"
|
||||||
|
>
|
||||||
|
<a-doption>{{ t('bugManagement.edit.localUpload') }}</a-doption>
|
||||||
|
</MsUpload>
|
||||||
|
<a-doption @click="handleLineFile">{{ t('bugManagement.edit.linkFile') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
<a-button type="outline">
|
||||||
|
<template #icon>
|
||||||
|
<icon-plus />
|
||||||
|
</template>
|
||||||
|
{{ t('bugManagement.edit.uploadFile') }}
|
||||||
|
</a-button>
|
||||||
|
</a-dropdown>
|
||||||
|
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
|
||||||
|
<FileList
|
||||||
|
:show-tab="false"
|
||||||
|
:file-list="fileList"
|
||||||
|
:upload-func="uploadFile"
|
||||||
|
@delete-file="deleteFile"
|
||||||
|
@reupload="reupload"
|
||||||
|
@handle-preview="handlePreview"
|
||||||
|
>
|
||||||
|
</FileList>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FileItem } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import MsIconfont from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
|
||||||
|
import FileList from '@/components/pure/ms-upload/fileList.vue';
|
||||||
|
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const contentEditAble = ref(false);
|
||||||
|
const fileList = ref<FileItem[]>([]);
|
||||||
|
const form = ref({
|
||||||
|
content: '',
|
||||||
|
fileList: [],
|
||||||
|
});
|
||||||
|
const uploadFile = (file: File) => {
|
||||||
|
const fileItem: FileItem = {
|
||||||
|
uid: `${Date.now()}`,
|
||||||
|
name: file.name,
|
||||||
|
status: 'init',
|
||||||
|
file,
|
||||||
|
};
|
||||||
|
fileList.value.push(fileItem);
|
||||||
|
return Promise.resolve(fileItem);
|
||||||
|
};
|
||||||
|
const handlePreview = (item: FileItem) => {
|
||||||
|
const { url } = item;
|
||||||
|
window.open(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFile = (item: FileItem) => {
|
||||||
|
fileList.value = fileList.value.filter((e) => e.uid !== item.uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reupload = (item: FileItem) => {
|
||||||
|
fileList.value = fileList.value.map((e) => {
|
||||||
|
if (e.uid === item.uid) {
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
status: 'init',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLineFile = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
&-title {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
&-action {
|
||||||
|
color: rgb(var(--primary-7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -24,6 +24,9 @@
|
||||||
<MsRichText v-model="form.content" />
|
<MsRichText v-model="form.content" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div>
|
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div>
|
||||||
|
|
||||||
|
<a-dropdown trigger="hover">
|
||||||
|
<template #content>
|
||||||
<MsUpload
|
<MsUpload
|
||||||
v-model:file-list="fileList"
|
v-model:file-list="fileList"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
|
@ -34,13 +37,17 @@
|
||||||
size-unit="MB"
|
size-unit="MB"
|
||||||
:max-size="500"
|
:max-size="500"
|
||||||
>
|
>
|
||||||
|
<a-doption>{{ t('bugManagement.edit.localUpload') }}</a-doption>
|
||||||
|
</MsUpload>
|
||||||
|
<a-doption @click="handleLineFile">{{ t('bugManagement.edit.linkFile') }}</a-doption>
|
||||||
|
</template>
|
||||||
<a-button type="outline">
|
<a-button type="outline">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-plus />
|
<icon-plus />
|
||||||
</template>
|
</template>
|
||||||
{{ t('bugManagement.edit.uploadFile') }}
|
{{ t('bugManagement.edit.uploadFile') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</MsUpload>
|
</a-dropdown>
|
||||||
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
|
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
|
||||||
<FileList
|
<FileList
|
||||||
:show-tab="false"
|
:show-tab="false"
|
||||||
|
@ -189,6 +196,8 @@
|
||||||
return Promise.resolve(fileItem);
|
return Promise.resolve(fileItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLineFile = () => {};
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
getTemplateOptions();
|
getTemplateOptions();
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
</template>
|
</template>
|
||||||
</MsAdvanceFilter>
|
</MsAdvanceFilter>
|
||||||
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
|
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
|
||||||
|
<template #name="{ record, rowIndex }">
|
||||||
|
<a-button type="text" class="px-0" @click="handleShowDetail(record.id, rowIndex)">{{ record.name }}</a-button>
|
||||||
|
</template>
|
||||||
<template #numberOfCase="{ record }">
|
<template #numberOfCase="{ record }">
|
||||||
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{
|
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{
|
||||||
record.memberCount
|
record.memberCount
|
||||||
|
@ -67,6 +70,14 @@
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<MsExportDrawer v-model:visible="exportVisible" :all-data="exportOptionData" />
|
<MsExportDrawer v-model:visible="exportVisible" :all-data="exportOptionData" />
|
||||||
|
<BugDetailDrawer
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
:detail-id="activeDetailId"
|
||||||
|
:detail-index="activeCaseIndex"
|
||||||
|
:table-data="propsRes.data"
|
||||||
|
:page-change="propsEvent.pageChange"
|
||||||
|
:pagination="propsRes.msPagination!"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -81,6 +92,7 @@
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
import BugDetailDrawer from './components/bug-detail-drawer.vue';
|
||||||
|
|
||||||
import { getBugList, getExportConfig } from '@/api/modules/bug-management';
|
import { getBugList, getExportConfig } from '@/api/modules/bug-management';
|
||||||
import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
|
import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
|
||||||
|
@ -101,6 +113,10 @@
|
||||||
const syncVisible = ref(false);
|
const syncVisible = ref(false);
|
||||||
const exportVisible = ref(false);
|
const exportVisible = ref(false);
|
||||||
const exportOptionData = ref<MsExportDrawerMap>({});
|
const exportOptionData = ref<MsExportDrawerMap>({});
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const activeDetailId = ref<string>('');
|
||||||
|
const activeCaseIndex = ref<number>(0);
|
||||||
|
|
||||||
const syncObject = reactive({
|
const syncObject = reactive({
|
||||||
time: '',
|
time: '',
|
||||||
operator: '',
|
operator: '',
|
||||||
|
@ -259,6 +275,12 @@
|
||||||
syncVisible.value = true;
|
syncVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowDetail = (id: string, rowIndex: number) => {
|
||||||
|
detailVisible.value = true;
|
||||||
|
activeDetailId.value = id;
|
||||||
|
activeCaseIndex.value = rowIndex;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopy = (record: BugListItem) => {
|
const handleCopy = (record: BugListItem) => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('create', record);
|
console.log('create', record);
|
||||||
|
|
|
@ -37,6 +37,17 @@ export default {
|
||||||
statusIsRequired: '状态不能为空',
|
statusIsRequired: '状态不能为空',
|
||||||
severityPlaceholder: '请选择严重程度',
|
severityPlaceholder: '请选择严重程度',
|
||||||
uploadFile: '添加附件',
|
uploadFile: '添加附件',
|
||||||
|
localUpload: '本地上传',
|
||||||
|
linkFile: '关联文件',
|
||||||
|
contentEdit: '内容编辑',
|
||||||
|
linkCase: '关联用例',
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
apiCase: '接口用例',
|
||||||
|
scenarioCase: '场景用例',
|
||||||
|
uiCase: 'UI用例',
|
||||||
|
performanceCase: '性能用例',
|
||||||
|
searchCase: '通过名称搜索',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default {
|
||||||
UI_TEST: 'UI test',
|
UI_TEST: 'UI test',
|
||||||
API_TEST: 'API test',
|
API_TEST: 'API test',
|
||||||
LOAD_TEST: 'Performance test',
|
LOAD_TEST: 'Performance test',
|
||||||
|
PERSONAL: 'Personal',
|
||||||
isDeleteUserGroup: 'Delete or not: {name}?',
|
isDeleteUserGroup: 'Delete or not: {name}?',
|
||||||
beforeDeleteUserGroup:
|
beforeDeleteUserGroup:
|
||||||
'After deletion, the project data under the organization will be deleted together. Please operate with caution!',
|
'After deletion, the project data under the organization will be deleted together. Please operate with caution!',
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default {
|
||||||
UI_TEST: 'UI测试',
|
UI_TEST: 'UI测试',
|
||||||
API_TEST: 'API测试',
|
API_TEST: 'API测试',
|
||||||
LOAD_TEST: '性能测试',
|
LOAD_TEST: '性能测试',
|
||||||
|
PERSONAL: '我的设置',
|
||||||
isDeleteUserGroup: '是否删除: {name}?',
|
isDeleteUserGroup: '是否删除: {name}?',
|
||||||
beforeDeleteUserGroup: '删除后,该组织下的项目数据将一起删除,请谨慎操作!',
|
beforeDeleteUserGroup: '删除后,该组织下的项目数据将一起删除,请谨慎操作!',
|
||||||
confirmDelete: '确认删除',
|
confirmDelete: '确认删除',
|
||||||
|
|
Loading…
Reference in New Issue