feat(功能用例): 功能用例和用例回收站页面和本地联调

This commit is contained in:
xinxin.wu 2023-11-25 18:00:46 +08:00 committed by Craftsman
parent 86ce86649a
commit 700003a01f
58 changed files with 5333 additions and 766 deletions

View File

@ -79,18 +79,26 @@ export class MSAxios {
/**
* @description:
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams, customFileKey = ''): Promise<T> {
uploadFile<T = any>(
config: AxiosRequestConfig,
params: UploadFileParams,
customFileKey = '',
isMultiple = false
): Promise<T> {
const formData = new window.FormData();
const fileName = params.fileList.length === 1 ? 'file' : 'files';
const fileName = isMultiple ? 'files' : 'file';
if (customFileKey !== '') {
params.fileList.forEach((file: File) => {
formData.append(customFileKey, file);
});
} else {
} else if (!isMultiple && !customFileKey) {
params.fileList.forEach((file: File) => {
formData.append(fileName, file);
});
} else {
params.fileList.forEach((item: any) => {
formData.append(fileName, item.file, item.file.name);
});
}
if (params.request) {
const requestData = JSON.stringify(params.request);

View File

@ -0,0 +1,149 @@
import MSR from '@/api/http/index';
import {
BatchCopyCaseUrl,
BatchDeleteCaseUrl,
BatchDeleteRecycleCaseListUrl,
BatchEditCaseUrl,
BatchMoveCaseUrl,
CreateCaseModuleTreeUrl,
CreateCaseUrl,
DeleteCaseModuleTreeUrl,
DeleteCaseUrl,
DeleteRecycleCaseListUrl,
DetailCaseUrl,
GetAssociatedFilePageUrl,
GetCaseListUrl,
GetCaseModulesCountUrl,
GetCaseModuleTreeUrl,
GetDefaultTemplateFieldsUrl,
GetRecycleCaseListUrl,
GetRecycleCaseModulesCountUrl,
GetTrashCaseModuleTreeUrl,
MoveCaseModuleTreeUrl,
RecoverRecycleCaseListUrl,
RestoreCaseListUrl,
UpdateCaseModuleTreeUrl,
UpdateCaseUrl,
} from '@/api/requrls/case-management/featureCase';
import type {
AssociatedList,
BatchDeleteType,
BatchEditCaseType,
BatchMoveOrCopyType,
CaseManagementTable,
CaseModuleQueryParams,
CreateOrUpdateModule,
DeleteCaseType,
ModulesTreeType,
MoveModules,
UpdateModule,
} from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common';
// 获取模块树
export function getCaseModuleTree(projectId: string) {
return MSR.get<ModulesTreeType[]>({ url: `${GetCaseModuleTreeUrl}/${projectId}` });
}
// 创建模块树
export function createCaseModuleTree(data: CreateOrUpdateModule) {
return MSR.post({ url: CreateCaseModuleTreeUrl, data });
}
// 更新模块树
export function updateCaseModuleTree(data: UpdateModule) {
return MSR.post({ url: UpdateCaseModuleTreeUrl, data });
}
// 移动模块树
export function moveCaseModuleTree(data: MoveModules) {
return MSR.post({ url: MoveCaseModuleTreeUrl, data });
}
// 回收站-模块-获取模块树
export function getTrashCaseModuleTree(projectId: string) {
return MSR.get<ModulesTreeType[]>({ url: `${GetTrashCaseModuleTreeUrl}/${projectId}` });
}
// 删除模块
export function deleteCaseModuleTree(id: string) {
return MSR.get({ url: `${DeleteCaseModuleTreeUrl}/${id}` });
}
// 用例分页表
export function getCaseList(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: GetCaseListUrl, data });
}
// 删除用例
export function deleteCaseRequest(data: DeleteCaseType) {
return MSR.post({ url: `${DeleteCaseUrl}`, data });
}
// 获取默认模版自定义字段
export function getCaseDefaultFields(projectId: string) {
return MSR.get({ url: `${GetDefaultTemplateFieldsUrl}/${projectId}` });
}
// 获取关联文件列表
export function getAssociatedFileListUrl(data: TableQueryParams) {
return MSR.post<CommonList<AssociatedList>>({ url: GetAssociatedFilePageUrl, data });
}
// 创建用例
export function createCaseRequest(data: Record<string, any>) {
return MSR.uploadFile({ url: CreateCaseUrl }, { request: data.request, fileList: data.fileList }, '', true);
}
// 编辑用例
export function updateCaseRequest(data: Record<string, any>) {
return MSR.uploadFile({ url: UpdateCaseUrl }, { request: data.request, fileList: data.fileList }, '', true);
}
// 用例详情
export function getCaseDetail(id: string) {
return MSR.get({ url: `${DetailCaseUrl}/${id}` });
}
// 批量删除用例
export function batchDeleteCase(data: BatchDeleteType) {
return MSR.post({ url: `${BatchDeleteCaseUrl}`, data });
}
// 批量编辑属性
export function batchEditAttrs(data: BatchEditCaseType) {
return MSR.post({ url: `${BatchEditCaseUrl}`, data });
}
// 批量移动到模块
export function batchMoveToModules(data: BatchMoveOrCopyType) {
return MSR.post({ url: `${BatchMoveCaseUrl}`, data });
}
// 批量复制到模块
export function batchCopyToModules(data: BatchMoveOrCopyType) {
return MSR.post({ url: `${BatchCopyCaseUrl}`, data });
}
// 回收站
// 回收站用例分页表
export function getRecycleListRequest(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: GetRecycleCaseListUrl, data });
}
// 获取回收站模块数量
export function getRecycleModulesCounts(data: CaseModuleQueryParams) {
return MSR.post({ url: GetRecycleCaseModulesCountUrl, data });
}
// 获取全部用例模块数量
export function getCaseModulesCounts(data: CaseModuleQueryParams) {
return MSR.post({ url: GetCaseModulesCountUrl, data });
}
// 批量恢复回收站用例表
export function restoreCaseList(data: BatchMoveOrCopyType) {
return MSR.post({ url: RestoreCaseListUrl, data });
}
// 批量彻底删除回收站用例表
export function batchDeleteRecycleCase(data: BatchMoveOrCopyType) {
return MSR.post({ url: BatchDeleteRecycleCaseListUrl, data });
}
// 恢复回收站单个用例
export function recoverRecycleCase(id: string) {
return MSR.get({ url: `${RecoverRecycleCaseListUrl}/${id}` });
}
// 删除回收站单个用例
export function deleteRecycleCaseList(id: string) {
return MSR.get({ url: `${DeleteRecycleCaseListUrl}/${id}` });
}
export default {};

View File

@ -12,13 +12,13 @@ import {
} from '@/api/requrls/setting/member';
import type { CommonList, TableQueryParams } from '@/models/common';
import type { AddorUpdateMemberModel, BatchAddProjectModel, LinkItem, MemberItem } from '@/models/setting/member';
import type { AddOrUpdateMemberModel, BatchAddProjectModel, LinkItem, MemberItem } from '@/models/setting/member';
// 获取成员列表
export function getMemberList(data: TableQueryParams) {
return MSR.post<CommonList<MemberItem>>({ url: GetMemberListUrl, data });
}
// 添加成员
export function addOrUpdate(data: AddorUpdateMemberModel, type: string) {
export function addOrUpdate(data: AddOrUpdateMemberModel, type: string) {
if (type === 'add') {
return MSR.post({ url: AddMemberUrl, data });
}

View File

@ -0,0 +1,62 @@
// 用例管理列表
export const GetCaseListUrl = '/functional/case/page';
// 用例管理-添加
export const CreateCaseUrl = '/functional/case/add';
// 用例管理-更新
export const UpdateCaseUrl = '/functional/case/update';
// 用例管理-删除
export const DeleteCaseUrl = '/functional/case/delete';
// 用例管理-详情
export const DetailCaseUrl = '/functional/case/detail';
// 用例管理-批量移动用例
export const BatchMoveCaseUrl = '/functional/case/batch/move';
// 用例管理-批量删除用例
export const BatchDeleteCaseUrl = '/functional/case/batch/delete-to-gc';
// 用例管理-批量删除用例
export const BatchEditCaseUrl = '/functional/case/batch/edit';
// 用例管理-批量复制
export const BatchCopyCaseUrl = '/functional/case/batch/copy';
// 用例管理-关注/取消关注用例
export const FollowerCaseUrl = '/functional/case/edit/follower';
// 获取用例关注人
export const GetCaseFollowerUrl = '/functional/case/follower';
// 获取用例模板自定义字段
export const GetCaseCustomFieldsUrl = '/functional/case/default/template/field';
// 获取表头自定义字段(高级搜索中的自定义字段)
export const GetSearchCustomFieldsUrl = '/functional/case/custom/field';
// 关联文件列表
export const GetAssociatedFilePageUrl = '/attachment/page';
// 获取模块树
export const GetCaseModuleTreeUrl = '/functional/case/module/tree';
// 创建模块树
export const CreateCaseModuleTreeUrl = '/functional/case/module/add';
// 更新模块树
export const UpdateCaseModuleTreeUrl = '/functional/case/module/update';
// 移动模块
export const MoveCaseModuleTreeUrl = '/functional/case/module/move';
// 回收站-模块-获取模块树
export const GetTrashCaseModuleTreeUrl = '/functional/case/module/trash/tree';
// 删除模块
export const DeleteCaseModuleTreeUrl = '/functional/case/module/delete';
// 获取默认模版自定义字段
export const GetDefaultTemplateFieldsUrl = '/functional/case/default/template/field';
// 回收站
// 回收站分页
export const GetRecycleCaseListUrl = '/functional/case/trash/page';
// 获取回收站模块数量
export const GetRecycleCaseModulesCountUrl = '/functional/case/trash/module/count';
// 获取全部用例模块数量
export const GetCaseModulesCountUrl = '/functional/case/module/count';
// 恢复回收站用例表
export const RestoreCaseListUrl = '/functional/case/trash/batch/recover';
// 批量彻底删除回收站用例表
export const BatchDeleteRecycleCaseListUrl = '/functional/case/trash/batch/delete';
// 恢复回收站单个用例
export const RecoverRecycleCaseListUrl = '/functional/case/trash/recover';
// 删除回收站单个用例
export const DeleteRecycleCaseListUrl = '/functional/case/trash/delete';
export default {};

View File

@ -1,7 +1,7 @@
@font-face {
font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1697180140168') format('woff2'), url('iconfont.woff?t=1697180140168') format('woff'),
url('iconfont.ttf?t=1697180140168') format('truetype'), url('iconfont.svg?t=1697180140168#iconfont') format('svg');
src: url('iconfont.woff2?t=1700905969825') format('woff2'), url('iconfont.woff?t=1700905969825') format('woff'),
url('iconfont.ttf?t=1700905969825') format('truetype'), url('iconfont.svg?t=1700905969825#iconfont') format('svg');
}
.iconfont {
font-size: 16px;
@ -10,6 +10,102 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-icon_text-wrap-overflow::before {
content: '\e78f';
}
.icon-icon_template_filled::before {
content: '\e78e';
}
.icon-icon_laser_filled::before {
content: '\e78b';
}
.icon-icon_stop_outlined::before {
content: '\e78c';
}
.icon-icon_card_filled::before {
content: '\e78d';
}
.icon-icon_community-tab_outlined::before {
content: '\e77e';
}
.icon-icon_play-round_filled1::before {
content: '\e77f';
}
.icon-icon_thumbdown_outlined::before {
content: '\e780';
}
.icon-icon_file-doc_colorful::before {
content: '\e781';
}
.icon-icon-product-forum_filled::before {
content: '\e782';
}
.icon-icon_file-add_colorful::before {
content: '\e783';
}
.icon-icon_group_filled::before {
content: '\e784';
}
.icon-icon_send_colorful::before {
content: '\e785';
}
.icon-icon-product-forum_colorful::before {
content: '\e786';
}
.icon-icon_nearby-group_outlined::before {
content: '\e787';
}
.icon-icon_thumbdown_filled::before {
content: '\e788';
}
.icon-icon_thumbsup_filled::before {
content: '\e789';
}
.icon-icon_file-add_outlined::before {
content: '\e78a';
}
.icon-icon_thumbsup_outlined::before {
content: '\e77b';
}
.icon-icon-product-forum_outlined::before {
content: '\e77c';
}
.icon-icon_test-tracking_colorful::before {
content: '\e77d';
}
.icon-icon_reply::before {
content: '\e778';
}
.icon-icon_comment_collapse_text_input::before {
content: '\e779';
}
.icon-icon_comment_expand_text_input::before {
content: '\e77a';
}
.icon-icon_chart_graph::before {
content: '\e777';
}
.icon-icon_share1::before {
content: '\e776';
}
.icon-icon_text::before {
content: '\e775';
}
.icon-icon_click::before {
content: '\e697';
}
.icon-icon_feedback_outlined::before {
content: '\e773';
}
.icon-icon_done_outlined::before {
content: '\e774';
}
.icon-icon_dataset_outlined::before {
content: '\e772';
}
.icon-icon_outer-borders_outlined::before {
content: '\e771';
}
.icon-icon_checkbox::before {
content: '\e76e';
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,230 @@
"css_prefix_text": "icon-",
"description": "DE、MS项目icon管理",
"glyphs": [
{
"icon_id": "38195845",
"name": "icon_text-wrap-overflow",
"font_class": "icon_text-wrap-overflow",
"unicode": "e78f",
"unicode_decimal": 59279
},
{
"icon_id": "38036023",
"name": "icon_template_filled",
"font_class": "icon_template_filled",
"unicode": "e78e",
"unicode_decimal": 59278
},
{
"icon_id": "38020820",
"name": "icon_laser_filled",
"font_class": "icon_laser_filled",
"unicode": "e78b",
"unicode_decimal": 59275
},
{
"icon_id": "38020819",
"name": "icon_stop_outlined",
"font_class": "icon_stop_outlined",
"unicode": "e78c",
"unicode_decimal": 59276
},
{
"icon_id": "38020817",
"name": "icon_card_filled",
"font_class": "icon_card_filled",
"unicode": "e78d",
"unicode_decimal": 59277
},
{
"icon_id": "38020824",
"name": "icon_community-tab_outlined",
"font_class": "icon_community-tab_outlined",
"unicode": "e77e",
"unicode_decimal": 59262
},
{
"icon_id": "38020833",
"name": "icon_play-round_filled",
"font_class": "icon_play-round_filled1",
"unicode": "e77f",
"unicode_decimal": 59263
},
{
"icon_id": "38020826",
"name": "icon_thumbdown_outlined",
"font_class": "icon_thumbdown_outlined",
"unicode": "e780",
"unicode_decimal": 59264
},
{
"icon_id": "38020832",
"name": "icon_file-doc_colorful",
"font_class": "icon_file-doc_colorful",
"unicode": "e781",
"unicode_decimal": 59265
},
{
"icon_id": "38020828",
"name": "icon-product-forum_filled",
"font_class": "icon-product-forum_filled",
"unicode": "e782",
"unicode_decimal": 59266
},
{
"icon_id": "38020827",
"name": "icon_file-add_colorful",
"font_class": "icon_file-add_colorful",
"unicode": "e783",
"unicode_decimal": 59267
},
{
"icon_id": "38020830",
"name": "icon_group_filled",
"font_class": "icon_group_filled",
"unicode": "e784",
"unicode_decimal": 59268
},
{
"icon_id": "38020829",
"name": "icon_send_colorful",
"font_class": "icon_send_colorful",
"unicode": "e785",
"unicode_decimal": 59269
},
{
"icon_id": "38020823",
"name": "icon-product-forum_colorful",
"font_class": "icon-product-forum_colorful",
"unicode": "e786",
"unicode_decimal": 59270
},
{
"icon_id": "38020822",
"name": "icon_nearby-group_outlined",
"font_class": "icon_nearby-group_outlined",
"unicode": "e787",
"unicode_decimal": 59271
},
{
"icon_id": "38020825",
"name": "icon_thumbdown_filled",
"font_class": "icon_thumbdown_filled",
"unicode": "e788",
"unicode_decimal": 59272
},
{
"icon_id": "38020821",
"name": "icon_thumbsup_filled",
"font_class": "icon_thumbsup_filled",
"unicode": "e789",
"unicode_decimal": 59273
},
{
"icon_id": "38020818",
"name": "icon_file-add_outlined",
"font_class": "icon_file-add_outlined",
"unicode": "e78a",
"unicode_decimal": 59274
},
{
"icon_id": "38020835",
"name": "icon_thumbsup_outlined",
"font_class": "icon_thumbsup_outlined",
"unicode": "e77b",
"unicode_decimal": 59259
},
{
"icon_id": "38020834",
"name": "icon-product-forum_outlined",
"font_class": "icon-product-forum_outlined",
"unicode": "e77c",
"unicode_decimal": 59260
},
{
"icon_id": "38020831",
"name": "icon_test-tracking_colorful",
"font_class": "icon_test-tracking_colorful",
"unicode": "e77d",
"unicode_decimal": 59261
},
{
"icon_id": "37903599",
"name": "icon_reply",
"font_class": "icon_reply",
"unicode": "e778",
"unicode_decimal": 59256
},
{
"icon_id": "37903597",
"name": "icon_comment_collapse_text_input",
"font_class": "icon_comment_collapse_text_input",
"unicode": "e779",
"unicode_decimal": 59257
},
{
"icon_id": "37903598",
"name": "icon_comment_expand_text_input",
"font_class": "icon_comment_expand_text_input",
"unicode": "e77a",
"unicode_decimal": 59258
},
{
"icon_id": "37878729",
"name": "icon_chart_graph",
"font_class": "icon_chart_graph",
"unicode": "e777",
"unicode_decimal": 59255
},
{
"icon_id": "37848043",
"name": "icon_share",
"font_class": "icon_share1",
"unicode": "e776",
"unicode_decimal": 59254
},
{
"icon_id": "37842152",
"name": "icon_text",
"font_class": "icon_text",
"unicode": "e775",
"unicode_decimal": 59253
},
{
"icon_id": "37841481",
"name": "icon_click",
"font_class": "icon_click",
"unicode": "e697",
"unicode_decimal": 59031
},
{
"icon_id": "37789808",
"name": "icon_feedback_outlined",
"font_class": "icon_feedback_outlined",
"unicode": "e773",
"unicode_decimal": 59251
},
{
"icon_id": "37789807",
"name": "icon_done_outlined",
"font_class": "icon_done_outlined",
"unicode": "e774",
"unicode_decimal": 59252
},
{
"icon_id": "37789689",
"name": "icon_dataset_outlined",
"font_class": "icon_dataset_outlined",
"unicode": "e772",
"unicode_decimal": 59250
},
{
"icon_id": "37715855",
"name": "icon_outer-borders_outlined",
"font_class": "icon_outer-borders_outlined",
"unicode": "e771",
"unicode_decimal": 59249
},
{
"icon_id": "37662517",
"name": "icon_checkbox",
@ -1608,13 +1832,6 @@
"unicode": "e695",
"unicode_decimal": 59029
},
{
"icon_id": "32849836",
"name": "icon_logs_outlined-1",
"font_class": "icon_logs_outlined-1",
"unicode": "e697",
"unicode_decimal": 59031
},
{
"icon_id": "32849837",
"name": "icon_refresh_outlined",

View File

@ -14,6 +14,70 @@
/>
<missing-glyph />
<glyph glyph-name="icon_text-wrap-overflow" unicode="&#59279;" d="M746.666667 488.32a234.666667 234.666667 0 0 0 0-469.333333h-78.208v-52.181334a26.069333 26.069333 0 0 0-41.728-20.821333l-111.232 83.413333a52.138667 52.138667 0 0 0 0 83.456l111.232 83.413334a26.069333 26.069333 0 0 0 41.728-20.864v-52.138667H746.666667a130.389333 130.389333 0 1 1 0 260.736H94.805333a52.138667 52.138667 0 0 0 0 104.32H746.666667zM303.402667 123.264a52.138667 52.138667 0 0 0 0-104.277333H94.805333a52.138667 52.138667 0 0 0 0 104.277333h208.64zM94.805333 853.333333h834.389334a52.138667 52.138667 0 0 0 0-104.277333H94.805333a52.138667 52.138667 0 1 0 0 104.277333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_template_filled" unicode="&#59278;" d="M728.96 853.333333H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-853.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h682.666666a42.666667 42.666667 0 0 1 42.666667 42.666666V686.250667a42.666667 42.666667 0 0 1-12.501333 30.165333l-124.330667 124.416A42.666667 42.666667 0 0 1 729.002667 853.333333zM298.666667 533.333333a21.333333 21.333333 0 0 0 21.333333 21.333334h384a21.333333 21.333333 0 0 0 21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 0-21.333333-21.333334h-384a21.333333 21.333333 0 0 0-21.333333 21.333334v42.666666z m170.666666-170.666666a21.333333 21.333333 0 0 0 21.333334 21.333333h213.333333a21.333333 21.333333 0 0 0 21.333333-21.333333v-213.333334a21.333333 21.333333 0 0 0-21.333333-21.333333h-213.333333a21.333333 21.333333 0 0 0-21.333334 21.333333v213.333334z m-170.666666 0a21.333333 21.333333 0 0 0 21.333333 21.333333h42.666667a21.333333 21.333333 0 0 0 21.333333-21.333333v-213.333334a21.333333 21.333333 0 0 0-21.333333-21.333333h-42.666667a21.333333 21.333333 0 0 0-21.333333 21.333333v213.333334z" horiz-adv-x="1024" />
<glyph glyph-name="icon_laser_filled" unicode="&#59275;" d="M533.248 604.245333a234.666667 234.666667 0 1 1-241.664-241.493333l-36.693333 91.776a149.333333 149.333333 0 1 0 186.666666 186.368l91.690667-36.693333zM523.818667-45.397333c14.464-36.096 65.664-35.669333 79.488 0.64l92.714666 242.858666 244.864 94.592c36.138667 13.994667 36.437333 65.024 0.426667 79.445334L337.962667 613.504c-34.858667 13.909333-69.418667-20.650667-55.466667-55.466667l241.322667-603.434666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_stop_outlined" unicode="&#59276;" d="M981.333333 384c0-259.2-210.133333-469.333333-469.333333-469.333333S42.666667 124.8 42.666667 384 252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333z m-85.333333 0a384 384 0 1 1-768 0 384 384 0 0 1 768 0zM384 554.666667h256c23.466667 0 42.666667-19.072 42.666667-42.666667v-256c0-23.552-19.2-42.666667-42.666667-42.666667H384c-23.466667 0-42.666667 19.114667-42.666667 42.666667V512c0 23.594667 19.2 42.666667 42.666667 42.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_card_filled" unicode="&#59277;" d="M426.666667 810.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667v-298.666667a42.666667 42.666667 0 0 1 42.666667-42.666666h298.666667a42.666667 42.666667 0 0 1 42.666666 42.666666V768a42.666667 42.666667 0 0 1-42.666666 42.666667zM426.666667 341.333333H128a42.666667 42.666667 0 0 1-42.666667-42.666666v-298.666667a42.666667 42.666667 0 0 1 42.666667-42.666667h298.666667a42.666667 42.666667 0 0 1 42.666666 42.666667v298.666667a42.666667 42.666667 0 0 1-42.666666 42.666666zM896 810.666667h-298.666667a42.666667 42.666667 0 0 1-42.666666-42.666667v-298.666667a42.666667 42.666667 0 0 1 42.666666-42.666666h298.666667a42.666667 42.666667 0 0 1 42.666667 42.666666V768a42.666667 42.666667 0 0 1-42.666667 42.666667zM893.482667 341.333333H599.893333c-24.96 0-45.184-20.224-45.184-45.184v-293.632c0-24.96 20.224-45.184 45.184-45.184h293.632c24.96 0 45.184 20.224 45.184 45.184V296.106667c0 24.96-20.224 45.184-45.184 45.184z" horiz-adv-x="1024" />
<glyph glyph-name="icon_community-tab_outlined" unicode="&#59262;" d="M213.333333 167.808c0 142.933333 120.021333 258.816 258.901334 258.816L552.149333 426.666667c138.922667 0 258.56-115.882667 258.56-258.858667V21.333333a42.666667 42.666667 0 0 0-42.666666-42.666666H256a42.666667 42.666667 0 0 0-42.666667 42.666666v146.474667z m258.901334 173.482667C379.605333 341.333333 298.666667 262.784 298.666667 167.808V64h426.666666v103.808C725.333333 262.912 644.693333 341.333333 552.192 341.333333h-79.914667zM617.6 555.733333A149.333333 149.333333 0 0 0 513.152 512H510.976a149.333333 149.333333 0 1 0 106.666667 43.733333zM512 725.333333a64 64 0 1 1 0-128 64 64 0 0 1 0 128zM320 522.666667a96 96 0 1 0-192 0 96 96 0 0 0 192 0z m-64 0a32 32 0 1 1-64 0 32 32 0 0 1 64 0zM215.253333 384C111.061333 384 64 297.088 64 189.866667v-115.2c0-17.706667 14.336-32 32-32H149.333333v128c0 154.837333 72.96 189.781333 116.309334 210.602666l4.266666 2.048A183.850667 183.850667 0 0 1 253.781333 384h-38.570666zM808.704 384c104.192 0 151.210667-86.912 151.210667-194.133333v-115.2a32 32 0 0 0-32-32h-53.333334v128c0 154.837333-72.874667 189.781333-116.266666 210.602666l-4.266667 2.048c5.333333 0.426667 10.666667 0.682667 16.085333 0.682667h38.570667zM800 426.666667a96 96 0 1 1 0 192 96 96 0 0 1 0-192z m0 64a32 32 0 1 0 0 64 32 32 0 0 0 0-64z" horiz-adv-x="1024" />
<glyph glyph-name="icon_play-round_filled1" unicode="&#59263;" d="M512 853.333333c259.2 0 469.333333-210.133333 469.333333-469.333333s-210.133333-469.333333-469.333333-469.333333S42.666667 124.8 42.666667 384 252.8 853.333333 512 853.333333zM421.077333 597.333333c-9.813333 0-19.2-3.84-26.24-10.666666A35.797333 35.797333 0 0 1 384 560.896v-353.92c0-6.826667 1.962667-13.44 5.632-19.242667a37.674667 37.674667 0 0 1 51.072-11.562666l288.554667 176.938666A36.352 36.352 0 0 1 746.666667 384a36.352 36.352 0 0 1-17.408 30.890667L440.746667 591.786667A37.589333 37.589333 0 0 1 421.077333 597.333333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_thumbdown_outlined" unicode="&#59264;" d="M128.341333 814.08H42.666667v-599.68h85.674666C128 213.333333 128 810.666667 128.341333 814.08z m492.544-599.722667h183.722667c127.786667 0 157.141333 112.810667 127.786667 197.333334l-127.786667 336.469333A90.88 90.88 0 0 1 716.8 814.421333H213.674667a21.418667 21.418667 0 0 1-21.376-21.376v-557.312c0-11.818667 9.557333-21.418667 21.376-21.418666H257.706667c13.952 0 27.008-6.784 35.029333-18.176l177.834667-252.970667c12.032-19.157333 44.544-33.834667 79.146666-18.048 51.968 23.637333 114.133333 75.264 114.133334 160.853333 0 32.256-14.336 75.050667-42.965334 128.341334z m183.722667 85.632h-327.04l67.84-126.208c22.570667-41.984 32.768-72.490667 32.768-87.808 0-24.874667-5.205333-52.992-49.834667-76.032l-191.104 271.872a42.837333 42.837333 0 0 1-35.029333 18.176h-24.277333V728.789333H716.8c2.645333 0 4.693333-1.578667 5.205333-3.413333l1.066667-3.882667 128.768-338.944c16.938667-49.877333 0.042667-82.56-47.232-82.56z" horiz-adv-x="1024" />
<glyph glyph-name="icon_file-doc_colorful" unicode="&#59265;" d="M170.666667 789.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h420.693334a21.333333 21.333333 0 0 0 15.232-6.4l197.973333-201.386667a21.333333 21.333333 0 0 0 6.101333-14.933333V-21.333333a42.666667 42.666667 0 0 0-42.666666-42.666667H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v810.666666zM640 831.146667a21.333333 21.333333 0 0 0 9.258667-5.546667l197.973333-201.386667a21.333333 21.333333 0 0 0 3.925333-5.546666h-162.730666A48.426667 48.426667 0 0 0 640 667.093333V831.146667zM344.448 461.568h323.498667c3.84 0 6.954667-3.114667 6.954666-6.954667v-32.597333c0-3.84-3.114667-6.997333-6.954666-6.997333H344.448a6.997333 6.997333 0 0 0-6.997333 6.997333v32.597333c0 3.84 3.114667 6.954667 6.997333 6.954667z m0-139.648h323.498667c3.84 0 6.954667-3.114667 6.954666-6.954667v-32.597333c0-3.84-3.114667-6.997333-6.954666-6.997333H344.448a6.997333 6.997333 0 0 0-6.997333 6.997333v32.597333c0 3.84 3.114667 6.954667 6.997333 6.954667z m0-139.605333h172.202667c3.84 0 6.997333-3.114667 6.997333-6.997334v-32.597333c0-3.84-3.114667-6.954667-6.997333-6.954667H344.448a6.997333 6.997333 0 0 0-6.997333 6.954667v32.597333c0 3.84 3.114667 6.997333 6.997333 6.997334z" horiz-adv-x="1024" />
<glyph glyph-name="icon-product-forum_filled" unicode="&#59266;" d="M725.333333 426.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h170.666667a42.666667 42.666667 0 0 0 42.666666-42.666666v-341.333334a42.666667 42.666667 0 0 0-42.666666-42.666666h-89.002667l-51.498667-51.498667a42.666667 42.666667 0 0 0-60.330666 0L686.336 42.666667H469.333333a42.666667 42.666667 0 0 0-42.666666 42.666666v170.666667a42.666667 42.666667 0 0 0 42.666666 42.666667h256v128z m85.333334-42.666667v-128a42.666667 42.666667 0 0 0-42.666667-42.666667h-256v-85.333333h192a42.666667 42.666667 0 0 0 30.165333-12.501333l33.834667-33.834667 33.834667 33.834667A42.666667 42.666667 0 0 0 832 128H896v256h-85.333333zM85.333333 810.666667a42.666667 42.666667 0 0 1-42.666666-42.666667v-512a42.666667 42.666667 0 0 1 42.666666-42.666667h89.002667l72.832-72.832a42.666667 42.666667 0 0 1 60.330667 0L380.330667 213.333333H768a42.666667 42.666667 0 0 1 42.666667 42.666667V768a42.666667 42.666667 0 0 1-42.666667 42.666667H85.333333z m213.333334-298.666667a42.666667 42.666667 0 1 0-85.333334 0 42.666667 42.666667 0 0 0 85.333334 0z m170.666666 0a42.666667 42.666667 0 1 0-85.333333 0 42.666667 42.666667 0 0 0 85.333333 0z m170.666667 0a42.666667 42.666667 0 1 0-85.333333 0 42.666667 42.666667 0 0 0 85.333333 0z" horiz-adv-x="1024" />
<glyph glyph-name="icon_file-add_colorful" unicode="&#59267;" d="M170.666667 783.573333C170.666667 810.325333 191.744 832 217.728 832h425.130667c6.229333 0 12.202667-2.56 16.597333-7.04l186.88-190.634667a24.576 24.576 0 0 0 6.997333-17.194666v-632.704c0-26.752-21.077333-48.426667-47.061333-48.426667H217.728C191.744-64 170.666667-42.325333 170.666667-15.573333V783.573333zM652.586667 829.866667a23.466667 23.466667 0 0 0 6.869333-4.906667l186.922667-190.634667a24.32 24.32 0 0 0 5.290666-8.149333h-152.021333c-25.984 0-47.061333 21.674667-47.061333 48.426667V829.866667zM512 469.333333a21.333333 21.333333 0 0 1-21.333333-21.333333v-128h-128a21.333333 21.333333 0 0 1 0-42.666667h128v-128a21.333333 21.333333 0 0 1 42.666666 0v128h128a21.333333 21.333333 0 0 1 0 42.666667h-128v128a21.333333 21.333333 0 0 1-21.333333 21.333333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_group_filled" unicode="&#59268;" d="M362.666667 384a192 192 0 1 1 0 384 192 192 0 0 1 0-384zM64-42.666667a42.666667 42.666667 0 0 0-42.666667 42.666667v82.474667C21.333333 225.408 141.354667 341.333333 280.234667 341.333333h165.248c138.88 0 258.517333-115.882667 258.517333-258.858666V0a42.666667 42.666667 0 0 0-42.666667-42.666667h-597.333333zM789.333333 0v12.629333c0 98.645333 0 200.704-85.333333 264.704 3.584 0.298667 9.557333 0.213333 15.36 0.085334l7.850667-0.085334H810.666667c104.192 0 192-65.578667 192-172.8v-72.533333a32 32 0 0 0-32-32H789.333333zM746.666667 341.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" horiz-adv-x="1024" />
<glyph glyph-name="icon_send_colorful" unicode="&#59269;" d="M512 332.373333v-267.349333l146.773333-132.266667a43.477333 43.477333 0 0 1 72.661334 19.541334l220.373333 780.544c6.058667 21.376-20.522667 36.608-35.925333 20.565333L512 332.373333zM881.706667 788.181333c16.213333 15.573333 0.426667 42.410667-21.034667 35.797334L81.92 583.424a43.434667 43.434667 0 0 1-19.712-72.704l133.888-146.645333h243.413333l442.154667 424.106666z" horiz-adv-x="1024" />
<glyph glyph-name="icon-product-forum_colorful" unicode="&#59270;" d="M725.333333 426.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h170.666667a42.666667 42.666667 0 0 0 42.666666-42.666666v-341.333334a42.666667 42.666667 0 0 0-42.666666-42.666666h-89.002667l-51.498667-51.498667a42.666667 42.666667 0 0 0-60.330666 0L686.336 42.666667H469.333333a42.666667 42.666667 0 0 0-42.666666 42.666666v170.666667a42.666667 42.666667 0 0 0 42.666666 42.666667h256v128z m85.333334-42.666667v-128a42.666667 42.666667 0 0 0-42.666667-42.666667h-256v-85.333333h192a42.666667 42.666667 0 0 0 30.165333-12.501333l33.834667-33.834667 33.834667 33.834667A42.666667 42.666667 0 0 0 832 128H896v256h-85.333333zM85.333333 810.666667a42.666667 42.666667 0 0 1-42.666666-42.666667v-512a42.666667 42.666667 0 0 1 42.666666-42.666667h89.002667l72.832-72.832a42.666667 42.666667 0 0 1 60.330667 0L380.330667 213.333333H768a42.666667 42.666667 0 0 1 42.666667 42.666667V768a42.666667 42.666667 0 0 1-42.666667 42.666667H85.333333z m213.333334-298.666667a42.666667 42.666667 0 1 0-85.333334 0 42.666667 42.666667 0 0 0 85.333334 0z m170.666666 0a42.666667 42.666667 0 1 0-85.333333 0 42.666667 42.666667 0 0 0 85.333333 0z m170.666667 0a42.666667 42.666667 0 1 0-85.333333 0 42.666667 42.666667 0 0 0 85.333333 0z" horiz-adv-x="1024" />
<glyph glyph-name="icon_nearby-group_outlined" unicode="&#59271;" d="M256 426.666667a170.666667 170.666667 0 1 1 0 341.333333 170.666667 170.666667 0 0 1 0-341.333333z m0 85.333333a85.333333 85.333333 0 1 0 0 170.666667 85.333333 85.333333 0 0 0 0-170.666667zM42.666667 213.333333a170.666667 170.666667 0 0 0 170.666666 170.666667h85.333334a170.666667 170.666667 0 0 0 170.666666-170.666667v-170.666666a42.666667 42.666667 0 0 0-42.666666-42.666667H85.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v170.666666z m256 85.333334H213.333333a85.333333 85.333333 0 0 1-85.333333-85.333334v-128h256v128a85.333333 85.333333 0 0 1-85.333333 85.333334zM554.666667 213.333333a170.666667 170.666667 0 0 0 170.666666 170.666667h85.333334a170.666667 170.666667 0 0 0 170.666666-170.666667v-170.666666a42.666667 42.666667 0 0 0-42.666666-42.666667h-341.333334a42.666667 42.666667 0 0 0-42.666666 42.666667v170.666666z m85.333333 0v-128h256v128a85.333333 85.333333 0 0 1-85.333333 85.333334h-85.333334a85.333333 85.333333 0 0 1-85.333333-85.333334zM938.666667 597.333333a170.666667 170.666667 0 1 1-341.333334 0 170.666667 170.666667 0 0 1 341.333334 0z m-85.333334 0a85.333333 85.333333 0 1 0-170.666666 0 85.333333 85.333333 0 0 0 170.666666 0z" horiz-adv-x="1024" />
<glyph glyph-name="icon_thumbdown_filled" unicode="&#59272;" d="M620.842667 210.56h183.722666c127.829333 0 157.184 112.810667 127.829334 197.376l-127.829334 336.426667A90.88 90.88 0 0 1 716.8 810.666667H213.76a21.418667 21.418667 0 0 1-21.461333-21.418667v-557.269333c0-11.818667 9.6-21.418667 21.418666-21.418667H257.706667c13.952 0 27.008-6.826667 35.029333-18.218667l177.834667-252.928c12.032-19.2 44.501333-33.834667 79.146666-18.090666 51.925333 23.68 114.133333 75.306667 114.133334 160.853333 0 32.256-14.336 75.093333-43.008 128.384zM128 810.666667H42.325333v-599.68H128C127.658667 212.053333 127.658667 807.253333 128 810.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_thumbsup_filled" unicode="&#59273;" d="M128.298667-42.282667H42.666667V557.354667h85.674666C128 554.666667 126.72-40.618667 128.341333-42.24zM804.565333 557.44h-183.722666c28.672 53.333333 43.008 96.128 43.008 128.384 0 85.546667-62.208 137.173333-114.133334 160.853333-34.645333 15.786667-67.114667 1.066667-79.146666-18.090666L292.693333 575.658667a42.837333 42.837333 0 0 0-35.029333-18.218667H213.674667a21.418667 21.418667 0 0 1-21.418667-21.418667v-557.269333c0-11.818667 9.6-21.418667 21.418667-21.418667H716.8a90.88 90.88 0 0 1 87.808 66.261334l127.829333 336.469333c29.354667 84.565333 0 197.376-127.829333 197.376z" horiz-adv-x="1024" />
<glyph glyph-name="icon_file-add_outlined" unicode="&#59274;" d="M213.333333 768h469.333334v-107.52a21.333333 21.333333 0 0 1 21.333333-21.333333H810.666667V341.333333h85.333333V686.293333a42.666667 42.666667 0 0 1-12.501333 30.165334l-124.330667 124.373333A42.666667 42.666667 0 0 1 729.002667 853.333333H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-853.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h341.333333v85.333333H213.333333V768zM746.666667 256a21.333333 21.333333 0 0 1-21.333334-21.333333V128h-106.666666a21.333333 21.333333 0 0 1-21.333334-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333334-21.333333H725.333333v-106.666667a21.333333 21.333333 0 0 1 21.333334-21.333333h42.666666a21.333333 21.333333 0 0 1 21.333334 21.333333V42.666667h106.666666a21.333333 21.333333 0 0 1 21.333334 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333334 21.333333H810.666667v106.666667a21.333333 21.333333 0 0 1-21.333334 21.333333h-42.666666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_thumbsup_outlined" unicode="&#59259;" d="M128.341333-42.282667H42.666667V557.354667h85.674666C128 554.666667 126.72-43.946667 128.341333-42.24zM620.8 557.44h183.722667c127.829333 0 157.184-112.810667 127.829333-197.333333l-127.829333-336.469334A90.88 90.88 0 0 0 716.8-42.666667H213.76a21.418667 21.418667 0 0 0-21.461333 21.418667V536.021333c0 11.818667 9.6 21.418667 21.418666 21.418667H257.706667c13.952 0 27.008 6.826667 35.029333 18.218667L470.613333 828.586667c12.032 19.2 44.501333 33.834667 79.146667 18.090666 51.925333-23.68 114.133333-75.306667 114.133333-160.853333 0-32.256-14.336-75.093333-43.008-128.384z m183.722667-85.674667h-326.997334l67.84 126.208c22.528 41.984 32.768 72.533333 32.768 87.850667 0 24.832-5.248 52.949333-49.834666 76.032L337.237333 489.984a42.837333 42.837333 0 0 0-35.029333-18.218667h-24.277333v-428.8H716.8c2.688 0 4.736 1.578667 5.248 3.413334l1.066667 3.882666 128.768 338.944c16.896 49.92 0 82.56-47.274667 82.56z" horiz-adv-x="1024" />
<glyph glyph-name="icon-product-forum_outlined" unicode="&#59260;" d="M725.333333 426.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h170.666667a42.666667 42.666667 0 0 0 42.666666-42.666666v-341.333334a42.666667 42.666667 0 0 0-42.666666-42.666666h-89.002667l-51.498667-51.498667a42.666667 42.666667 0 0 0-60.330666 0L686.336 42.666667H469.333333a42.666667 42.666667 0 0 0-42.666666 42.666666v170.666667a42.666667 42.666667 0 0 0 42.666666 42.666667h256v128z m85.333334-42.666667v-128a42.666667 42.666667 0 0 0-42.666667-42.666667h-256v-85.333333h192a42.666667 42.666667 0 0 0 30.165333-12.501333l33.834667-33.834667 33.834667 33.834667A42.666667 42.666667 0 0 0 832 128H896v256h-85.333333zM42.666667 768a42.666667 42.666667 0 0 0 42.666666 42.666667h682.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-512a42.666667 42.666667 0 0 0-42.666667-42.666667H380.330667l-72.832-72.832a42.666667 42.666667 0 0 0-60.330667 0L174.336 213.333333H85.333333a42.666667 42.666667 0 0 0-42.666666 42.666667V768z m85.333333-42.666667v-426.666666h64a42.666667 42.666667 0 0 0 30.165333-12.501334L277.333333 230.997333l55.168 55.168A42.666667 42.666667 0 0 0 362.666667 298.666667H725.333333V725.333333H128zM298.666667 512a42.666667 42.666667 0 1 0-85.333334 0 42.666667 42.666667 0 0 0 85.333334 0z m170.666666 0a42.666667 42.666667 0 1 0-85.333333 0 42.666667 42.666667 0 0 0 85.333333 0z m128-42.666667a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
<glyph glyph-name="icon_test-tracking_colorful" unicode="&#59261;" d="M172.714667 808.618667H298.666667V640a42.666667 42.666667 0 0 1 42.666666-42.666667h341.333334a42.666667 42.666667 0 0 1 42.666666 42.666667V808.618667h125.952c30.677333 0 44.714667-17.237333 44.714667-44.672v-804.565334c0-27.434667-14.037333-44.714667-44.714667-44.714666H172.714667C141.994667-85.333333 128-68.096 128-40.618667V763.946667c0 27.392 14.037333 44.672 44.714667 44.672zM321.024 426.666667a22.357333 22.357333 0 0 1-22.357333-22.357334v-44.714666c0-12.330667 9.984-22.314667 22.357333-22.314667h381.952c12.373333 0 22.357333 9.984 22.357333 22.314667v44.714666a22.357333 22.357333 0 0 1-22.357333 22.357334H321.024z m0-213.333334a22.357333 22.357333 0 0 1-22.357333-22.357333v-44.672c0-12.373333 9.984-22.357333 22.357333-22.357333h394.154667c12.373333 0 22.357333 9.984 22.357333 22.357333v44.672a22.357333 22.357333 0 0 1-22.357333 22.357333H321.024zM405.333333 853.333333a21.333333 21.333333 0 0 1-21.333333-21.333333v-128a21.333333 21.333333 0 0 1 21.333333-21.333333h213.333334a21.333333 21.333333 0 0 1 21.333333 21.333333v128a21.333333 21.333333 0 0 1-21.333333 21.333333h-213.333334z" horiz-adv-x="1024" />
<glyph glyph-name="icon_reply" unicode="&#59256;" d="M938.666667 768a42.666667 42.666667 0 0 0 42.666666-42.666667v-640a42.666667 42.666667 0 0 0-42.666666-42.666666H500.693333l-204.288-102.144a42.666667 42.666667 0 0 0-61.44 33.237333L234.666667-21.333333V42.666667H85.333333a42.666667 42.666667 0 0 0-42.368 37.674666L42.666667 85.333333V725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h853.333334z m-42.666667-85.333333H128v-554.666667h149.333333a42.666667 42.666667 0 0 0 42.368-37.674667L320 85.333333v-37.589333l151.594667 75.733333a42.666667 42.666667 0 0 0 14.122666 4.266667L490.666667 128H896V682.666667z m-170.666667-234.666667a42.666667 42.666667 0 0 0 0-85.333333H298.666667a42.666667 42.666667 0 0 0 0 85.333333h426.666666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_comment_collapse_text_input" unicode="&#59257;" d="M938.666667 768a42.666667 42.666667 0 0 0 42.666666-42.666667v-234.666666a42.666667 42.666667 0 0 0-85.333333 0V682.666667H128v-554.666667h106.666667a42.666667 42.666667 0 0 0 42.368-37.674667L277.333333 85.333333v-37.589333l151.594667 75.733333a42.666667 42.666667 0 0 0 14.122667 4.266667L448 128h96a42.666667 42.666667 0 0 0 0-85.333333h-85.973333l-204.288-102.144a42.666667 42.666667 0 0 0-61.44 33.237333L192-21.333333V42.666667H85.333333a42.666667 42.666667 0 0 0-42.368 37.674666L42.666667 85.333333V725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h853.333334z m-204.8-546.133333a42.666667 42.666667 0 0 0 42.666666-42.666667V42.666667a42.666667 42.666667 0 0 0-85.333333 0v93.866666H597.333333a42.666667 42.666667 0 0 0-42.368 37.674667L554.666667 179.2a42.666667 42.666667 0 0 0 42.666666 42.666667h136.533334z m68.266666 204.8a42.666667 42.666667 0 0 0 42.666667-42.666667v-93.866667H938.666667a42.666667 42.666667 0 0 0 42.368-37.674666L981.333333 247.466667a42.666667 42.666667 0 0 0-42.666666-42.666667h-136.533334a42.666667 42.666667 0 0 0-42.666666 42.666667V384a42.666667 42.666667 0 0 0 42.666666 42.666667zM512 469.333333a42.666667 42.666667 0 0 0 0-85.333333H256a42.666667 42.666667 0 0 0 0 85.333333h256zM384 597.333333a42.666667 42.666667 0 1 0 0-85.333333H256a42.666667 42.666667 0 1 0 0 85.333333h128z" horiz-adv-x="1024" />
<glyph glyph-name="icon_comment_expand_text_input" unicode="&#59258;" d="M938.666667 768a42.666667 42.666667 0 0 0 42.666666-42.666667v-234.666666a42.666667 42.666667 0 0 0-85.333333 0V682.666667H128v-554.666667h106.666667a42.666667 42.666667 0 0 0 42.368-37.674667L277.333333 85.333333v-37.589333l151.594667 75.733333a42.666667 42.666667 0 0 0 14.122667 4.266667L448 128h96a42.666667 42.666667 0 0 0 0-85.333333h-85.973333l-204.288-102.144a42.666667 42.666667 0 0 0-61.44 33.237333L192-21.333333V42.666667H85.333333a42.666667 42.666667 0 0 0-42.368 37.674666L42.666667 85.333333V725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h853.333334z m-298.666667-507.264a42.666667 42.666667 0 0 0 42.666667-42.666667v-90.026666l90.069333-0.042667a42.666667 42.666667 0 0 0 42.368-37.674667l0.298667-4.992a42.666667 42.666667 0 0 0-42.666667-42.666666H640a42.666667 42.666667 0 0 0-42.666667 42.666666v132.736a42.666667 42.666667 0 0 0 42.666667 42.666667zM938.666667 426.666667a42.666667 42.666667 0 0 0 42.666666-42.666667v-132.736a42.666667 42.666667 0 1 0-85.333333 0V341.333333h-90.069333a42.666667 42.666667 0 0 0-42.368 37.674667L763.264 384a42.666667 42.666667 0 0 0 42.666667 42.666667H938.666667z m-426.666667 42.666666a42.666667 42.666667 0 0 0 0-85.333333H256a42.666667 42.666667 0 0 0 0 85.333333h256zM384 597.333333a42.666667 42.666667 0 1 0 0-85.333333H256a42.666667 42.666667 0 1 0 0 85.333333h128z" horiz-adv-x="1024" />
<glyph glyph-name="icon_chart_graph" unicode="&#59255;" d="M661.333333 810.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-192a42.666667 42.666667 0 0 0-42.666667-42.666667H554.666667V426.666667h192a42.666667 42.666667 0 0 0 42.666666-42.666667v-149.333333H896a42.666667 42.666667 0 0 0 42.666667-42.666667V0a42.666667 42.666667 0 0 0-42.666667-42.666667h-298.666667a42.666667 42.666667 0 0 0-42.666666 42.666667v192a42.666667 42.666667 0 0 0 42.666666 42.666667h106.666667V341.333333h-384v-106.666666H426.666667a42.666667 42.666667 0 0 0 42.666666-42.666667V0a42.666667 42.666667 0 0 0-42.666666-42.666667H128a42.666667 42.666667 0 0 0-42.666667 42.666667v192a42.666667 42.666667 0 0 0 42.666667 42.666667h106.666667V384a42.666667 42.666667 0 0 0 42.666666 42.666667H469.333333V533.333333H362.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V768a42.666667 42.666667 0 0 0 42.666667 42.666667h298.666666zM384 149.333333H170.666667V42.666667h213.333333v106.666666z m469.333333 0h-213.333333V42.666667h213.333333v106.666666zM618.666667 725.333333h-213.333334v-106.666666h213.333334V725.333333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_share1" unicode="&#59254;" d="M746.666667 810.666667a149.333333 149.333333 0 1 0-106.453334-254.037334l-218.965333-132.608a149.461333 149.461333 0 0 0 1.024-76.117333l222.890667-131.712a149.333333 149.333333 0 1 0-43.434667-73.472l-222.890667 131.754667a149.333333 149.333333 0 1 0-2.986666 221.824l224.341333 135.893333A149.333333 149.333333 0 0 0 746.666667 810.666667z m0-640a64 64 0 1 1 0-128 64 64 0 0 1 0 128z m-469.333334 277.333333a64 64 0 1 1 0-128 64 64 0 0 1 0 128z m469.333334 277.333333a64 64 0 1 1 0-128 64 64 0 0 1 0 128z" horiz-adv-x="1024" />
<glyph glyph-name="icon_text" unicode="&#59253;" d="M576 810.666667l3.114667-0.128 1.322666-0.085334L576 810.666667c2.986667 0 5.888-0.298667 8.704-0.853334l0.725333-0.213333 4.778667-1.365333a42.538667 42.538667 0 0 0 17.237333-11.392l234.666667-256 0.682667-0.768c0.768-0.853333 1.493333-1.749333 2.133333-2.688l-2.816 3.413333a43.008 43.008 0 0 0 10.88-23.466667L853.333333 512v-512a42.666667 42.666667 0 0 0-42.666666-42.666667H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667V768a42.666667 42.666667 0 0 0 42.666666 42.666667h362.666667z m-42.666667-85.333334H256v-682.666666h512V469.333333h-192a42.666667 42.666667 0 0 0-42.368 37.674667L533.333333 512V725.333333zM597.333333 298.666667a42.666667 42.666667 0 1 0 0-85.333334H341.333333a42.666667 42.666667 0 0 0 0 85.333334h256z m-170.666666 170.666666a42.666667 42.666667 0 1 0 0-85.333333H341.333333a42.666667 42.666667 0 0 0 0 85.333333h85.333334z m192 188.970667V554.666667h95.018666L618.666667 658.304z" horiz-adv-x="1024" />
<glyph glyph-name="icon_click" unicode="&#59031;" d="M554.666667 853.333333a42.666667 42.666667 0 0 0 42.666666-42.666666v-170.666667a42.666667 42.666667 0 0 0-85.333333 0V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666z m-127.061334-439.296a46.208 46.208 0 0 0 54.357334 54.357334l462.208-92.416c40.362667-8.106667 50.773333-60.928 16.554666-83.797334l-91.733333-61.141333 98.773333-98.773333c16.64-16.682667 17.92-42.88 3.84-61.013334l-3.84-4.352-138.666666-138.666666a46.208 46.208 0 0 0-65.365334 0l-98.773333 98.773333-61.141333-91.733333c-21.802667-32.682667-70.954667-24.661333-82.432 11.264l-1.365334 5.290666-92.416 462.208z m104.192-49.834666l56.96-284.8 30.549334 45.781333a46.208 46.208 0 0 0 66.858666 10.88l4.266667-3.84 105.984-105.898667 73.258667 73.258667-105.941334 105.941333a46.208 46.208 0 0 0 2.474667 67.669334l4.608 3.498666 45.781333 30.549334-284.8 56.96z m288.768 348.16a44.202667 44.202667 0 0 0 62.506667-62.464l-125.013333-124.970667a44.202667 44.202667 0 0 0-62.464 62.506667l125.013333 124.970666z m-512-512a44.202667 44.202667 0 1 0 62.506667-62.464l-125.013333-124.970667a44.202667 44.202667 0 1 0-62.464 62.506667l125.013333 124.970666zM256 384a42.666667 42.666667 0 0 0 0-85.333333H85.333333a42.666667 42.666667 0 0 0 0 85.333333h170.666667zM183.594667 712.405333a44.202667 44.202667 0 0 0 62.506666 0l124.970667-125.013333a44.202667 44.202667 0 0 0-62.506667-62.464l-124.970666 125.013333a44.202667 44.202667 0 0 0 0 62.464z" horiz-adv-x="1024" />
<glyph glyph-name="icon_feedback_outlined" unicode="&#59251;" d="M840.832 840.832A42.666667 42.666667 0 0 0 853.333333 810.666667v-341.333334h-85.333333V768H170.666667v-768h213.333333v-85.333333H128a42.666667 42.666667 0 0 0-42.666667 42.666666V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h682.666667a42.666667 42.666667 0 0 0 30.165333-12.501333zM277.333333 640a21.333333 21.333333 0 0 1-21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333333-21.333333h384a21.333333 21.333333 0 0 1 21.333334 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333334 21.333333h-384zM868.138667 158.848l-120.32 120.32-264.448-266.112L469.333333-100.949333a21.333333 21.333333 0 0 1 21.333334-21.333334l114.176 18.133334 263.296 262.997333zM899.114667 370.602667c-16.64 16.64-42.368 17.92-57.472 2.901333l-63.786667-64 120.490667-120.490667 63.872 63.744 2.005333 2.133334c12.970667 15.317333 11.093333 39.509333-4.778667 55.424l-60.330666 60.288zM256 448a21.333333 21.333333 0 0 0 21.333333 21.333333h298.666667a21.333333 21.333333 0 0 0 21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333333-21.333333h-298.666667a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_done_outlined" unicode="&#59252;" d="M840.832 840.832A42.666667 42.666667 0 0 0 853.333333 810.666667v-341.333334h-85.333333V768H170.666667v-768h213.333333v-85.333333H128a42.666667 42.666667 0 0 0-42.666667 42.666666V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h682.666667a42.666667 42.666667 0 0 0 30.165333-12.501333zM277.333333 640a21.333333 21.333333 0 0 1-21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333333-21.333333h384a21.333333 21.333333 0 0 1 21.333334 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333334 21.333333h-384zM256 448a21.333333 21.333333 0 0 0 21.333333 21.333333h298.666667a21.333333 21.333333 0 0 0 21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333333-21.333333h-298.666667a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667zM859.648 235.264l30.165333-30.165333a21.333333 21.333333 0 0 0 0-30.165334l-211.2-211.2a21.205333 21.205333 0 0 0-11.306666-5.930666l-2.474667-0.298667h-2.56a21.248 21.248 0 0 0-13.824 6.229333l-120.661333 120.661334a21.333333 21.333333 0 0 0 0 30.165333l30.165333 30.165333a21.333333 21.333333 0 0 0 30.165333 0l75.434667-75.392 165.973333 165.930667a21.333333 21.333333 0 0 0 30.122667 0z" horiz-adv-x="1024" />
<glyph glyph-name="icon_dataset_outlined" unicode="&#59250;" d="M42.666667 569.856a42.666667 42.666667 0 0 0 25.002666 38.826667l426.666667 193.962666a42.666667 42.666667 0 0 0 35.328 0l426.666667-193.92a42.666667 42.666667 0 0 0 25.002666-38.826666v-415.530667a42.666667 42.666667 0 0 0-23.594666-38.144l-426.666667-213.333333a42.666667 42.666667 0 0 0-38.144 0l-426.666667 213.333333A42.666667 42.666667 0 0 0 42.666667 154.368V569.856z m777.301333 7.082667L512 716.928 202.368 576.213333l307.925333-132.693333 309.674667 133.418667zM554.666667 369.749333v-359.68l341.333333 170.666667V516.778667l-341.333333-147.029334zM128 515.328v-334.592l341.333333-170.666667v358.229334L128 515.328z" horiz-adv-x="1024" />
<glyph glyph-name="icon_outer-borders_outlined" unicode="&#59249;" d="M910.208 736.725333c0 25.130667-20.352 45.482667-45.482667 45.482667H159.274667c-25.130667 0-45.525333-20.352-45.525334-45.482667v-705.450666c0-25.130667 20.394667-45.482667 45.525334-45.482667h705.450666c25.130667 0 45.482667 20.352 45.482667 45.482667V736.725333z m-711.082667-665.6V696.874667h625.749334v-625.749334H199.125333zM492.074667 663.722667a22.741333 22.741333 0 0 1-22.741334-22.784v-39.808c0-12.586667 10.197333-22.741333 22.741334-22.741334h39.850666A22.741333 22.741333 0 0 1 554.666667 601.130667v39.808a22.741333 22.741333 0 0 1-22.741334 22.784h-39.850666zM469.333333 522.410667c0 12.586667 10.197333 22.784 22.741334 22.784h39.850666c12.544 0 22.741333-10.197333 22.741334-22.784v-39.808a22.741333 22.741333 0 0 0-22.741334-22.741334h-39.850666A22.741333 22.741333 0 0 0 469.333333 482.56v39.808z m0-118.485334c0 12.544 10.197333 22.741333 22.741334 22.741334h39.850666c12.544 0 22.741333-10.197333 22.741334-22.741334v-39.850666a22.741333 22.741333 0 0 0-22.741334-22.741334h-39.850666a22.741333 22.741333 0 0 0-22.741334 22.741334v39.850666z m0-118.528c0 12.544 10.197333 22.741333 22.741334 22.741334h39.850666c12.544 0 22.741333-10.197333 22.741334-22.741334v-39.808a22.741333 22.741333 0 0 0-22.741334-22.784h-39.850666a22.741333 22.741333 0 0 0-22.741334 22.784v39.808z m22.741334-95.744a22.741333 22.741333 0 0 1-22.741334-22.784v-39.808c0-12.586667 10.197333-22.784 22.741334-22.784h39.850666a22.741333 22.741333 0 0 1 22.741334 22.784v39.808a22.741333 22.741333 0 0 1-22.741334 22.784h-39.850666z m214.272 214.272c0 12.544 10.24 22.741333 22.784 22.741334h39.808c12.586667 0 22.741333-10.197333 22.741333-22.741334v-39.850666a22.741333 22.741333 0 0 0-22.741333-22.741334h-39.808a22.741333 22.741333 0 0 0-22.784 22.741334v39.850666zM610.602667 426.666667a22.741333 22.741333 0 0 1-22.741334-22.741334v-39.850666c0-12.544 10.154667-22.741333 22.741334-22.741334h39.808a22.741333 22.741333 0 0 1 22.784 22.741334v39.850666a22.741333 22.741333 0 0 1-22.784 22.741334h-39.808z m-259.797334-22.741334c0 12.544 10.197333 22.741333 22.741334 22.741334h39.850666c12.544 0 22.741333-10.197333 22.741334-22.741334v-39.850666A22.741333 22.741333 0 0 0 413.44 341.333333h-39.850667a22.741333 22.741333 0 0 0-22.741333 22.741334v39.850666zM255.061333 426.666667a22.741333 22.741333 0 0 1-22.784-22.741334v-39.850666c0-12.544 10.24-22.741333 22.784-22.741334h39.808a22.741333 22.741333 0 0 1 22.741334 22.741334v39.850666A22.741333 22.741333 0 0 1 294.869333 426.666667h-39.808z" horiz-adv-x="1024" />
<glyph glyph-name="icon_checkbox" unicode="&#59246;" d="M640 810.666667a42.666667 42.666667 0 0 0 0-85.333334H192a21.333333 21.333333 0 0 1-21.333333-21.333333v-640a21.333333 21.333333 0 0 1 21.333333-21.333333h640a21.333333 21.333333 0 0 1 21.333333 21.333333V469.333333a42.666667 42.666667 0 0 0 85.333334 0v-405.333333a106.666667 106.666667 0 0 0-106.666667-106.666667h-640A106.666667 106.666667 0 0 0 85.333333 64v640A106.666667 106.666667 0 0 0 192 810.666667H640z m259.456-29.269334a42.666667 42.666667 0 0 0 9.941333-59.52l-320-448a42.666667 42.666667 0 0 0-61.397333-8.533333l-213.333333 170.666667a42.666667 42.666667 0 1 0 53.333333 66.645333l178.090667-142.506667 293.845333 411.306667a42.666667 42.666667 0 0 0 59.52 9.941333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_radio" unicode="&#59247;" d="M512 853.333333c259.2 0 469.333333-210.133333 469.333333-469.333333s-210.133333-469.333333-469.333333-469.333333S42.666667 124.8 42.666667 384 252.8 853.333333 512 853.333333z m0-85.333333a384 384 0 1 1 0-768 384 384 0 0 1 0 768z m0-170.666667a213.333333 213.333333 0 1 0 0-426.666666 213.333333 213.333333 0 0 0 0 426.666666z m0-85.333333a128 128 0 1 1 0-256 128 128 0 0 1 0 256z" horiz-adv-x="1024" />
@ -374,7 +438,7 @@
<glyph glyph-name="icon_file-sketch_colorful" unicode="&#59138;" d="M170.666667 789.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h417.834667a21.333333 21.333333 0 0 0 15.061333-6.229333l200.874667-200.874667a21.333333 21.333333 0 0 0 6.229333-15.061333V-21.333333a42.666667 42.666667 0 0 0-42.666666-42.666667H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v810.666666zM170.666667 789.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h417.834667a21.333333 21.333333 0 0 0 15.061333-6.229333l200.874667-200.874667a21.333333 21.333333 0 0 0 6.229333-15.061333V-21.333333a42.666667 42.666667 0 0 0-42.666666-42.666667H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v810.666666zM640 830.08a21.290667 21.290667 0 0 0 6.229333-4.309333l200.874667-200.874667a21.376 21.376 0 0 0 4.309333-6.229333H682.666667a42.666667 42.666667 0 0 0-42.666667 42.666666V830.08zM428.202667 426.666667a31.018667 31.018667 0 0 1-24.618667-12.16l-55.893333-73.045334a31.018667 31.018667 0 0 1 0.426666-38.186666l139.605334-175.232a31.018667 31.018667 0 0 1 48.554666 0l139.605334 175.232a31.018667 31.018667 0 0 1 0.426666 38.186666l-55.893333 73.045334a31.018667 31.018667 0 0 1-24.618667 12.16h-167.594666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_block_filled" unicode="&#59115;" d="M512-85.333333c259.2 0 469.333333 210.133333 469.333333 469.333333S771.2 853.333333 512 853.333333 42.666667 643.2 42.666667 384s210.133333-469.333333 469.333333-469.333333zM298.666667 405.333333a21.333333 21.333333 0 0 0 21.333333 21.333334h384a21.333333 21.333333 0 0 0 21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 0-21.333333-21.333334h-384a21.333333 21.333333 0 0 0-21.333333 21.333334v42.666666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_block_filled" unicode="&#59115;" d="M981.333333 384c0-259.2-210.133333-469.333333-469.333333-469.333333S42.666667 124.79999999999995 42.666667 384 252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333zM320 426.666667a21.333333 21.333333 0 0 1-21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 1 21.333333-21.333334h384a21.333333 21.333333 0 0 1 21.333333 21.333334v42.666666a21.333333 21.333333 0 0 1-21.333333 21.333334h-384z" horiz-adv-x="1024" />
<glyph glyph-name="icon_not-started_filled" unicode="&#59124;" d="M981.333333 384c0-259.2-210.133333-469.333333-469.333333-469.333333S42.666667 124.79999999999995 42.666667 384 252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333z m-213.333333 64h-85.333333a21.248 21.248 0 0 1-21.333334-21.333333v-85.333334a21.333333 21.333333 0 0 1 21.333334-21.333333h85.333333a21.248 21.248 0 0 1 21.333333 21.333333v85.333334a21.248 21.248 0 0 1-21.333333 21.333333z m-426.666667 0H256a21.290667 21.290667 0 0 1-21.333333-21.333333v-85.333334a21.333333 21.333333 0 0 1 21.333333-21.333333h85.333333a21.333333 21.333333 0 0 1 21.333334 21.333333v85.333334a21.333333 21.333333 0 0 1-21.333334 21.333333z m221.482667-1.621333A21.248 21.248 0 0 1 554.666667 448h-85.333334a21.248 21.248 0 0 1-21.333333-21.333333v-85.333334a21.333333 21.333333 0 0 1 21.333333-21.333333h85.333334a21.248 21.248 0 0 1 21.333333 21.333333v85.333334a21.248 21.248 0 0 1-13.184 19.712z" horiz-adv-x="1024" />
@ -472,8 +536,6 @@
<glyph glyph-name="icon_new-item_outlined" unicode="&#59029;" d="M896 810.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-768a42.666667 42.666667 0 0 0-42.666667-42.666667H128a42.666667 42.666667 0 0 0-42.666667 42.666667V768a42.666667 42.666667 0 0 0 42.666667 42.666667h768z m-64-85.333334h-640a21.333333 21.333333 0 0 1-21.205333-18.816L170.666667 704v-640a21.333333 21.333333 0 0 1 18.816-21.162667L192 42.66666699999996h640a21.333333 21.333333 0 0 1 21.205333 18.858666L853.333333 64v640a21.333333 21.333333 0 0 1-21.333333 21.333333z m-298.666667-128a21.333333 21.333333 0 0 0 21.333334-21.333333V426.666667h149.333333a21.333333 21.333333 0 0 0 21.333333-21.333334v-42.666666a21.248 21.248 0 0 0-21.333333-21.333334H554.666667v-149.333333a21.248 21.248 0 0 0-21.333334-21.333333h-42.666666a21.333333 21.333333 0 0 0-21.333334 21.333333V341.33333300000004H320a21.333333 21.333333 0 0 0-21.333333 21.333334v42.666666a21.333333 21.333333 0 0 0 21.333333 21.333334H469.333333V576a21.290667 21.290667 0 0 0 21.333334 21.333333h42.666666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_logs_outlined-1" unicode="&#59031;" d="M682.666667 768H213.333333v-768h597.333334V639.146667h-106.666667a21.333333 21.333333 0 0 0-21.333333 21.333333V768zM170.666667 853.333333h558.293333a42.666667 42.666667 0 0 0 30.208-12.501333l124.373333-124.373333a42.666667 42.666667 0 0 0 12.458667-30.165334V-42.666667a42.666667 42.666667 0 0 0-42.666667-42.666666H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666666V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666z m170.666666-384h341.333334a21.333333 21.333333 0 0 0 21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333333-21.333333H341.333333a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667A21.333333 21.333333 0 0 0 341.333333 469.333333z m0-213.333333h192a21.248 21.248 0 0 0 21.333334-21.333333v-42.666667a21.248 21.248 0 0 0-21.333334-21.333333H341.333333a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667A21.333333 21.333333 0 0 0 341.333333 256z" horiz-adv-x="1024" />
<glyph glyph-name="icon_refresh_outlined" unicode="&#59032;" d="M757.12 554.666667a298.666667 298.666667 0 1 1 41.173333-256h88.192A384 384 0 1 0 810.666667 625.365333V746.666667a21.333333 21.333333 0 0 0 21.333333 21.333333h42.666667a21.333333 21.333333 0 0 0 21.333333-21.333333V512a42.666667 42.666667 0 0 0-42.666667-42.666667h-234.666666a21.333333 21.333333 0 0 0-21.333334 21.333334v42.666666a21.333333 21.333333 0 0 0 21.333334 21.333334h138.453333z" horiz-adv-x="1024" />
<glyph glyph-name="icon_font-color_outlined" unicode="&#59033;" d="M128 19.925333c0 12.544 9.813333 22.741333 21.930667 22.741334h724.138666c12.117333 0 21.930667-10.197333 21.930667-22.741334v-125.184c0-12.544-9.813333-22.741333-21.930667-22.741333H149.930667A22.357333 22.357333 0 0 0 128-105.258667v125.184zM462.634667 768a21.333333 21.333333 0 0 1-19.882667-13.653333L224.512 187.733333a21.333333 21.333333 0 0 1 19.882667-29.013333H297.813333a21.333333 21.333333 0 0 1 19.882667 13.525333l61.013333 155.562667A21.333333 21.333333 0 0 0 398.549333 341.333333h211.114667a21.333333 21.333333 0 0 0 20.138667-14.293333l53.845333-154.026667a21.333333 21.333333 0 0 1 20.138667-14.293333h53.632a21.333333 21.333333 0 0 1 19.882666 29.013333L559.061333 754.346667a21.333333 21.333333 0 0 1-19.882666 13.653333h-76.544z m-52.394667-341.333333l88.362667 239.914666a2.432 2.432 0 0 0 4.608 0L590.848 426.666667H410.24z" horiz-adv-x="1024" />
@ -482,7 +544,7 @@
<glyph glyph-name="icon_moments-categories_outlined" unicode="&#59036;" d="M523.477333 782.08l429.568-273.408a21.333333 21.333333 0 0 0 0-36.010667L523.52 199.296a21.333333 21.333333 0 0 0-22.912 0L70.954667 472.661333a21.333333 21.333333 0 0 0 0 36.010667l429.610666 273.365333a21.333333 21.333333 0 0 0 22.912 0zM201.6 490.666667L512 293.12l310.4 197.546667L512 688.213333 201.6 490.666667zM110.805333 303.530667a21.333333 21.333333 0 0 1-29.354666-7.04l-22.314667-36.394667a21.333333 21.333333 0 0 1 7.04-29.312l390.613333-239.530667a84.992 84.992 0 0 1 89.088 0l390.613334 239.530667a21.333333 21.333333 0 0 1 7.04 29.312l-22.314667 36.394667a21.333333 21.333333 0 0 1-29.312 7.04L506.88 67.413333a10.666667 10.666667 0 0 0-11.136 0l-384.981333 236.074667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_doc-replace_outlined" unicode="&#59037;" d="M85.333333-1.066667a45.653333 45.653333 0 0 1 45.482667-45.738666h295.68v85.76H170.581333V724.906667H767.573333v-300.202667h85.333334V764.928A45.653333 45.653333 0 0 1 807.338667 810.666667H130.816A45.653333 45.653333 0 0 1 85.333333 764.928v-765.994667zM255.914667 585.002667c0 6.357333 5.077333 11.477333 11.349333 11.477333h403.669333c6.272 0 11.392-5.12 11.392-11.477333v-62.890667c0-6.314667-5.12-11.434667-11.392-11.434667h-403.626666a11.392 11.392 0 0 0-11.392 11.434667V585.002667zM267.264 424.704a11.392 11.392 0 0 1-11.349333-11.434667v-62.890666c0-6.314667 5.077333-11.434667 11.349333-11.434667h173.397333c6.314667 0 11.392 5.12 11.392 11.434667v62.890666a11.392 11.392 0 0 1-11.392 11.434667H267.264zM891.52 170.752h-349.738667a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667a21.333333 21.333333 0 0 0 21.333333 21.333333h192v58.752a21.333333 21.333333 0 0 0 35.626667 15.872l136.405333-122.794666a21.333333 21.333333 0 0 0-14.293333-37.162667zM691.114667 0.085333v-58.794666a21.333333 21.333333 0 0 0-35.626667-15.829334l-136.405333 122.752a21.333333 21.333333 0 0 0 14.293333 37.205334h349.738667a21.333333 21.333333 0 0 0 21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 0-21.333333-21.333334h-192z" horiz-adv-x="1024" />
<glyph glyph-name="icon_doc-replace_outlined" unicode="&#59037;" d="M891.52 170.75199999999995h-349.738667a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667a21.333333 21.333333 0 0 0 21.333333 21.333333h192v58.752a21.333333 21.333333 0 0 0 35.626667 15.872l136.405333-122.794666a21.333333 21.333333 0 0 0-14.293333-37.162667zM691.114667 0.08533299999999144v-58.794666a21.333333 21.333333 0 0 0-35.626667-15.829334l-136.405333 122.752a21.333333 21.333333 0 0 0 14.293333 37.205334h349.738667a21.333333 21.333333 0 0 0 21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 0-21.333333-21.333334h-192zM840.832 840.832A42.666667 42.666667 0 0 0 853.333333 810.666667v-341.333334h-85.333333V768H170.666667v-768h213.333333v-85.333333H128a42.666667 42.666667 0 0 0-42.666667 42.666666V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h682.666667a42.666667 42.666667 0 0 0 30.165333-12.501333zM277.333333 640a21.333333 21.333333 0 0 1-21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333333-21.333333h384a21.333333 21.333333 0 0 1 21.333334 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333334 21.333333h-384zM256 448a21.333333 21.333333 0 0 0 21.333333 21.333333h298.666667a21.333333 21.333333 0 0 0 21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333333-21.333333h-298.666667a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_italic_outlined" unicode="&#59038;" d="M341.333333 746.666667a21.333333 21.333333 0 0 0 21.333334 21.333333h469.333333a21.333333 21.333333 0 0 0 21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333333-21.333333H640l-170.666667-597.333334h192a21.333333 21.333333 0 0 0 21.333334-21.333333v-42.666667a21.333333 21.333333 0 0 0-21.333334-21.333333h-469.333333a21.333333 21.333333 0 0 0-21.333333 21.333333v42.666667a21.333333 21.333333 0 0 0 21.333333 21.333333H384l170.666667 597.333334H362.666667a21.333333 21.333333 0 0 0-21.333334 21.333333v42.666667z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 413 KiB

View File

@ -6,7 +6,7 @@
{{ t('msBatchModal.batchModalSubTitle', { count: (props.selectData || []).length }) }}
</div>
</template>
<a-spin :loading="loading">
<a-spin :loading="loading" class="w-full">
<a-alert v-if="props.action === 'batchAddProject'" class="mb-[16px]">
{{ t('msBatchModal.batchModalTip') }}
</a-alert>
@ -19,8 +19,9 @@
title: 'name',
children: 'children',
disabled: 'disabled',
isLeaf: 'isLeaf',
}"
height="370px"
show-search
/>
</a-spin>
<template #footer>

View File

@ -135,7 +135,7 @@ export const MULTIPLE_INPUT = {
title: '',
value: [],
props: {
placeholder: t('formCreate.PleaseSelect'),
placeholder: t('formCreate.PleaseEnter'),
},
};

View File

@ -152,4 +152,3 @@
</script>
<style scoped></style>
@/store/modules/form-create/form-create

View File

@ -116,7 +116,7 @@
}
);
const emits = defineEmits<{
(e: 'confirm', isPass: boolean): void;
(e: 'confirm', formValue?: { field: string }, cancel?: () => void): void;
(e: 'cancel'): void;
(e: 'update:visible', visible: boolean): void;
}>();
@ -131,6 +131,11 @@
isPass.value = isValidatePass;
};
//
const form = ref({
field: props.fieldConfig?.field || '',
});
//
const validateForm = async () => {
await formRef.value?.validate((errors) => {
@ -142,16 +147,6 @@
});
};
const handleConfirm = async () => {
await validateForm();
emits('confirm', isPass.value);
};
//
const form = ref({
field: props.fieldConfig?.field || '',
});
//
const reset = () => {
form.value.field = '';
@ -164,6 +159,14 @@
reset();
};
const handleConfirm = async () => {
await validateForm();
if (props.isDelete) {
emits('confirm');
} else {
emits('confirm', form.value, handleCancel);
}
};
//
const titleClass = computed(() => {
return props.isDelete
@ -208,6 +211,7 @@
defineExpose({
form,
isPass,
});
</script>

View File

@ -595,4 +595,9 @@
:deep(.arco-table .arco-table-expand-btn:hover) {
border-color: transparent;
}
:deep(.arco-table-drag-handle) {
.arco-icon-drag-dot-vertical {
color: var(--color-text-brand);
}
}
</style>

View File

@ -4,7 +4,7 @@
<template v-for="(element, idx) in baseAction" :key="element.label">
<a-divider v-if="element.isDivider" class="divider mx-0 my-[6px]" />
<a-button
v-else
v-if="!element.isDivider && !element.children"
class="ml-[12px]"
:class="{
'arco-btn-outline--danger': element.danger,
@ -14,6 +14,28 @@
@click="handleSelect(element)"
>{{ t(element.label as string) }}</a-button
>
<!-- baseAction多菜单选择 -->
<a-dropdown v-if="!element.isDivider && element.children" position="tr" @select="handleSelect">
<a-button
class="ml-[12px]"
:class="{
'arco-btn-outline--danger': element.danger,
'ml-[16px]': idx === 0,
}"
type="outline"
@click="handleSelect"
>{{ t(element.label as string) }}</a-button
>
<template #content>
<template v-for="item in element.children" :key="item.label">
<a-divider v-if="element.isDivider" margin="4px" />
<a-doption v-else :value="item" :class="{ delete: item.danger }">
{{ t(item.label as string) }}
</a-doption>
</template>
</template>
</a-dropdown>
<!-- baseAction多菜单选择 -->
</template>
<div v-if="moreAction?.length" class="drop-down relative ml-[16px] inline-block">
<a-dropdown position="tr" @select="handleSelect">

View File

@ -117,6 +117,7 @@ export interface BatchActionParams {
eventTag?: string;
isDivider?: boolean;
danger?: boolean;
children?: BatchActionParams[];
}
export interface BatchActionConfig {
baseAction: BatchActionParams[];

View File

@ -32,7 +32,7 @@
const attrs = useAttrs();
const filterTagList = computed(() => {
return props.tagList.filter((item: any) => item) || [];
return (props.tagList || []).filter((item: any) => item) || [];
});
const showTagList = computed(() => {

View File

@ -1,5 +1,5 @@
<template>
<div class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white">
<div v-if="props.mode === 'remote'" class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white">
<a-radio-group v-model:model-value="fileListTab" type="button" size="small">
<a-radio value="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio>
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio>
@ -105,15 +105,21 @@
import { getFileEnum, getFileIcon } from './iconMap';
import type { MsFileItem } from './types';
const props = defineProps<{
const props = withDefaults(
defineProps<{
mode?: 'static' | 'remote'; // |
fileList: MsFileItem[];
uploadFunc: (params: any) => Promise<any>; //
uploadFunc?: (params: any) => Promise<any>; //
requestParams?: Record<string, any>; //
route?: string; //
routeQuery?: Record<string, string>; //
handleDelete?: (item: MsFileItem) => void;
handleReupload?: (item: MsFileItem) => void;
}>();
}>(),
{
mode: 'remote',
}
);
const emit = defineEmits<{
(e: 'update:fileList', fileList: MsFileItem[]): void;
(e: 'delete', item: MsFileItem): void;
@ -179,9 +185,11 @@
*/
function startUpload() {
emit('start');
if (props.mode === 'remote' && props.uploadFunc) {
asyncTaskStore.setUploadFunc(props.uploadFunc, props.requestParams);
asyncTaskStore.startUpload(innerFileList.value, props.route, props.routeQuery);
}
}
/**
* 后台上传

View File

@ -1,5 +1,6 @@
<template>
<a-upload
v-if="showDropArea"
v-bind="{ ...props }"
v-model:file-list="fileList"
:accept="
@ -9,6 +10,10 @@
"
:multiple="props.multiple"
:disabled="props.disabled"
:class="getAllScreenClass"
:style="{
width: props.isAllScreen ? `calc(100% - ${menuWidth}px - 16px)` : '100%',
}"
@change="handleChange"
@before-upload="beforeUpload"
>
@ -80,6 +85,7 @@
sizeUnit: 'MB' | 'KB'; //
isLimit: boolean; //
draggable: boolean; //
isAllScreen?: boolean; //
}> & {
accept: UploadType;
fileList: MsFileItem[];
@ -88,7 +94,9 @@
const props = withDefaults(defineProps<UploadProps>(), {
showSubText: true,
isLimit: true,
isAllScreen: false,
});
const emit = defineEmits(['update:fileList', 'change']);
const defaultMaxSize = 50;
@ -126,16 +134,97 @@
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
emit('change', _fileList, fileItem);
}
const total = ref(''); //
const other = ref(''); //
const showDropArea = ref(false);
watch(
() => props.isAllScreen,
(val) => {
if (val) {
total.value = '100vh';
other.value = '110px';
showDropArea.value = false;
} else {
total.value = '154px';
other.value = '0px';
showDropArea.value = true;
}
},
{ immediate: true }
);
const getAllScreenClass = computed(() => {
return props.isAllScreen ? ['!fixed', 'right-[16px]', '-bottom-[10px]', 'z-[999]', 'opacity-90'] : [];
});
//
function disableDefaultEvents() {
const doc = document.documentElement;
doc.addEventListener('dragleave', (e) => e.preventDefault()); //
doc.addEventListener('drop', (e) => e.preventDefault()); //
doc.addEventListener('dragenter', (e) => e.preventDefault()); //
doc.addEventListener('dragover', (e) => e.preventDefault()); //
}
const menuWidth = ref<number>();
const resizeObserver = ref();
const targetElement = ref();
function init() {
const ele = document.querySelector('body');
targetElement.value = document.querySelector('.menu-wrapper');
resizeObserver.value = new ResizeObserver((entries) => {
entries.forEach((item) => {
menuWidth.value = item.contentRect.width;
});
});
resizeObserver.value.observe(targetElement.value);
menuWidth.value = targetElement.value.getBoundingClientRect().width;
if (ele) {
ele.addEventListener('dragenter', () => {
showDropArea.value = true;
});
//
ele.addEventListener('dragleave', (e: any) => {
if (
e.target.nodeName === 'HTML' ||
e.target === e.explicitOriginalTarget ||
(!e.fromElement &&
(e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight))
) {
showDropArea.value = false;
}
});
//
ele.addEventListener('drop', (e) => {
showDropArea.value = false;
e.preventDefault();
});
}
}
onMounted(() => {
disableDefaultEvents();
init();
});
onBeforeUnmount(() => {
resizeObserver.value.disconnect();
});
</script>
<style lang="less" scoped>
.ms-upload-area {
@apply flex w-full flex-col items-center justify-center;
height: 154px;
height: calc(v-bind(total) - v-bind(other));
border: 1px dashed var(--color-text-input-border);
border-color: rgb(var(--primary-5)) !important;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
@apply flex flex-col items-center justify-center;
.ms-upload-icon-box {
@apply rounded-full bg-white;

View File

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

View File

@ -337,4 +337,34 @@ export const pathMap: PathMapItem[] = [
},
],
},
{
key: 'FEATURE_TEST', // 功能测试
locale: 'menu.featureTest',
route: RouteEnum.FEATURE_TEST,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'FEATURE_TEST_CASE', // 功能测试-功能用例
locale: 'menu.featureTest.featureCase',
route: RouteEnum.FEATURE_TEST_CASE,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'FEATURE_TEST_CASE_DETAIL', // 功能测试-功能用例
locale: 'menu.featureTest.featureCaseDetail',
route: RouteEnum.FEATURE_TEST_CASE_DETAIL,
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'FEATURE_TEST_CASE_RECYCLE', // 功能测试-功能用例-回收站
locale: 'menu.featureTest.featureCaseRecycle',
route: RouteEnum.FEATURE_TEST_CASE_RECYCLE,
permission: [],
level: MENU_LEVEL[2],
},
],
},
];

View File

@ -0,0 +1,14 @@
export enum StatusType {
UN_REVIEWED = 'icon-icon_block_filled', // 未评审
UNDER_REVIEWED = 'icon-icon_testing', // 评审中
PASS = 'icon-icon_succeed_colorful', // 已通过
UN_PASS = 'icon-icon_close_colorful', // 未通过
RE_REVIEWED = 'icon-icon_resubmit_filled', // 重新提审
UN_EXECUTED = 'icon-icon_block_filled', // 未执行
PASSED = 'icon-icon_succeed_colorful', // 已执行
FAILED = 'icon-icon_close_colorful', // 失败
BLOCKED = 'icon-icon_block_filled', // 阻塞
SKIPPED = 'icon-icon_skip_planarity', // 跳过
}
export default {};

View File

@ -5,6 +5,10 @@ export enum FormCreateKeyEnum {
PROJECT_DEFECT_SYNC_TEMPLATE = 'ProjectDefectSyncTemplate',
// 关联需求
PROJECT__RELATED_TEMPLATE = 'ProjectRelatedTemplate',
// 功能用例自定义字段
CASE_MANAGEMENT_FIELD = 'caseManagementFields',
// 自定义属性
CASE_CUSTOM_ATTRS = 'caseCustomAttributes',
}
export default {};

View File

@ -9,6 +9,8 @@ export enum BugManagementRouteEnum {
export enum FeatureTestRouteEnum {
FEATURE_TEST = 'featureTest',
FEATURE_TEST_CASE = 'featureTestCase',
FEATURE_TEST_CASE_RECYCLE = 'featureTestCaseRecycle',
FEATURE_TEST_CASE_DETAIL = 'featureTestCaseDetail',
}
export enum PerformanceTestRouteEnum {

View File

@ -25,9 +25,13 @@ export enum TableKeyEnum {
ORGANIZATION_PROJECT_USER_DRAWER = 'organizationProjectUserDrawer',
FILE_MANAGEMENT_FILE = 'fileManagementFile',
FILE_MANAGEMENT_CASE = 'fileManagementCase',
FILE_MANAGEMENT_CASE_RECYCLE = 'fileManagementCaseRecycle',
FILE_MANAGEMENT_VERSION = 'fileManagementVersion',
PROJECT_MANAGEMENT_MENU_FALSE_ALERT = 'projectManagementMenuFalseAlert',
ORGANIZATION_TEMPLATE_DEFECT_TABLE = 'organizationTemplateManagementDefect',
CASE_MANAGEMENT_TABLE = 'caseManagement',
CASE_MANAGEMENT_DETAIL_TABLE = 'caseManagementDetailTable',
CASE_MANAGEMENT_ASSOCIATED_TABLE = 'caseManagementAssociatedFileTable',
BUG_MANAGEMENT = 'bugManagement',
}

View File

@ -35,6 +35,9 @@ export default {
'menu.projectManagement.messageManagement': '消息管理',
'menu.projectManagement.messageManagementEdit': '更新模板',
'menu.featureTest.featureCase': '功能用例',
'menu.featureTest.featureCaseRecycle': '回收站',
'menu.featureTest.featureCaseList': '用例列表',
'menu.featureTest.featureCaseDetail': '创建用例',
'menu.projectManagement.projectPermission': '项目与权限',
'menu.settings': '系统设置',
'menu.settings.system': '系统',

View File

@ -0,0 +1,184 @@
import { TableQueryParams } from '@/models/common';
import { StatusType } from '@/enums/caseEnum';
export interface ModulesTreeType {
id: string;
name: string;
type: string;
parentId: string;
children: ModulesTreeType[];
attachInfo: Record<string, any>; // 附加信息
count: number; // 节点资源数量(多数情况下不会随着节点信息返回,视接口而定)
}
// 创建模块
export interface CreateOrUpdateModule {
projectId: string;
name: string;
parentId: string;
}
// 更新模块
export interface UpdateModule {
id: string;
name: string;
}
// 移动模块树
export interface MoveModules {
dragNodeId: string; // 被拖拽的节点
dropNodeId: string; // 放入的节点
dropPosition: number; // 放入的位置(取值:-1,0,1。 -1dropNodeId节点之前。 0:dropNodeId节点内。 1dropNodeId节点后
}
export interface customFieldsItem {
caseId?: string; // 用例id
fieldId: string;
value: string;
}
// 功能用例表
export interface CaseManagementTable {
id: string;
num: number;
moduleId: string; // 模块ID
projectId: string;
templateId: string; // 模板ID
name: string; // 名称
reviewStatus: StatusType[keyof StatusType]; // 评审状态:未评审/评审中/通过/不通过/重新提审
tags: any; // 标签JSON)
caseEditType: string; // 编辑模式:步骤模式/文本模式
pos: number; // 自定义排序间隔5000
versionId: string; // 版本ID
refId: string; // 指向初始版本ID
lastExecuteResult: string; // 最近的执行结果:未执行/通过/失败/阻塞/跳过
deleted: true; // 是否在回收站0-否1-是
publicCase: true; // 是否是公共用例0-否1-是
latest: true; // 是否为最新版本0-否1-是
createUser: string;
updateUser: string;
deleteUser: string;
createTime: string;
updateTime: string;
deleteTime: string;
customFields: customFieldsItem[]; // 自定义字段集合
}
// 选择类型步骤和预期结果列表
export interface StepList {
id: string;
step: string; // 步骤
expected: string; // 预期
showStep: boolean; // 编辑步骤模式
showExpected: boolean; // 编辑预期模式
}
// 关联文件列表
export interface AssociatedList {
id: string;
name: string;
fileType: string; // 文件类型
projectId: string;
tags: any;
description: string;
moduleName: string; // 模块名称
moduleId: string;
createUser: string;
createTime: number | string;
updateUser: string;
updateTime: number;
storage: string;
size: number;
enable: true;
refId: string;
filePath: string; // 文件路径
[key: string]: any;
}
export interface CreateCaseType {
request: CaseManagementTable;
files: File[];
}
export interface FileListQueryParams extends TableQueryParams {
moduleIds: string[];
versionId: string;
projectId: string;
name: string;
}
export interface DeleteCaseType {
id: string;
deleteAll?: boolean;
projectId: string;
}
export interface BatchDeleteType {
selectIds: string[];
projectId: string;
}
export interface OptionsFieldId {
fieldId: string;
value: string;
text: string;
internal: boolean; // 是否是内置
}
export interface CustomAttributes {
fieldId: string;
fieldName: string;
required: boolean;
apiFieldId: null | undefined | 'string'; // 三方API
defaultValue: string;
type: string;
options: OptionsFieldId[];
}
// 批量编辑
export interface BatchEditCaseType {
selectIds: string[];
projectId: string;
append: boolean; // 是否追加标签
tags: string[];
customField: {
fieldId: string;
value: string;
};
}
// 批量移动
export interface BatchMoveOrCopyType {
selectIds: string[] | undefined;
projectId: string;
moduleId?: string;
moduleIds: string[];
selectAll: boolean;
excludeIds: string[] | undefined;
condition: Record<string, any>;
}
export interface CreateCase {
projectId: string;
templateId: string;
name: string;
prerequisite: string; // prerequisite
caseEditType: string; // 编辑模式:步骤模式/文本模式
steps: string;
textDescription: string;
expectedResult: string; // 预期结果
description: string;
publicCase: boolean; // 是否公共用例
moduleId: string;
versionId: string;
tags: any;
customFields: Record<string, any>; // 自定义字段集合
relateFileMetaIds: string[]; // 关联文件ID集合
[key: string]: any;
}
// 回收站
export interface CaseModuleQueryParams extends TableQueryParams {
moduleIds: string[];
projectId: string;
}
// export interface BatchParams extends BatchMoveOrCopyType {
// selectIds: string[];
// selectAll: boolean;
// moduleIds: string[];
// projectId: string;
// }

View File

@ -36,7 +36,7 @@ export interface MemberItem {
// 成员列表
export type MemberList = MemberItem[];
// 添加成员
export interface AddorUpdateMemberModel {
export interface AddOrUpdateMemberModel {
id?: string;
organizationId?: string;
memberIds?: string[];

View File

@ -101,7 +101,8 @@ export interface CustomField {
fieldId: string;
required?: boolean; // 是否必填
apiFieldId?: string; // api字段名
defaultValue: string | string[] | null | number; // 默认值
defaultValue?: string | string[] | null | number; // 默认值
[key: string]: any;
}
export interface ActionTemplateManage {

View File

@ -0,0 +1,72 @@
import { FeatureTestRouteEnum } from '@/enums/routeEnum';
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const FeatureTest: AppRouteRecordRaw = {
path: '/feature-test',
name: FeatureTestRouteEnum.FEATURE_TEST,
redirect: '/feature-test/featureCase',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.featureTest',
icon: 'icon-icon_functional_testing',
order: 3,
hideChildrenInMenu: true,
},
children: [
// 功能用例
{
path: 'featureCase',
name: FeatureTestRouteEnum.FEATURE_TEST_CASE,
component: () => import('@/views/case-management/caseManagementFeature/index.vue'),
meta: {
locale: 'menu.featureTest.featureCase',
roles: ['*'],
isTopMenu: true,
},
},
// 功能用例回收站
{
path: 'featureCaseRecycle',
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_RECYCLE,
component: () => import('@/views/case-management/caseManagementFeature/components/recycleCaseTable.vue'),
meta: {
locale: 'menu.featureTest.featureCaseRecycle',
roles: ['*'],
breadcrumbs: [
{
name: FeatureTestRouteEnum.FEATURE_TEST_CASE,
locale: 'menu.featureTest.featureCaseList',
},
{
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_RECYCLE,
locale: 'menu.featureTest.featureCaseRecycle',
},
],
},
},
// 创建用例&编辑用例
{
path: 'featureCaseDetail',
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_DETAIL,
component: () => import('@/views/case-management/caseManagementFeature/components/caseDetail.vue'),
meta: {
locale: 'menu.featureTest.featureCaseDetail',
roles: ['*'],
breadcrumbs: [
{
name: FeatureTestRouteEnum.FEATURE_TEST_CASE,
locale: 'menu.featureTest.featureCase',
},
{
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_DETAIL,
locale: 'menu.featureTest.featureCaseDetail',
},
],
},
},
],
};
export default FeatureTest;

View File

@ -1,32 +0,0 @@
import { FeatureTestRouteEnum } from '@/enums/routeEnum';
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const FeatureTest: AppRouteRecordRaw = {
path: '/feature-test',
name: FeatureTestRouteEnum.FEATURE_TEST,
redirect: '/feature-test/featureCase',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.featureTest',
icon: 'icon-icon_functional_testing',
order: 3,
hideChildrenInMenu: true,
},
children: [
// 功能用例
{
path: 'featureCase',
name: FeatureTestRouteEnum.FEATURE_TEST_CASE,
component: () => import('@/views/feature-test/featureCase/index.vue'),
meta: {
locale: 'menu.featureTest.featureCase',
roles: ['*'],
isTopMenu: true,
},
},
],
};
export default FeatureTest;

View File

@ -0,0 +1,60 @@
import { defineStore } from 'pinia';
import { getCaseModulesCounts, getRecycleModulesCounts } from '@/api/modules/case-management/featureCase';
import type { CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const useFeatureCaseStore = defineStore('featureCase', {
persist: true,
state: (): {
moduleId: string[]; // 当前选中模块
allModuleId: string[]; // 所有模块
caseTree: ModuleTreeNode[]; // 用例树
modulesCount: Record<string, any>; // 用例树模块数量
recycleModulesCount: Record<string, any>; // 回收站模块数量
operatingState: boolean; // 操作状态
} => ({
moduleId: [],
allModuleId: [],
caseTree: [],
modulesCount: {},
recycleModulesCount: {},
operatingState: false,
}),
actions: {
// 设置选择moduleId
setModuleId(currentModuleId: string[], offspringIds: string[]) {
this.moduleId = currentModuleId;
if (offspringIds.length > 0) {
this.allModuleId = offspringIds;
}
},
// 设置用例树
setModulesTree(tree: ModuleTreeNode[]) {
this.caseTree = tree;
},
// 获取模块数量
async getCaseModulesCountCount(params: CaseModuleQueryParams) {
try {
this.modulesCount = await getCaseModulesCounts(params);
} catch (error) {
console.log(error);
}
},
// 获取模块数量
async getRecycleMModulesCountCount(params: CaseModuleQueryParams) {
try {
this.recycleModulesCount = await getRecycleModulesCounts(params);
} catch (error) {
console.log(error);
}
},
// 设置是否是编辑或者新增成功状态
setIsAlreadySuccess(state: boolean) {
this.operatingState = state;
},
},
});
export default useFeatureCaseStore;

View File

@ -0,0 +1,216 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('featureTest.featureCase.associatedFile')"
:ok-text="t('featureTest.featureCase.associated')"
:ok-loading="drawerLoading"
:width="960"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<div class="mb-4 grid grid-cols-4 gap-2">
<div class="col-end-4">
<a-select v-model="fileType" @change="changeSelect">
<a-option key="" value="">{{ t('common.all') }}</a-option>
<a-option v-for="item of fileTypeList" :key="item" :value="item">{{ item }}</a-option>
<template #prefix
><span>{{ t('featureTest.featureCase.fileType') }}</span></template
>
</a-select></div
>
<div>
<a-input-search
v-model="searchParams.keyword"
:max-length="250"
:placeholder="t('project.member.searchMember')"
allow-clear
@search="searchHandler"
@press-enter="searchHandler"
></a-input-search
></div>
</div>
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<div class="flex items-center">
<span> <MsIcon class="mr-1" :type="getFileType(record.fileType)" /></span>
<span>{{ record.name }}</span>
</div>
</template>
</MsBaseTable>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { FileIconMap, getFileEnum } from '@/components/pure/ms-upload/iconMap';
import { getAssociatedFileListUrl } from '@/api/modules/case-management/featureCase';
import { getFileTypes } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import { TableQueryParams } from '@/models/common';
import { TableKeyEnum } from '@/enums/tableEnum';
import { UploadStatus } from '@/enums/uploadEnum';
const { t } = useI18n();
const showDrawer = ref<boolean>(false);
const drawerLoading = ref<boolean>(false);
const appStore = useAppStore();
const getCurrentProjectId = computed(() => appStore.getCurrentProjectId);
const fileTypeList = ref<string[]>([]);
const fileType = ref('');
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'save', selectList: AssociatedList[]): void;
}>();
const columns: MsTableColumn = [
{
title: 'featureTest.featureCase.fileName',
dataIndex: 'name',
slotName: 'name',
showInTable: true,
showTooltip: true,
showDrag: false,
width: 300,
},
{
title: 'featureTest.featureCase.description',
dataIndex: 'description',
showInTable: true,
showTooltip: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tags',
dataIndex: 'tags',
isTag: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCreateUser',
dataIndex: 'createUser',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnUpdateUser',
dataIndex: 'updateUser',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
showInTable: true,
},
{
title: 'featureTest.featureCase.tableColumnUpdateTime',
dataIndex: 'updateTime',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
showInTable: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getAssociatedFileListUrl,
{
tableKey: TableKeyEnum.CASE_MANAGEMENT_ASSOCIATED_TABLE,
selectable: true,
noDisable: true,
columns,
scroll: {
x: 1400,
},
heightUsed: 300,
},
(record) => ({
...record,
tags: record.tags || [],
})
);
const searchParams = ref<TableQueryParams>({
keyword: '',
filter: {},
moduleIds: [],
fileType: fileType.value,
projectId: getCurrentProjectId.value,
});
const initData = async () => {
setLoadListParams({ ...searchParams.value });
await loadList();
};
const searchHandler = () => {
initData();
resetSelector();
};
function changeSelect(value: string | number | boolean | Record<string, any>) {
searchParams.value.fileType = value;
initData();
}
const tableSelected = ref<AssociatedList[]>([]);
function handleDrawerConfirm() {
const selectedIds = [...propsRes.value.selectedKeys];
tableSelected.value = propsRes.value.data.filter((item: any) => selectedIds.indexOf(item.id) > -1);
emit('save', tableSelected.value);
showDrawer.value = false;
propsRes.value.selectedKeys.clear();
}
function handleDrawerCancel() {
showDrawer.value = false;
resetSelector();
}
function getFileType(type: string) {
const fileTypes = type ? getFileEnum(`/${type.toLowerCase()}`) : 'unknown';
return FileIconMap[fileTypes][UploadStatus.done];
}
watch(
() => props.visible,
(val) => {
showDrawer.value = val;
if (val) {
initData();
}
}
);
watch(
() => showDrawer.value,
(val) => {
emit('update:visible', val);
}
);
onBeforeMount(async () => {
fileTypeList.value = await getFileTypes(appStore.currentProjectId);
});
</script>
<style scoped></style>

View File

@ -0,0 +1,222 @@
<template>
<MsDialog
v-model:visible="isVisible"
dialog-size="small"
:title="t('featureTest.featureCase.batchEdit', { number: props.batchParams.currentSelectCount })"
ok-text="common.confirm"
:confirm="confirmHandler"
:close="closeHandler"
:switch-props="{
switchName: t('featureTest.featureCase.appendTag'),
switchTooltip: t('featureTest.featureCase.enableTags'),
showSwitch: form.selectedAttrsId === 'systemTags' ? true : false,
enable: form.append,
}"
>
<div class="form">
<a-form ref="formRef" :model="form" size="large" layout="vertical">
<a-form-item
field="selectedAttrsId"
:label="t('featureTest.featureCase.selectAttrs')"
asterisk-position="end"
:rules="[{ required: true, message: t('system.orgTemplate.stateNameNotNull') }]"
>
<a-select v-model="form.selectedAttrsId" :placeholder="t('featureTest.featureCase.PleaseSelect')">
<a-option v-for="item of totalAttrs" :key="item.fieldId" :value="item.fieldId">{{
item.fieldName
}}</a-option>
<a-option key="systemTags" value="systemTags">{{ t('featureTest.featureCase.tags') }}</a-option>
</a-select>
</a-form-item>
<a-form-item
v-if="form.selectedAttrsId === 'systemTags'"
field="tags"
:label="t('featureTest.featureCase.batchUpdate')"
asterisk-position="end"
:rules="[{ required: true, message: t('featureTest.featureCase.PleaseInputTags') }]"
>
<a-input-tag
v-model="form.tags"
:placeholder="t('featureTest.featureCase.pleaseEnterInputTags')"
allow-clear
/>
</a-form-item>
<MsFormCreate
v-if="formRules.length && form.selectedAttrsId !== 'systemTags'"
ref="formCreateRef"
v-model:api="fApi"
:form-rule="formRules"
:form-create-key="FormCreateKeyEnum.CASE_CUSTOM_ATTRS"
/>
</a-form>
</div>
</MsDialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance } from '@arco-design/web-vue';
import MsDialog from '@/components/pure/ms-dialog/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
import type { FormItem } from '@/components/pure/ms-form-create/types';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { batchEditAttrs, getCaseDefaultFields } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFormCreateStore from '@/store/modules/form-create/form-create';
import type { BatchEditCaseType, CustomAttributes } from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import Message from '@arco-design/web-vue/es/message';
const isVisible = ref<boolean>(false);
const appStore = useAppStore();
const formCreateStore = useFormCreateStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
batchParams: BatchActionQueryParams;
}>();
const emits = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'success'): void;
}>();
const currentProjectId = computed(() => appStore.currentProjectId);
const initForm = {
selectedAttrsId: '',
append: false,
tags: [],
};
const form = ref({ ...initForm });
const formRef = ref<FormInstance | null>(null);
const initDefaultForm: FormItem = {
type: 'SELECT',
name: 'name',
label: 'featureTest.featureCase.batchUpdate',
value: '',
options: [],
props: {
modelValue: '',
options: [],
disabled: true,
},
required: true,
};
/**
* 初始化批量编辑属性
*/
const totalAttrs = ref<CustomAttributes[]>([]);
async function initDefaultFields() {
try {
const res = await getCaseDefaultFields(currentProjectId.value);
totalAttrs.value = res.customFields;
} catch (error) {
console.log(error);
}
}
const formRules = ref<FormItem[]>([{ ...initDefaultForm }]);
const fApi = ref<any>({});
const formCreateValue = computed(() => formCreateStore.formCreateRuleMap.get(FormCreateKeyEnum.CASE_CUSTOM_ATTRS));
const updateType = computed(() => {
return totalAttrs.value.find((item: any) => item.fieldId === form.value.selectedAttrsId)?.type;
});
watch(
() => updateType.value,
(val) => {
const currentAttrs = totalAttrs.value.filter((item: any) => item.fieldId === form.value.selectedAttrsId);
if (val) {
formRules.value = currentAttrs.map((item: CustomAttributes) => {
return {
type: val,
name: item.fieldId,
label: 'featureTest.featureCase.batchUpdate',
value: item.defaultValue,
options: item.options,
props: {
modelValue: item.defaultValue,
options: item.options,
disabled: !form.value.selectedAttrsId,
},
required: item.required,
};
}) as FormItem[];
}
}
);
function closeHandler() {
isVisible.value = false;
formRef.value?.resetFields();
form.value = { ...initForm };
formRules.value = [{ ...initDefaultForm }];
}
async function confirmHandler(enable: boolean | undefined) {
await formRef.value?.validate().then(async (error) => {
if (!error) {
try {
const customField = {
fieldId: '',
value: '',
};
formCreateValue.value?.forEach((item: any) => {
customField.fieldId = item.field;
customField.value = JSON.stringify(item.value);
});
const params: BatchEditCaseType = {
selectIds: props.batchParams.selectedIds as string[],
projectId: currentProjectId.value,
append: enable as boolean,
tags: form.value.tags,
customField,
};
await batchEditAttrs(params);
Message.success(t('featureTest.featureCase.editSuccess'));
closeHandler();
emits('success');
} catch (e) {
console.log(e);
}
} else {
return false;
}
});
}
watch(
() => isVisible.value,
(val) => {
emits('update:visible', val);
}
);
watch(
() => props.visible,
(val) => {
isVisible.value = val;
if (val) {
initDefaultFields();
}
}
);
onBeforeUnmount(() => {
formRules.value = [];
});
</script>
<style scoped></style>

View File

@ -0,0 +1,143 @@
<template>
<MsCard
:loading="loading"
:title="title"
:is-edit="isEdit"
has-breadcrumb
@save="saveHandler"
@save-and-continue="saveHandler(true)"
>
<template #headerRight>
<a-select class="w-[240px]" :placeholder="t('featureTest.featureCase.versionPlaceholder')">
<a-option v-for="template of versionOptions" :key="template.id" :value="template.id">{{
template.name
}}</a-option>
</a-select>
</template>
<CaseTemplateDetail ref="caseModuleDetailRef" v-model:form-mode-value="caseDetailInfo" />
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import MsCard from '@/components/pure/ms-card/index.vue';
import CaseTemplateDetail from './caseTemplateDetail.vue';
import { createCaseRequest, updateCaseRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { scrollIntoView } from '@/utils/dom';
import { FeatureTestRouteEnum } from '@/enums/routeEnum';
import Message from '@arco-design/web-vue/es/message';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const featureCaseStore = useFeatureCaseStore();
const caseDetailInfo = ref<Record<string, any>>({
request: {},
fileList: [],
});
const versionOptions = ref([
{
id: '1001',
name: '模板01',
},
]);
const title = ref('');
const loading = ref(false);
const isEdit = computed(() => !!route.query.id);
const isContinueFlag = ref(false);
async function save() {
try {
loading.value = true;
if (isEdit.value) {
await updateCaseRequest(caseDetailInfo.value);
Message.success(t('featureTest.featureCase.editSuccess'));
} else {
await createCaseRequest(caseDetailInfo.value);
Message.success(t('common.addSuccess'));
}
router.push({ name: FeatureTestRouteEnum.FEATURE_TEST_CASE, query: { ...route.query } });
featureCaseStore.setIsAlreadySuccess(true);
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
const caseModuleDetailRef = ref();
//
function saveHandler(isContinue = false) {
const { caseFormRef, formRef, fApi } = caseModuleDetailRef.value;
isContinueFlag.value = isContinue;
caseFormRef?.validate().then((res: any) => {
if (!res) {
fApi.validate((valid: any) => {
if (valid === true) {
formRef?.validate().then((result: any) => {
if (!result) {
return save();
}
});
}
});
}
return scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
watchEffect(() => {
if (isEdit.value) {
title.value = t('featureTest.featureCase.updateCase');
} else {
title.value = t('featureTest.featureCase.creatingCase');
}
});
</script>
<style scoped lang="less">
.wrapper-preview {
display: flex;
.preview-left {
width: 100%;
border-right: 1px solid var(--color-text-n8);
.changeType {
padding: 2px 4px;
border-radius: 4px;
color: var(--color-text-4);
:deep(.arco-icon-down) {
font-size: 14px;
}
&:hover {
color: rgb(var(--primary-5));
background: rgb(var(--primary-1));
cursor: pointer;
}
}
}
.preview-right {
width: 428px;
}
}
.circle {
width: 16px;
height: 16px;
line-height: 16px;
border-radius: 50%;
text-align: center;
color: var(--color-text-4);
background: var(--color-text-n8);
}
</style>

View File

@ -0,0 +1,779 @@
<template>
<div class="page-header mb-4 h-[34px]">
<div class="text-[var(--color-text-1)]"
>{{ t('featureTest.featureCase.allCase') }}
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount.all }})</span></div
>
<div class="flex w-[80%] items-center justify-end">
<a-select class="w-[240px]" :placeholder="t('featureTest.featureCase.versionPlaceholder')">
<a-option v-for="version of versionOptions" :key="version.id" :value="version.id">{{ version.name }}</a-option>
</a-select>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('featureTest.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
></a-input-search>
<MsTag
:type="isExpandFilter ? 'primary' : 'default'"
:theme="isExpandFilter ? 'lightOutLine' : 'outline'"
size="large"
class="-mt-[3px] min-w-[64px] cursor-pointer"
>
<span :class="!isExpandFilter ? 'text-[var(--color-text-4)]' : ''" @click="isExpandFilterHandler"
><icon-filter class="mr-[4px]" :style="{ 'font-size': '16px' }" />{{
t('featureTest.featureCase.filter')
}}</span
>
</MsTag>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type ml-[4px]">
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio>
<a-radio value="xMind" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_mindnote_outlined" /></a-radio>
</a-radio-group>
</div>
</div>
<FilterPanel v-show="isExpandFilter"></FilterPanel>
<!-- 脑图开始 -->
<MinderEditor
v-if="showType === 'xMind'"
:import-json="importJson"
:tags="['模块', '用例', '前置条件', '备注', '步骤', '预期结果']"
tag-enable
sequence-enable
@node-click="handleNodeClick"
/>
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>
<!-- 脑图结束 -->
<!-- 用例表开始 -->
<ms-base-table
v-bind="propsRes"
:action-config="tableBatchActions"
@selected-change="handleTableSelect"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #name="{ record }">
<a-button type="text" class="px-0" @click="showCaseDetail(record.id)">{{ record.name }}</a-button>
</template>
<template #reviewStatus="{ record }">
<MsIcon
:type="getStatusText(record.reviewStatus)?.iconType || ''"
class="mr-1"
:class="[getReviewStatusClass(record.reviewStatus)]"
></MsIcon>
<span>{{ getStatusText(record.reviewStatus)?.statusType || '' }} </span>
</template>
<template #lastExecuteResult="{ record }">
<MsIcon
:type="getStatusText(record.lastExecuteResult)?.iconType || ''"
class="mr-1"
:class="[getReviewStatusClass(record.lastExecuteResult)]"
></MsIcon>
<span>{{ getStatusText(record.lastExecuteResult)?.statusType || '' }}</span>
</template>
<template #moduleId="{ record }">
<a-tooltip :content="getModules(record.moduleId)" position="top">
<span class="one-line-text inline-block">{{ getModules(record.moduleId) }}</span>
</a-tooltip>
</template>
<template #operation="{ record }">
<MsButton @click="editCase(record)">{{ t('common.edit') }}</MsButton>
<MsButton class="!mr-0" @click="deleteCase(record)">{{ t('common.delete') }}</MsButton>
</template>
</ms-base-table>
<!-- 用例表结束 -->
<a-modal
v-model:visible="showBatchMoveDrawer"
title-align="start"
class="ms-modal-no-padding ms-modal-small"
:mask-closable="false"
:ok-text="
t(
isMove
? 'featureTest.featureCase.batchMoveSelectedModules'
: 'featureTest.featureCase.batchCopySelectedModules',
{
number: batchParams?.currentSelectCount || batchParams?.selectedIds?.length,
}
)
"
:ok-button-props="{ disabled: selectedModuleKeys.length === 0 }"
:cancel-button-props="{ disabled: batchMoveCaseLoading }"
:on-before-ok="handleCaseMoveOrCopy"
@close="handleMoveCaseModalCancel"
>
<template #title>
<div class="flex w-full items-center justify-between">
<div>
{{ isMove ? t('featureTest.featureCase.batchMoveTitle') : t('featureTest.featureCase.batchCopyTitle') }}
<span class="ml-[4px] text-[var(--color-text-4)]">
{{ t('featureTest.featureCase.batchMove', { number: batchParams.currentSelectCount }) }}
</span>
</div>
<div class="mr-2">
<a-select class="w-[120px]" placeholder="请选择版本">
<a-option v-for="item of versionOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
</div>
</div>
</template>
<FeatureCaseTree
v-if="showBatchMoveDrawer"
ref="caseTreeRef"
v-model:selected-keys="selectedModuleKeys"
:active-folder="props.activeFolder"
:is-expand-all="true"
is-modal
@case-node-select="caseNodeSelect"
></FeatureCaseTree>
</a-modal>
<ExportExcelDrawer v-model:visible="showExportExcelVisible" />
<BatchEditModal v-model:visible="showEditModel" :batch-params="batchParams" @success="successHandler" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MinderEditor from '@/components/pure/minder-editor/minderEditor.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import FilterPanel from '@/components/business/ms-filter-panel/searchForm.vue';
import BatchEditModal from './batchEditModal.vue';
import FeatureCaseTree from './caseTree.vue';
import ExportExcelDrawer from './exportExcelDrawer.vue';
import {
batchCopyToModules,
batchDeleteCase,
batchMoveToModules,
deleteCaseRequest,
getCaseDetail,
getCaseList,
updateCaseRequest,
} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore, useTableStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { characterLimit, findNodePathByKey } from '@/utils';
import type { CaseManagementTable, CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { FeatureTestRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { getReviewStatusClass, getStatusText } from './utils';
import debounce from 'lodash-es/debounce';
const { openModal } = useModal();
const { t } = useI18n();
const router = useRouter();
const appStore = useAppStore();
const featureCaseStore = useFeatureCaseStore();
const tableStore = useTableStore();
const props = defineProps<{
activeFolder: string;
activeFolderType: 'folder' | 'module';
offspringIds: string[]; // id
modulesCount: Record<string, number>; //
}>();
const emit = defineEmits<{
(e: 'init', params: CaseModuleQueryParams): void;
}>();
const keyword = ref<string>();
const showType = ref<string>('list');
const isExpandFilter = ref<boolean>(false);
const versionOptions = ref([
{
id: '1001',
name: 'v_1.0',
},
]);
const caseTreeData = computed(() => featureCaseStore.caseTree);
const moduleId = computed(() => featureCaseStore.moduleId[0]);
const currentProjectId = computed(() => appStore.currentProjectId);
// ||
const isExpandFilterHandler = () => {
isExpandFilter.value = !isExpandFilter.value;
};
const visible = ref<boolean>(false);
const nodeData = ref<any>({});
const importJson = ref<any>({});
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes('用例')) {
visible.value = true;
nodeData.value = data;
}
}
onBeforeMount(() => {
importJson.value = {
root: {
data: {
text: '测试用例',
id: 'xxxx',
},
children: [
{
data: {
id: 'sdasdas',
text: '模块 1',
resource: ['模块'],
},
},
{
data: {
id: 'dasdasda',
text: '模块 2',
expandState: 'collapse',
},
children: [
{
data: {
id: 'frihofiuho3f',
text: '用例 1',
resource: ['用例'],
},
},
{
data: {
id: 'df09348f034f',
text: ' 用例 2',
resource: ['用例'],
},
},
],
},
],
},
template: 'default',
};
});
const columns: MsTableColumn = [
{
title: 'featureTest.featureCase.tableColumnID',
dataIndex: 'id',
width: 200,
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
ellipsis: true,
showDrag: false,
},
{
title: 'featureTest.featureCase.tableColumnName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
editType: ColumnEditTypeEnum.INPUT,
sortable: {
sortDirections: ['ascend', 'descend'],
},
ellipsis: true,
showDrag: false,
},
{
title: 'featureTest.featureCase.tableColumnLevel',
dataIndex: 'level',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCaseState',
dataIndex: 'caseState',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnReviewResult',
dataIndex: 'reviewStatus',
slotName: 'reviewStatus',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnExecutionResult',
dataIndex: 'lastExecuteResult',
slotName: 'lastExecuteResult',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnVersion',
slotName: 'versionId',
dataIndex: 'versionId',
width: 300,
showTooltip: true,
showInTable: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnModule',
slotName: 'moduleId',
showInTable: true,
width: 300,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnTag',
slotName: 'tags',
dataIndex: 'tags',
showInTable: true,
isTag: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCreateUser',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnUpdateUser',
slotName: 'updateUser',
dataIndex: 'updateUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnUpdateTime',
slotName: 'updateTime',
dataIndex: 'updateTime',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const tableBatchActions = {
baseAction: [
{
label: 'featureTest.featureCase.export',
eventTag: 'export',
children: [
{
label: 'featureTest.featureCase.exportExcel',
eventTag: 'exportExcel',
},
{
label: 'featureTest.featureCase.exportXMind',
eventTag: 'exportXMind',
},
],
},
{
label: 'common.edit',
eventTag: 'batchEdit',
},
{
label: 'featureTest.featureCase.moveTo',
eventTag: 'batchMoveTo',
},
{
label: 'featureTest.featureCase.copyTo',
eventTag: 'batchCopyTo',
},
],
moreAction: [
{
label: 'featureTest.featureCase.associatedDemand',
eventTag: 'associatedDemand',
},
{
label: 'featureTest.featureCase.generatingDependencies',
eventTag: 'generatingDependencies',
},
{
label: 'featureTest.featureCase.addToPublic',
eventTag: 'addToPublic',
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
},
],
};
/**
* 处理更新用例参数
* @param detailResult 详情字段
*/
function getUpdateParams(detailResult: Record<string, any>, name: string) {
const { customFields } = detailResult;
const customFieldsMaps: Record<string, any> = {};
customFields.forEach((item: any) => {
customFieldsMaps[item.fieldId] = JSON.parse(item.defaultValue);
});
return {
request: {
...detailResult,
name,
customFields: customFieldsMaps,
tags: JSON.parse(detailResult.tags),
},
fileList: [],
};
}
/**
* 更新用例名称
*/
async function updateCaseName(record: CaseManagementTable) {
try {
const detailResult = await getCaseDetail(record.id);
const params = await getUpdateParams(detailResult, record.name);
await updateCaseRequest(params);
Message.success(t('common.updateSuccess'));
return Promise.resolve(true);
} catch (error) {
console.log(error);
return Promise.resolve(false);
}
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setProps } = useTable(
getCaseList,
{
tableKey: TableKeyEnum.CASE_MANAGEMENT_TABLE,
scroll: { x: 3200 },
selectable: true,
showSetting: true,
heightUsed: 340,
enableDrag: true,
},
(record) => ({
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
}),
updateCaseName
);
//
function emitTableParams() {
emit('init', {
keyword: keyword.value,
moduleIds: [],
projectId: currentProjectId.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
});
}
const tableSelected = ref<(string | number)[]>([]);
function handleTableSelect(selectArr: (string | number)[]) {
tableSelected.value = selectArr;
}
const searchParams = ref<TableQueryParams>({
projectId: currentProjectId.value,
moduleIds: [],
});
function getLoadListParams() {
if (props.activeFolder === 'all') {
searchParams.value.moduleIds = [];
} else {
searchParams.value.moduleIds = [moduleId.value, ...props.offspringIds];
}
setLoadListParams({
...searchParams.value,
keyword: keyword.value,
});
}
//
async function initData() {
getLoadListParams();
loadList();
emitTableParams();
}
//
function editCase(record: CaseManagementTable) {
router.push({
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_DETAIL,
query: {
id: record.id,
},
});
}
//
function deleteCase(record: CaseManagementTable) {
openModal({
type: 'error',
title: t('featureTest.featureCase.deleteCaseTitle', { name: characterLimit(record.name) }),
content: t('featureTest.featureCase.beforeDeleteCase'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
const params = {
id: record.id,
deleteAll: false,
projectId: currentProjectId.value,
};
await deleteCaseRequest(params);
Message.success(t('common.deleteSuccess'));
emitTableParams();
loadList();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
const showExportExcelVisible = ref<boolean>(false);
// Excel
function handleShowExportExcel() {
showExportExcelVisible.value = true;
}
const selectData = ref<string[] | undefined>([]);
const showEditModel = ref<boolean>(false);
//
function batchEdit() {
showEditModel.value = true;
}
const showBatchMoveDrawer = ref<boolean>(false);
/**
* 处理文件夹树节点选中事件
*/
const selectedModuleKeys = ref<string[]>([]); //
const batchMoveCaseLoading = ref(false);
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
currentSelectCount: 0,
});
const isMove = ref<boolean>(false);
//
async function handleCaseMoveOrCopy() {
batchMoveCaseLoading.value = true;
try {
const params = {
selectIds: batchParams.value.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [],
condition: { keyword: keyword.value },
projectId: currentProjectId.value,
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder],
moduleId: selectedModuleKeys.value[0],
};
if (isMove.value) {
await batchMoveToModules(params);
Message.success(t('featureTest.featureCase.batchMoveSuccess'));
} else {
await batchCopyToModules(params);
Message.success(t('featureTest.featureCase.batchCopySuccess'));
}
isMove.value = false;
emitTableParams();
loadList();
resetSelector();
} catch (error) {
console.log(error);
} finally {
batchMoveCaseLoading.value = false;
}
}
function handleMoveCaseModalCancel() {
showBatchMoveDrawer.value = false;
selectedModuleKeys.value = [];
}
function caseNodeSelect(keys: string[]) {
selectedModuleKeys.value = keys;
}
//
function batchMoveOrCopy() {
showBatchMoveDrawer.value = true;
}
// name
function getModules(moduleIds: string) {
const modules = findNodePathByKey(caseTreeData.value, moduleIds, undefined, 'id');
const moduleName = (modules || []).treePath.map((item: any) => item.name);
if (moduleName.length === 1) {
return moduleName[0];
}
return `/${moduleName.join('/')}`;
}
//
async function batchDelete() {
openModal({
type: 'error',
title: t('featureTest.featureCase.batchDelete', { number: (selectData.value || []).length }),
content: t('featureTest.featureCase.beforeDeleteCase'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await batchDeleteCase({
selectIds: batchParams.value.selectedIds as string[],
projectId: currentProjectId.value,
});
resetSelector();
Message.success(t('common.deleteSuccess'));
emitTableParams();
loadList();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = params;
if (event.eventTag === 'exportExcel') {
handleShowExportExcel();
} else if (event.eventTag === 'batchEdit') {
batchEdit();
} else if (event.eventTag === 'delete') {
batchDelete();
} else if (event.eventTag === 'batchMoveTo') {
batchMoveOrCopy();
isMove.value = true;
} else if (event.eventTag === 'batchCopyTo') {
batchMoveOrCopy();
isMove.value = false;
}
}
const searchList = debounce(() => {
getLoadListParams();
loadList();
}, 100);
function successHandler() {
loadList();
emitTableParams();
resetSelector();
}
//
function showCaseDetail(id: string) {}
watch(
() => showType.value,
() => {
initData();
}
);
watch(
() => props.activeFolder,
() => {
keyword.value = '';
initData();
resetSelector();
},
{ immediate: true }
);
tableStore.initColumn(TableKeyEnum.CASE_MANAGEMENT_TABLE, columns, 'drawer');
</script>
<style scoped lang="less">
.page-header {
@apply flex items-center justify-between;
}
.filter-panel {
background: var(--color-text-n9);
@apply mt-1 rounded-md p-3;
.condition-text {
color: var(--color-text-2);
}
}
</style>

View File

@ -0,0 +1,891 @@
<template>
<div class="wrapper-preview">
<div class="preview-left pr-4">
<a-form ref="caseFormRef" class="rounded-[4px]" :model="form" layout="vertical">
<a-form-item
field="name"
:label="t('system.orgTemplate.caseName')"
:rules="[{ required: true, message: t('system.orgTemplate.caseNamePlaceholder') }]"
required
asterisk-position="end"
>
<a-input
v-model="form.name"
:max-length="255"
:placeholder="t('system.orgTemplate.caseNamePlaceholder')"
show-word-limit
allow-clear
></a-input>
</a-form-item>
<a-form-item field="precondition" :label="t('system.orgTemplate.precondition')" asterisk-position="end">
<MsRichText v-model:model-value="form.prerequisite" />
</a-form-item>
<a-form-item
field="step"
:label="
form.caseEditType === 'STEP'
? t('system.orgTemplate.stepDescription')
: t('system.orgTemplate.textDescription')
"
class="relative"
>
<div class="absolute left-16 top-0">
<a-divider direction="vertical" />
<a-dropdown :popup-max-height="false" @select="handleSelectType">
<span class="changeType">{{ t('system.orgTemplate.changeType') }} <icon-down /></span>
<template #content>
<a-doption value="STEP" :class="getSelectTypeClass('STEP')">
{{ t('system.orgTemplate.stepDescription') }}</a-doption
>
<a-doption value="TEXT" :class="getSelectTypeClass('TEXT')">{{
t('system.orgTemplate.textDescription')
}}</a-doption>
</template>
</a-dropdown>
</div>
<!-- 步骤描述 -->
<div v-if="form.caseEditType === 'STEP'" class="w-full">
<MsBaseTable v-bind="propsRes" ref="stepTableRef" v-on="propsEvent">
<template #index="{ rowIndex }">
<div class="circle text-xs font-medium"> {{ rowIndex + 1 }}</div>
</template>
<template #caseStep="{ record }">
<a-textarea
v-if="record.showStep"
v-model="record.step"
size="mini"
:auto-size="true"
class="w-max-[267px]"
:placeholder="t('system.orgTemplate.stepTip')"
@blur="blurHandler(record, 'step')"
/>
<div
v-else-if="record.step && !record.showStep"
class="w-full cursor-pointer"
@click="edit(record, 'step')"
>{{ record.step }}</div
>
<div
v-else-if="!record.caseStep && !record.showStep"
class="placeholder w-full cursor-pointer text-[var(--color-text-brand)]"
@click="edit(record, 'step')"
>{{ t('system.orgTemplate.stepTip') }}</div
>
</template>
<template #expectedResult="{ record }">
<a-textarea
v-if="record.showExpected"
v-model="record.expected"
size="mini"
:auto-size="true"
class="w-max-[267px]"
:placeholder="t('system.orgTemplate.expectationTip')"
@blur="blurHandler(record, 'expected')"
/>
<div
v-else-if="record.expected && !record.showExpected"
class="w-full cursor-pointer"
@click="edit(record, 'expected')"
>{{ record.expected }}</div
>
<div
v-else-if="!record.expected && !record.showExpected"
class="placeholder w-full cursor-pointer text-[var(--color-text-brand)]"
@click="edit(record, 'expected')"
>{{ t('system.orgTemplate.expectationTip') }}</div
>
</template>
<template #operation="{ record }">
<MsTableMoreAction
v-if="!record.internal"
:list="moreActions"
@select="(item:ActionsItem) => handleMoreActionSelect(item,record)"
/>
</template>
</MsBaseTable>
<a-button class="mt-2 px-0" type="text" @click="addStep">
<template #icon>
<icon-plus class="text-[14px]" />
</template>
{{ t('system.orgTemplate.addStep') }}
</a-button>
</div>
<!-- 文本描述 -->
<MsRichText v-else v-model:modelValue="form.textDescription" />
</a-form-item>
<a-form-item
v-if="form.caseEditType === 'TEXT'"
field="remark"
:label="t('featureTest.featureCase.expectedResult')"
>
<MsRichText v-model:modelValue="form.expectedResult" />
</a-form-item>
<a-form-item field="remark" :label="t('featureTest.featureCase.remark')">
<MsRichText v-model:modelValue="form.description" />
</a-form-item>
<a-form-item field="attachment" :label="t('featureTest.featureCase.addAttachment')">
<div class="flex flex-col">
<div class="mb-1">
<a-dropdown position="tr" trigger="hover">
<a-button type="outline">
<template #icon> <icon-plus class="text-[14px]" /> </template
>{{ t('system.orgTemplate.addAttachment') }}</a-button
>
<template #content>
<a-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:show-file-list="false"
:before-upload="beforeUpload"
@change="handleChange"
>
<template #upload-button>
<a-button type="text" class="!text-[var(--color-text-1)]">
<icon-upload />{{ t('featureTest.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('featureTest.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>
</a-form-item>
</a-form>
<!-- 文件列表开始 -->
<div class="w-[90%]">
<MsFileList ref="fileListRef" v-model:file-list="fileList" mode="static">
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton type="button" status="danger" class="!mr-[4px]" @click="transferFile(item)">
{{ t('featureTest.featureCase.storage') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
{{ t('featureTest.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="cancelAssociated(item)">
{{ t('featureTest.featureCase.cancelLink') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
{{ t('featureTest.featureCase.download') }}
</MsButton>
</div>
</template>
</MsFileList>
</div>
<!-- 文件列表结束 -->
</div>
<!-- 自定义字段开始 -->
<div class="preview-right px-4">
<div>
<a-skeleton v-if="isLoading" :loading="isLoading" :animation="true">
<a-space direction="vertical" class="w-full" size="large">
<a-skeleton-line :rows="rowLength" :line-height="30" :line-spacing="30" />
</a-space>
</a-skeleton>
<a-form v-else ref="formRef" class="rounded-[4px]" :model="form" layout="vertical">
<a-form-item
field="moduleId"
asterisk-position="end"
:label="t('system.orgTemplate.modules')"
:rules="[{ required: true, message: t('system.orgTemplate.moduleRuleTip') }]"
>
<a-tree-select
v-model="form.moduleId"
:allow-search="true"
:data="caseTree"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
}"
:tree-props="{
virtualListProps: {
height: 200,
},
}"
></a-tree-select>
</a-form-item>
<MsFormCreate
v-if="formRules.length"
ref="formCreateRef"
v-model:api="fApi"
:form-rule="formRules"
:form-create-key="FormCreateKeyEnum.CASE_MANAGEMENT_FIELD"
/>
<a-form-item field="tags" :label="t('system.orgTemplate.tags')">
<a-input-tag v-model="form.tags" :placeholder="t('formCreate.PleaseEnter')" allow-clear />
</a-form-item>
</a-form>
</div>
</div>
<!-- 自定义字段结束 -->
</div>
<div class=" ">
<MsUpload
v-model:file-list="fileList"
accept="none"
:auto-upload="false"
:sub-text="acceptType === 'jar' ? '' : t('project.fileManagement.normalFileSubText', { size: 50 })"
multiple
draggable
size-unit="MB"
:max-size="50"
:is-all-screen="true"
class="mb-[16px]"
@change="handleChange"
/>
</div>
<AssociatedFileDrawer v-model:visible="showDrawer" @save="saveSelectAssociatedFile" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { FormInstance } from '@arco-design/web-vue';
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 MsRichText from '@/components/pure/ms-rich-text/MsRichText.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 MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AssociatedFileDrawer from './associatedFileDrawer.vue';
import { getCaseDefaultFields, getCaseDetail } from '@/api/modules/case-management/featureCase';
import { getProjectFieldList } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import useFormCreateStore from '@/store/modules/form-create/form-create';
import { getGenerateId } from '@/utils';
import type { AssociatedList, CreateCase, StepList } from '@/models/caseManagement/featureCase';
import type { CustomField, DefinedFieldItem } from '@/models/setting/template';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import {
getCustomDetailFields,
getTotalFieldOptionList,
} from '@/views/setting/organization/template/components/fieldSetting';
const { t } = useI18n();
const route = useRoute();
const appStore = useAppStore();
const formCreateStore = useFormCreateStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const props = defineProps<{
formModeValue: Record<string, any>; //
}>();
const emit = defineEmits(['update:formModeValue', 'changeFile']);
const acceptType = ref('none'); // -
const templateFieldColumns: MsTableColumn = [
{
title: 'system.orgTemplate.numberIndex',
dataIndex: 'index',
slotName: 'index',
width: 100,
showDrag: false,
showInTable: true,
},
{
title: 'system.orgTemplate.useCaseStep',
slotName: 'caseStep',
dataIndex: 'caseStep',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.expectedResult',
dataIndex: 'expectedResult',
slotName: 'expectedResult',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.operation',
slotName: 'operation',
fixed: 'right',
width: 200,
showInTable: true,
showDrag: false,
},
];
const { propsRes, propsEvent, setProps } = useTable(undefined, {
tableKey: TableKeyEnum.CASE_MANAGEMENT_DETAIL_TABLE,
columns: templateFieldColumns,
scroll: { x: '800px' },
selectable: false,
noDisable: true,
size: 'default',
showSetting: false,
showPagination: false,
enableDrag: true,
});
const moreActions: ActionsItem[] = [
{
label: 'featureTest.featureCase.copyStep',
eventTag: 'copyStep',
},
{
label: 'featureTest.featureCase.InsertStepsBefore',
eventTag: 'InsertStepsBefore',
},
{
label: 'featureTest.featureCase.afterInsertingSteps',
eventTag: 'afterInsertingSteps',
},
{
isDivider: true,
},
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
const formRef = ref<FormInstance>();
const caseFormRef = ref<FormInstance>();
//
const selectData = ref<DefinedFieldItem[]>([]);
//
const stepData = ref<StepList[]>([
{
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
},
]);
const featureCaseStore = useFeatureCaseStore();
const modelId = computed(() => featureCaseStore.moduleId[0]);
const caseTree = computed(() => featureCaseStore.caseTree);
const initForm: CreateCase = {
projectId: currentProjectId.value,
templateId: '',
name: '',
prerequisite: '',
caseEditType: 'STEP',
steps: '',
textDescription: '',
expectedResult: '',
description: '',
publicCase: false,
moduleId: modelId.value,
versionId: '',
tags: [],
customFields: [],
relateFileMetaIds: [],
};
const form = ref<CreateCase>({ ...initForm });
watch(
() => stepData.value,
() => {
const res = stepData.value.map((item, index) => {
return {
num: index,
desc: item.step,
result: item.expected,
};
});
form.value.steps = JSON.stringify(res);
},
{ deep: true }
);
//
const params = ref<Record<string, any>>({
request: {},
fileList: [], //
});
//
function getSelectTypeClass(type: string) {
return form.value.caseEditType === type ? ['bg-[rgb(var(--primary-1))]', '!text-[rgb(var(--primary-5))]'] : [];
}
//
const handleSelectType = (value: string | number | Record<string, any> | undefined) => {
form.value.caseEditType = value as string;
};
//
const addStep = () => {
stepData.value.push({
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
});
};
//
function copyStep(record: StepList) {
stepData.value.push({
...record,
id: getGenerateId(),
});
}
//
function deleteStep(record: StepList) {
stepData.value = stepData.value.filter((item: any) => item.id !== record.id);
}
//
function insertStepsBefore(record: StepList) {
const index = stepData.value.map((item: any) => item.id).indexOf(record.id);
const insertItem = {
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
};
stepData.value.splice(index, 0, insertItem);
}
//
function afterInsertingSteps(record: StepList) {
const index = stepData.value.map((item: any) => item.id).indexOf(record.id);
const insertItem = {
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
};
stepData.value.splice(index + 1, 0, insertItem);
}
//
function edit(record: StepList, type: string) {
if (type === 'step') {
record.showStep = true;
} else {
record.showExpected = true;
}
}
//
function blurHandler(record: StepList, type: string) {
if (type === 'step') {
record.showStep = false;
} else {
record.showExpected = false;
}
}
//
const handleMoreActionSelect = (item: ActionsItem, record: StepList) => {
switch (item.eventTag) {
case 'copyStep':
copyStep(record);
break;
case 'InsertStepsBefore':
insertStepsBefore(record);
break;
case 'afterInsertingSteps':
afterInsertingSteps(record);
break;
default:
deleteStep(record);
break;
}
};
//
const totalTemplateField = ref<DefinedFieldItem[]>([]);
const isLoading = ref<boolean>(true);
//
async function getAllCaseFields() {
try {
totalTemplateField.value = await getProjectFieldList({ scopedId: currentProjectId.value, scene: 'FUNCTIONAL' });
totalTemplateField.value = getTotalFieldOptionList(totalTemplateField.value as DefinedFieldItem[]);
} catch (error) {
console.log(error);
}
}
//
async function initDefaultFields() {
try {
isLoading.value = true;
await getAllCaseFields();
const res = await getCaseDefaultFields(currentProjectId.value);
const { customFields, id } = res;
form.value.templateId = id;
selectData.value = getCustomDetailFields(totalTemplateField.value as DefinedFieldItem[], customFields);
isLoading.value = false;
} catch (error) {
console.log(error);
}
}
const fileList = ref<MsFileItem[]>([]);
function beforeUpload() {
return Promise.resolve(true);
}
//
function convertToFile(fileInfo: AssociatedList): MsFileItem {
const fileName = fileInfo.fileType ? `${fileInfo.name}.${fileInfo.fileType || ''}` : `${fileInfo.name}`;
const type = fileName.split('.')[1];
const file = new File([new Blob()], `${fileName}`, {
type: `application/${type}`,
});
Object.defineProperty(file, 'size', { value: fileInfo.size });
return {
enable: fileInfo.enable || false,
file,
name: fileName,
percent: 0,
status: 'done',
uid: fileInfo.id,
url: `http://172.16.200.18:8081/${fileInfo.filePath || ''}`,
local: fileInfo.local,
};
}
//
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
fileList.value.push(...fileResultList);
}
const title = ref('');
const isEdit = computed(() => !!route.query.id);
const attachmentsList = ref([]);
// localitem
const oldLocalFileList = computed(() => {
return attachmentsList.value.filter((item: any) => item.local);
});
//
const currentOldLocalFileList = computed(() => {
return fileList.value.filter((item) => item.local && item.status !== 'init').map((item: any) => item.uid);
});
// id
const associateFileIds = computed(() => {
return attachmentsList.value.filter((item: any) => !item.local).map((item: any) => item.id);
});
// list
const currentAlreadyAssociateFileList = computed(() => {
return fileList.value
.filter((item) => !item.local && !associateFileIds.value.includes(item.uid))
.map((item: any) => item.uid);
});
// ID
const newAssociateFileListIds = computed(() => {
return fileList.value
.filter((item: any) => !item.local && !associateFileIds.value.includes(item.uid))
.map((item: any) => item.uid);
});
// id
const deleteFileMetaIds = computed(() => {
return oldLocalFileList.value
.filter((item: any) => !currentOldLocalFileList.value.includes(item.id))
.map((item: any) => item.id);
});
// id
const unLinkFilesIds = computed(() => {
return associateFileIds.value.filter((id: string) => !currentAlreadyAssociateFileList.value.includes(id));
});
//
function getDetailData(detailResult: CreateCase) {
const { customFields, attachments, steps, tags } = detailResult;
form.value = {
...detailResult,
tags: JSON.parse(tags as string),
};
//
selectData.value = getCustomDetailFields(
totalTemplateField.value as DefinedFieldItem[],
customFields as CustomField[]
);
//
if (steps) {
stepData.value = JSON.parse(steps).map((item: any) => {
return {
step: item.desc,
expected: item.result,
};
});
}
attachmentsList.value = attachments;
//
fileList.value = attachments
.map((fileInfo: any) => {
return {
...fileInfo,
name: fileInfo.fileName,
};
})
.map((fileInfo: any) => {
return convertToFile(fileInfo);
});
// id
}
//
async function getCaseInfo() {
try {
isLoading.value = true;
await getAllCaseFields();
const detailResult = await getCaseDetail(route.query.id as string);
getDetailData(detailResult);
} catch (error) {
console.log(error);
} finally {
isLoading.value = false;
}
}
watchEffect(() => {
if (isEdit.value) {
title.value = t('featureTest.featureCase.updateCase');
//
getCaseInfo();
} else {
title.value = t('featureTest.featureCase.creatingCase');
initDefaultFields();
}
});
//
function getFilesParams() {
form.value.deleteFileMetaIds = deleteFileMetaIds.value;
form.value.unLinkFilesIds = unLinkFilesIds.value;
params.value.fileList = fileList.value.filter((item: any) => item.status === 'init');
form.value.relateFileMetaIds = newAssociateFileListIds.value;
}
//
watch(
() => fileList.value,
(val) => {
if (val) {
form.value.relateFileMetaIds = fileList.value.filter((item) => !item.local).map((item) => item.uid);
params.value.fileList = fileList.value.filter((item) => item.local && item.status === 'init');
if (isEdit.value) {
getFilesParams();
}
}
},
{ deep: true }
);
//
watch(
() => form.value,
(val) => {
if (val) {
if (val) {
params.value.request = { ...form.value };
emit('update:formModeValue', params.value);
featureCaseStore.setModuleId([form.value.moduleId], []);
}
}
},
{ deep: true }
);
//
watch(
() => props.formModeValue,
() => {
// params
params.value = {
...props.formModeValue,
};
},
{ deep: true }
);
const showDrawer = ref<boolean>(false);
function associatedFile() {
showDrawer.value = true;
}
watch(
() => stepData.value,
(val) => {
setProps({ data: val });
},
{ deep: true }
);
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
fileList.value = _fileList.map((e) => {
return {
...e,
enable: true, //
local: true, //
};
});
}
const rowLength = ref<number>(0);
const formRuleField = ref<FormItem[][]>([]);
const formRules = ref<FormItem[]>([]);
const fApi = ref<any>({});
const formRuleList = computed(() => formCreateStore.formCreateRuleMap.get(FormCreateKeyEnum.CASE_MANAGEMENT_FIELD));
//
const getFormRules = () => {
formRuleField.value = [];
formRules.value = [];
if (selectData.value && selectData.value.length) {
selectData.value.forEach((item: any) => {
const currentFormItem = item.formRules?.map((rule: any) => {
let optionsItem = [];
if (rule.options && rule.options.length) {
optionsItem = rule.options.map((opt: any) => {
return {
text: opt.label,
value: opt.value,
};
});
}
return {
type: item.type,
name: item.id,
label: item.name,
value: isEdit.value ? JSON.parse(rule.value) : rule.value,
options: optionsItem,
required: item.required,
props: {
modelValue: isEdit.value ? JSON.parse(rule.value) : rule.value,
options: optionsItem,
},
};
});
formRuleField.value.push(currentFormItem as FormItem[]);
});
formRules.value = formRuleField.value.flatMap((item) => item);
}
};
//
watch(
() => formRuleList.value,
() => {
const customFieldsMaps: Record<string, any> = {};
formRuleList.value?.forEach((item) => {
customFieldsMaps[item.field as string] = item.value;
});
form.value.customFields = customFieldsMaps as Record<string, any>;
},
{ deep: true }
);
// formCreate
watch(
() => selectData.value,
() => {
getFormRules();
rowLength.value = formRules.value.length + 2;
},
{ deep: true }
);
onMounted(() => {
setProps({ data: stepData.value });
});
onBeforeUnmount(() => {
formRules.value = [];
formRuleField.value = [];
});
//
function transferFile(item: any) {}
//
function downloadFile(item: any) {}
//
function cancelAssociated(item: any) {}
defineExpose({
caseFormRef,
formRef,
fApi,
});
</script>
<style scoped lang="less">
.wrapper-preview {
display: flex;
.preview-left {
width: calc(100% - 396px);
border-right: 1px solid var(--color-text-n8);
.changeType {
padding: 2px 4px;
border-radius: 4px;
color: var(--color-text-4);
:deep(.arco-icon-down) {
font-size: 14px;
}
&:hover {
color: rgb(var(--primary-5));
background: rgb(var(--primary-1));
cursor: pointer;
}
}
}
.preview-right {
width: 428px;
}
}
.circle {
width: 16px;
height: 16px;
line-height: 16px;
border-radius: 50%;
text-align: center;
color: var(--color-text-4);
background: var(--color-text-n8);
}
</style>

View File

@ -0,0 +1,366 @@
<template>
<a-input-search
v-model:model-value="groupKeyword"
:placeholder="t('featureTest.featureCase.searchTip')"
allow-clear
class="mb-[16px]"
></a-input-search>
<a-spin class="w-full" :style="{ height: `calc(100vh - 356px)` }" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys"
:data="caseTree"
:keyword="groupKeyword"
:node-more-actions="caseMoreActions"
:expand-all="props.isExpandAll"
:empty-text="t('featureTest.featureCase.caseEmptyContent')"
draggable
:virtual-list-props="virtualListProps"
block-node
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
title-tooltip-position="left"
@select="caseNodeSelect"
@more-action-select="handleCaseMoreSelect"
@more-actions-close="moreActionsClose"
@drop="handleDrag"
>
<template #title="nodeData">
<span class="text-[var(--color-text-1)]">{{ nodeData.name }}</span>
<span class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</span>
</template>
<template v-if="!props.isModal" #extra="nodeData">
<MsPopConfirm
:visible="addSubVisible"
:is-delete="false"
:all-names="[]"
:title="t('featureTest.featureCase.addSubModule')"
:ok-text="t('common.confirm')"
:field-config="{
placeholder: t('featureTest.featureCase.addGroupTip'),
}"
:loading="confirmLoading"
@confirm="addSubModule"
@cancel="resetFocusNodeKey"
>
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusKey(nodeData)">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</MsPopConfirm>
<MsPopConfirm
:title="t('featureTest.featureCase.rename')"
:all-names="[]"
:is-delete="false"
:ok-text="t('common.confirm')"
:field-config="{ field: renameCaseName }"
:loading="confirmLoading"
@confirm="updateNameModule"
@cancel="resetFocusNodeKey"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</MsPopConfirm>
</template>
</MsTree>
</a-spin>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import {
createCaseModuleTree,
deleteCaseModuleTree,
getCaseModuleTree,
moveCaseModuleTree,
updateCaseModuleTree,
} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { mapTree } from '@/utils';
import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const { t } = useI18n();
const { openModal } = useModal();
const appStore = useAppStore();
const focusNodeKey = ref<string>('');
const loading = ref(false);
const props = defineProps<{
isModal?: boolean; //
activeFolder?: string; // 使
selectedKeys?: Array<string | number>; // key
isExpandAll: boolean; //
allNames?: string[]; // name
modulesCount?: Record<string, number>; //
}>();
const emits = defineEmits(['update:selectedKeys', 'caseNodeSelect', 'init']);
const currentProjectId = computed(() => appStore.currentProjectId);
const groupKeyword = ref<string>('');
const caseTree = ref<ModuleTreeNode[]>([]);
const setFocusKey = (node: MsTreeNodeData) => {
focusNodeKey.value = node.id || '';
};
const caseMoreActions: ActionsItem[] = [
{
label: 'featureTest.featureCase.rename',
eventTag: 'rename',
},
{
label: 'featureTest.featureCase.delete',
eventTag: 'delete',
danger: true,
},
];
const selectedNodeKeys = ref(props.selectedKeys || []);
watch(
() => props.selectedKeys,
(val) => {
selectedNodeKeys.value = val || [];
}
);
watch(
() => selectedNodeKeys.value,
(val) => {
emits('update:selectedKeys', val);
}
);
const featureCaseStore = useFeatureCaseStore();
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules(isSetDefaultKey = false) {
try {
loading.value = true;
const res = await getCaseModuleTree(currentProjectId.value);
caseTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: e.id !== 'root' && !props.isModal,
disabled: e.id === props.activeFolder && props.isModal,
count: props.modulesCount?.[e.id] || 0,
};
});
featureCaseStore.setModulesTree(caseTree.value);
if (isSetDefaultKey) {
selectedNodeKeys.value = [caseTree.value[0].id];
}
emits(
'init',
caseTree.value.map((e) => e.name)
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
//
const deleteHandler = (node: MsTreeNodeData) => {
openModal({
type: 'error',
title: t('featureTest.featureCase.deleteTipTitle', { name: node.name }),
content: t('featureTest.featureCase.deleteCaseTipContent'),
okText: t('featureTest.featureCase.deleteConfirm'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
try {
await deleteCaseModuleTree(node.id);
Message.success(t('featureTest.featureCase.deleteSuccess'));
initModules(selectedNodeKeys.value[0] === node.id);
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
};
const renamePopVisible = ref(false);
const renameCaseName = ref('');
function resetFocusNodeKey() {
focusNodeKey.value = '';
renamePopVisible.value = false;
renameCaseName.value = '';
}
//
const caseNodeSelect = (selectedKeys: (string | number)[], node: MsTreeNodeData) => {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emits('caseNodeSelect', selectedKeys, offspringIds);
};
//
const handleCaseMoreSelect = (item: ActionsItem, node: MsTreeNodeData) => {
switch (item.eventTag) {
case 'delete':
deleteHandler(node);
resetFocusNodeKey();
break;
case 'rename':
renameCaseName.value = node.name || '';
renamePopVisible.value = true;
document.querySelector(`#renameSpan${node.id}`)?.dispatchEvent(new Event('click'));
break;
default:
break;
}
};
/**
* 处理文件夹树节点拖拽事件
* @param tree 树数据
* @param dragNode 拖拽节点
* @param dropNode 释放节点
* @param dropPosition 释放位置
*/
async function handleDrag(
tree: MsTreeNodeData[],
dragNode: MsTreeNodeData,
dropNode: MsTreeNodeData,
dropPosition: number
) {
try {
loading.value = true;
await moveCaseModuleTree({
dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '',
dropPosition,
});
Message.success(t('featureTest.featureCase.moduleMoveSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
initModules();
}
}
const moreActionsClose = () => {
if (!renamePopVisible.value) {
resetFocusNodeKey();
}
};
const addSubVisible = ref(false);
const confirmLoading = ref(false);
//
async function addSubModule(formValue?: { field: string }, cancel?: () => void) {
try {
confirmLoading.value = true;
const params: CreateOrUpdateModule = {
projectId: currentProjectId.value,
name: formValue?.field as string,
parentId: focusNodeKey.value,
};
await createCaseModuleTree(params);
Message.success(t('featureTest.featureCase.addSuccess'));
if (cancel) {
cancel();
}
initModules();
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
//
async function updateNameModule(formValue?: { field: string }, cancel?: () => void) {
try {
confirmLoading.value = true;
const params: UpdateModule = {
id: focusNodeKey.value,
name: formValue?.field as string,
};
await updateCaseModuleTree(params);
Message.success(t('featureTest.featureCase.addSuccess'));
if (cancel) {
cancel();
}
initModules();
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 360px)',
};
});
watch(
() => props.activeFolder,
(val) => {
if (val === 'all') {
initModules();
}
}
);
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
caseTree.value = mapTree<ModuleTreeNode>(caseTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
onBeforeMount(() => {
initModules();
});
defineExpose({
initModules,
});
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,208 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('featureTest.featureCase.associatedFile')"
:ok-text="t('featureTest.featureCase.associated')"
:ok-loading="drawerLoading"
:width="480"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<div class="header mb-6 flex justify-between">
<span class="font-medium">{{ t('featureTest.featureCase.SelectExportRange') }}</span>
<span class="text-[rgb(var(--primary-5))]">{{ t('featureTest.featureCase.clear') }}</span>
</div>
<div>
<a-checkbox class="mb-4" :model-value="checkedAll" :indeterminate="indeterminate" @change="handleChangeAll"
><div class="flex items-center">
<span class="mr-1">{{ t('featureTest.featureCase.baseField') }}</span
><span
><icon-up
v-if="foldBaseFields"
class="text-[12px] text-[var(--color-text-brand)]"
@click="toggle('base')" /><icon-right
v-else
class="text-[12px] text-[var(--color-text-brand)]"
@click="toggle('base')"
/></span>
</div>
</a-checkbox>
</div>
<a-checkbox-group v-if="foldBaseFields" v-model="baseFields" class="checkboxContainer" @change="handleChange">
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
</a-checkbox-group>
<!-- 自定义字段 -->
<div>
<a-checkbox class="mb-4" :model-value="checkedAll" :indeterminate="indeterminate" @change="handleChangeAll"
><div class="flex items-center">
<span class="mr-1">{{ t('featureTest.featureCase.customField') }}</span
><span
><icon-up
v-if="foldCustomFields"
class="text-[12px] text-[var(--color-text-brand)]"
@click="toggle('custom')" /><icon-right
v-else
class="text-[12px] text-[var(--color-text-brand)]"
@click="toggle('custom')"
/></span>
</div>
</a-checkbox>
</div>
<a-checkbox-group v-if="foldCustomFields" v-model="customFields" class="checkboxContainer" @change="handleChange">
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
</a-checkbox-group>
<!-- 其他字段 -->
<div>
<a-checkbox class="mb-4" :model-value="checkedAll" :indeterminate="indeterminate" @change="handleChangeAll"
><div class="flex items-center">
<span class="mr-1 flex items-center"
>{{ t('featureTest.featureCase.otherFields') }}<span></span>
<a-tooltip
:content="t('featureTest.featureCase.otherFieldsToolTip')"
position="top"
:mouse-enter-delay="500"
mini
>
<icon-question-circle
class="mx-1 text-[16px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]" /></a-tooltip
><icon-up
v-if="foldCustomFields"
class="text-[12px] text-[var(--color-text-brand)]"
@click="toggle('other')" /><icon-right
v-else
class="text-[12px] text-[var(--color-text-brand)]"
@click="toggle('other')"
/></span>
</div>
</a-checkbox>
</div>
<a-checkbox-group v-if="foldOtherFields" v-model="otherFields" class="checkboxContainer" @change="handleChange">
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
<div class="item checkbox">
<a-checkbox value="1">Option 1</a-checkbox>
</div>
</a-checkbox-group>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
}>();
const showDrawer = ref<boolean>(false);
const drawerLoading = ref<boolean>(false);
const checkedAll = ref<boolean>(false);
const indeterminate = ref<boolean>(false);
function handleChangeAll() {}
function handleChange() {}
function handleDrawerConfirm() {}
function handleDrawerCancel() {
showDrawer.value = false;
}
const foldBaseFields = ref<boolean>(true); //
const baseFields = ref<string[]>([]);
const foldCustomFields = ref<boolean>(true);
const customFields = ref<string[]>([]);
const foldOtherFields = ref<boolean>(true);
const otherFields = ref<string[]>([]);
function toggle(foldType: string) {
if (foldType === 'base') {
foldBaseFields.value = !foldBaseFields.value;
} else if (foldType === 'custom') {
foldCustomFields.value = !foldCustomFields.value;
} else {
foldOtherFields.value = !foldOtherFields.value;
}
}
watch(
() => props.visible,
(val) => {
showDrawer.value = val;
}
);
watch(
() => showDrawer.value,
(val) => {
emit('update:visible', val);
}
);
</script>
<style scoped lang="less">
.checkboxContainer {
display: grid;
margin-bottom: 16px;
grid-template-columns: repeat(auto-fit, minmax(116px, 1fr));
grid-gap: 16px;
.checkbox {
width: 90px;
white-space: nowrap;
@apply overflow-hidden text-ellipsis;
}
}
</style>

View File

@ -0,0 +1,706 @@
<template>
<div class="pageWrap">
<MsSplitBox>
<template #left>
<div class="p-[24px]">
<div class="mb-4 flex items-center">
<div class="back" @click="handleBack"><icon-arrow-left /></div>
<div
>{{ t('featureTest.featureCase.recycle')
}}<span class="ml-1 text-[var(--color-text-4)]">({{ recycleModulesCount.all }})</span></div
>
</div>
<a-divider class="my-[4px]" />
<div class="feature-case">
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.allCase') }}</div>
<div class="folder-count">({{ allCaseCount }})</div></div
>
<div class="ml-auto flex items-center">
<a-tooltip
:content="
isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')
"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
</div>
</div>
<a-input-search
v-model:model-value="groupKeyword"
:placeholder="t('featureTest.featureCase.searchTip')"
allow-clear
class="mb-[16px]"
></a-input-search>
<a-spin class="w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="selectedKeys"
:data="caseTree"
:keyword="groupKeyword"
:expand-all="isExpandAll"
:empty-text="t('featureTest.featureCase.caseEmptyContent')"
draggable
:virtual-list-props="virtualListProps"
block-node
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
title-tooltip-position="left"
@select="caseNodeSelect"
>
<template #title="nodeData">
<div @click="setFocusKey(nodeData)">
<span class="text-[var(--color-text-1)]">{{ nodeData.name }}</span>
<span class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</span>
</div>
</template>
</MsTree>
</a-spin>
</div>
</div>
</template>
<template #right>
<div class="p-[24px]">
<div class="page-header mb-4 h-[34px]">
<div class="text-[var(--color-text-1)]"
>{{ t('featureTest.featureCase.allCase') }}
<span class="text-[var(--color-text-4)]"> ({{ recycleModulesCount.all }})</span></div
>
<div class="flex w-[80%] items-center justify-end">
<a-select class="w-[240px]" :placeholder="t('featureTest.featureCase.versionPlaceholder')">
<a-option v-for="version of versionOptions" :key="version.id" :value="version.id">{{
version.name
}}</a-option>
</a-select>
<a-input-search
v-model="keyword"
:placeholder="t('featureTest.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
></a-input-search>
</div>
</div>
<ms-base-table
v-bind="propsRes"
:action-config="tableBatchActions"
@selected-change="handleTableSelect"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #reviewStatus="{ record }">
<MsIcon
:type="getStatusText(record.reviewStatus)?.iconType || ''"
class="mr-1"
:class="[getReviewStatusClass(record.reviewStatus)]"
></MsIcon>
<span>{{ getStatusText(record.reviewStatus)?.statusType || '' }} </span>
</template>
<template #lastExecuteResult="{ record }">
<MsIcon
:type="getStatusText(record.lastExecuteResult)?.iconType || ''"
class="mr-1"
:class="[getReviewStatusClass(record.lastExecuteResult)]"
></MsIcon>
<span>{{ getStatusText(record.lastExecuteResult)?.statusType || '' }}</span>
</template>
<template #moduleId="{ record }">
<a-tooltip :content="getModules(record.moduleId)" position="top">
<span class="one-line-text inline-block">{{ getModules(record.moduleId) }}</span>
</a-tooltip>
</template>
<template #operation="{ record }">
<MsButton @click="recoverCase(record.id)">{{ t('featureTest.featureCase.batchRecover') }}</MsButton>
<MsButton class="!mr-0" @click="handleBatchCleanOut(record)">{{
t('featureTest.featureCase.batchCleanOut')
}}</MsButton>
</template>
</ms-base-table>
</div>
</template>
</MsSplitBox>
</div>
</template>
<script setup lang="ts">
/*
* @description 功能用例-回收站
*/
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import {
batchDeleteRecycleCase,
deleteRecycleCaseList,
getRecycleListRequest,
getTrashCaseModuleTree,
recoverRecycleCase,
restoreCaseList,
} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore, useTableStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import { characterLimit, findNodePathByKey, mapTree } from '@/utils';
import type {
BatchMoveOrCopyType,
CaseManagementTable,
CaseModuleQueryParams,
} from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/projectManagement/file';
import { StatusType } from '@/enums/caseEnum';
import { FeatureTestRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { getReviewStatusClass, getStatusText } from './utils';
import debounce from 'lodash-es/debounce';
const tableStore = useTableStore();
const featureCaseStore = useFeatureCaseStore();
const { t } = useI18n();
const router = useRouter();
const { openModal } = useModal();
const allCaseCount = ref<number>(0);
const activeCaseType = ref<'folder' | 'module'>('folder'); //
const appStore = useAppStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const versionOptions = ref([
{
id: '1001',
name: 'v_1.0',
},
]);
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getRecycleListRequest,
{
tableKey: TableKeyEnum.FILE_MANAGEMENT_CASE_RECYCLE,
scroll: { x: 3200 },
selectable: true,
showSetting: true,
heightUsed: 340,
enableDrag: true,
},
(record) => ({
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
})
);
const columns: MsTableColumn = [
{
title: 'featureTest.featureCase.tableColumnID',
dataIndex: 'id',
width: 200,
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
ellipsis: true,
showDrag: false,
},
{
title: 'featureTest.featureCase.tableColumnName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
sortable: {
sortDirections: ['ascend', 'descend'],
},
ellipsis: true,
showDrag: false,
},
{
title: 'featureTest.featureCase.tableColumnLevel',
dataIndex: 'level',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCaseState',
dataIndex: 'caseState',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnReviewResult',
dataIndex: 'reviewStatus',
slotName: 'reviewStatus',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnExecutionResult',
dataIndex: 'lastExecuteResult',
slotName: 'lastExecuteResult',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnVersion',
slotName: 'versionId',
dataIndex: 'versionId',
width: 300,
showTooltip: true,
showInTable: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnModule',
slotName: 'moduleId',
showInTable: true,
width: 300,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnTag',
slotName: 'tags',
dataIndex: 'tags',
showInTable: true,
isTag: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCreateUser',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnUpdateUser',
slotName: 'updateUser',
dataIndex: 'updateUser',
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnUpdateTime',
slotName: 'updateTime',
dataIndex: 'updateTime',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'featureTest.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const tableBatchActions = {
baseAction: [
{
label: 'featureTest.featureCase.batchRecover',
eventTag: 'batchRecover',
},
{
label: 'featureTest.featureCase.batchCleanOut',
eventTag: 'batchCleanOut',
danger: true,
},
],
};
const tableSelected = ref<(string | number)[]>([]);
function handleTableSelect(selectArr: (string | number)[]) {
tableSelected.value = selectArr;
}
const isExpandAll = ref(false);
//
function expandHandler() {
isExpandAll.value = !isExpandAll.value;
}
const activeFolder = ref<string>('all');
//
function getActiveClass(type: string) {
return activeFolder.value === type ? 'folder-text case-active' : 'folder-text';
}
//
function setActiveFolder(type: string) {
activeFolder.value = type;
if (type === 'all') {
activeCaseType.value = 'folder';
}
}
//
const selectedKeys = computed({
get: () => [activeFolder.value],
set: (val) => val,
});
const offspringIds = ref<string[]>([]);
const selectedKeysNode = ref<(string | number)[]>([]);
const focusNodeKey = ref<string>('');
//
const caseNodeSelect = (selectedNodeKeys: (string | number)[] | string[], node: MsTreeNodeData) => {
[activeFolder.value] = selectedNodeKeys as string[];
offspringIds.value = [];
mapTree(node.children || [], (e) => {
offspringIds.value.push(e.id);
return e;
});
focusNodeKey.value = '';
};
const setFocusKey = (node: MsTreeNodeData) => {
focusNodeKey.value = node.id || '';
};
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 316px)',
};
});
const loading = ref(false);
const caseTree = ref<ModuleTreeNode[]>([]);
const recycleModulesCount = ref<Record<string, number>>({});
const groupKeyword = ref<string>('');
/**
* @param 获取回收站模块
*/
async function getRecycleModules(isSetDefaultKey = false) {
try {
loading.value = true;
const res = await getTrashCaseModuleTree(currentProjectId.value);
caseTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: false,
disabled: false,
count: recycleModulesCount.value?.[e.id] || 0,
};
});
if (isSetDefaultKey) {
selectedKeysNode.value = [caseTree.value[0].id];
}
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
const keyword = ref<string>('');
const searchParams = ref<TableQueryParams>({
projectId: currentProjectId.value,
moduleIds: [],
});
// count
const emitTableParams: CaseModuleQueryParams = {
keyword: keyword.value,
moduleIds: [],
projectId: currentProjectId.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
};
//
function initRecycleModulesCount() {
featureCaseStore.getRecycleMModulesCountCount(emitTableParams);
}
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
currentSelectCount: 0,
});
//
function getBatchParams(): BatchMoveOrCopyType {
return {
excludeIds: batchParams.value.excludeIds,
selectAll: batchParams.value.selectAll,
selectIds: batchParams.value.selectedIds,
condition: {
keyword: keyword.value,
},
moduleIds: searchParams.value.moduleIds,
projectId: currentProjectId.value,
};
}
//
async function handleBatchRecover() {
try {
await restoreCaseList(getBatchParams());
Message.success(t('featureTest.featureCase.recoveredSuccessfully'));
loadList();
resetSelector();
initRecycleModulesCount();
} catch (error) {
console.log(error);
}
}
//
async function handleBatchDelete() {
openModal({
type: 'error',
title: t('featureTest.featureCase.batchDelete', { number: batchParams.value.currentSelectCount }),
content: t('featureTest.featureCase.cleanOutDeleteTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await batchDeleteRecycleCase(getBatchParams());
Message.success(t('common.deleteSuccess'));
loadList();
resetSelector();
initRecycleModulesCount();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
//
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = { ...params };
if (event.eventTag === 'batchRecover') {
handleBatchRecover();
} else {
handleBatchDelete();
}
}
// name
function getModules(moduleIds: string) {
const modules = findNodePathByKey(caseTree.value, moduleIds, undefined, 'id');
if (modules) {
const moduleName = (modules || []).treePath.map((item: any) => item.name);
if (moduleName.length === 1) {
return moduleName[0];
}
return `/${moduleName.join('/')}`;
}
}
//
function getLoadListParams() {
if (activeFolder.value === 'all') {
searchParams.value.moduleIds = [];
} else {
searchParams.value.moduleIds = [activeFolder.value, ...offspringIds.value];
}
setLoadListParams({
...searchParams.value,
keyword: keyword.value,
});
}
//
function initRecycleList() {
getLoadListParams();
loadList();
}
const searchList = debounce(() => {
getLoadListParams();
loadList();
}, 100);
//
async function recoverCase(id: string) {
try {
await recoverRecycleCase(id);
Message.success(t('featureTest.featureCase.recoveredSuccessfully'));
loadList();
resetSelector();
initRecycleModulesCount();
} catch (error) {
console.log(error);
}
}
//
function handleBatchCleanOut(record: CaseManagementTable) {
openModal({
type: 'error',
title: t('featureTest.featureCase.deleteCaseTitle', { name: characterLimit(record.name) }),
content: t('featureTest.featureCase.cleanOutDeleteTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await deleteRecycleCaseList(record.id);
Message.success(t('common.deleteSuccess'));
loadList();
initRecycleModulesCount();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
//
function handleBack() {
router.push({
name: FeatureTestRouteEnum.FEATURE_TEST_CASE,
});
}
watch(
() => activeFolder.value,
() => {
initRecycleList();
}
);
watch(
() => featureCaseStore.recycleModulesCount,
(val) => {
recycleModulesCount.value = { ...val };
caseTree.value = mapTree<ModuleTreeNode>(caseTree.value, (node) => {
return {
...node,
count: val?.[node.id] || 0,
};
});
}
);
onMounted(() => {
initRecycleList();
getRecycleModules();
initRecycleModulesCount();
});
tableStore.initColumn(TableKeyEnum.FILE_MANAGEMENT_CASE_RECYCLE, columns, 'drawer');
</script>
<style scoped lang="less">
.pageWrap {
min-width: 1000px;
height: calc(100vh - 136px);
border-radius: var(--border-radius-large);
@apply bg-white;
.back {
margin-right: 8px;
width: 20px;
height: 20px;
border: 1px solid #ffffff;
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
.arco-icon {
color: rgb(var(--primary-5));
}
@apply flex cursor-pointer items-center rounded-full;
}
.case {
padding: 8px 4px;
border-radius: var(--border-radius-small);
@apply flex cursor-pointer items-center justify-between;
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
.case-active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
}
.page-header {
@apply flex items-center justify-between;
}
</style>

View File

@ -0,0 +1,93 @@
import { useI18n } from '@/hooks/useI18n';
import { StatusType } from '@/enums/caseEnum';
const { t } = useI18n();
// 获取列表对应的状态图标
const statusIconMap = [
{
key: 'UN_REVIEWED',
icon: StatusType.UN_REVIEWED,
statusText: t('featureTest.featureCase.notReviewed'),
},
{
key: 'UNDER_REVIEWED',
icon: StatusType.UNDER_REVIEWED,
statusText: t('featureTest.featureCase.reviewing'),
},
{
key: 'PASS',
icon: StatusType.PASS,
statusText: t('featureTest.featureCase.passed'),
},
{
key: 'UN_PASS',
icon: StatusType.UN_PASS,
statusText: t('featureTest.featureCase.notPass'),
},
{
key: 'RE_REVIEWED',
icon: StatusType.RE_REVIEWED,
statusText: t('featureTest.featureCase.retrial'),
},
{
key: 'UN_EXECUTED',
icon: StatusType.UN_EXECUTED,
statusText: t('featureTest.featureCase.nonExecution'),
},
{
key: 'PASSED',
icon: StatusType.PASSED,
statusText: t('featureTest.featureCase.passed'),
},
{
key: 'FAILED',
icon: StatusType.FAILED,
statusText: t('featureTest.featureCase.failure'),
},
{
key: 'BLOCKED',
icon: StatusType.BLOCKED,
statusText: t('featureTest.featureCase.chokeUp'),
},
{
key: 'SKIPPED',
icon: StatusType.SKIPPED,
statusText: t('featureTest.featureCase.skip'),
},
];
/** *
*
* @description
* @param {status}
*/
export function getStatusText(status: keyof typeof StatusType) {
const currentStatus = statusIconMap.find((item) => item.key === status);
return {
iconType: currentStatus?.icon,
statusType: currentStatus?.statusText,
};
}
/** *
*
* @description
* @param {status}
*/
export function getReviewStatusClass(status: keyof typeof StatusType) {
const grayColor = ['UN_REVIEWED', 'UN_EXECUTED'];
const yellowColor = ['RE_REVIEWED', 'BLOCKED'];
const blueColor = ['UNDER_REVIEWED', 'SKIPPED'];
if (grayColor.includes(status)) {
return 'text-[var(--color-text-brand)]';
}
if (yellowColor.includes(status)) {
return 'text-[rgb(var(--warning-6))]';
}
if (blueColor.includes(status)) {
return 'text-[rgb(var(--link-6))]';
}
}
export default {};

View File

@ -0,0 +1,319 @@
<template>
<div class="mb-[16px]">
<a-button type="primary" class="mr-[12px]" @click="caseDetail">
{{ t('featureTest.featureCase.creatingCase') }}
</a-button>
<a-button type="outline"> {{ t('featureTest.featureCase.importCase') }} </a-button>
</div>
<div class="pageWrap">
<MsSplitBox>
<template #left>
<div class="p-[24px] pb-0">
<div class="feature-case h-[100%]">
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('public')">
<MsIcon type="icon-icon_folder_outlined-1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.publicCase') }}</div>
<div class="folder-count">({{ publicCaseCount }})</div></div
>
<div class="back"><icon-arrow-right /></div>
</div>
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.allCase') }}</div>
<div class="folder-count">({{ modulesCount.all }})</div></div
>
<div class="ml-auto flex items-center">
<a-tooltip
:content="
isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')
"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<MsPopConfirm
ref="confirmRef"
v-model:visible="addSubVisible"
:is-delete="false"
:title="t('featureTest.featureCase.addSubModule')"
:all-names="rootModulesName"
:loading="confirmLoading"
:ok-text="t('common.confirm')"
:field-config="{
placeholder: t('featureTest.featureCase.addGroupTip'),
}"
@confirm="confirmHandler"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</MsPopConfirm>
</div>
</div>
<a-divider class="my-[8px]" />
<FeatureCaseTree
ref="caseTreeRef"
v-model:selected-keys="selectedKeys"
:all-names="rootModulesName"
:active-folder="activeFolder"
:is-expand-all="isExpandAll"
:modules-count="modulesCount"
@case-node-select="caseNodeSelect"
@init="setRootModules"
></FeatureCaseTree>
<div class="b-0 absolute w-[88%]">
<a-divider class="!my-0 !mb-2" />
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
<MsIcon type="icon-icon_delete-trash_outlined" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.recycle') }}</div>
<div class="folder-count">({{ recycleModulesCount.all }})</div></div
>
</div>
</div>
</div>
</div>
</template>
<template #right>
<div class="p-[24px]">
<CaseTable
:active-folder="activeFolder"
:offspring-ids="offspringIds"
:active-folder-type="activeCaseType"
:modules-count="modulesCount"
@init="initModulesCount"
></CaseTable>
</div>
</template>
</MsSplitBox>
</div>
</template>
<script setup lang="ts">
/**
* @description 功能测试-功能用例
*/
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import CaseTable from './components/caseTable.vue';
import FeatureCaseTree from './components/caseTree.vue';
import { createCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import type { CaseModuleQueryParams, CreateOrUpdateModule } from '@/models/caseManagement/featureCase';
import { FeatureTestRouteEnum } from '@/enums/routeEnum';
import Message from '@arco-design/web-vue/es/message';
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const currentProjectId = computed(() => appStore.currentProjectId);
const isExpandAll = ref(false);
const activeCaseType = ref<'folder' | 'module'>('folder'); //
const rootModulesName = ref<string[]>([]); //
const publicCaseCount = ref<number>(0); //
//
const expandHandler = () => {
isExpandAll.value = !isExpandAll.value;
};
const activeFolder = ref<string>('all');
//
const selectedKeys = computed({
get: () => [activeFolder.value],
set: (val) => val,
});
const offspringIds = ref<string[]>([]);
// ||
const setActiveFolder = (type: string) => {
activeFolder.value = type;
if (['public', 'all', 'recycle'].includes(type)) {
activeCaseType.value = 'folder';
}
if (type === 'recycle') {
router.push({
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_RECYCLE,
});
}
};
//
const getActiveClass = (type: string) => {
return activeFolder.value === type ? 'folder-text case-active' : 'folder-text';
};
const featureCaseStore = useFeatureCaseStore();
//
function caseNodeSelect(keys: string[], _offspringIds: string[]) {
[activeFolder.value] = keys;
activeCaseType.value = 'module';
offspringIds.value = [..._offspringIds];
featureCaseStore.setModuleId(keys, offspringIds.value);
}
const confirmLoading = ref(false);
const confirmRef = ref();
const addSubVisible = ref(false);
const caseTreeRef = ref();
//
const confirmHandler = async () => {
try {
confirmLoading.value = true;
const { field } = confirmRef.value.form;
if (!confirmRef.value.isPass) {
return;
}
const params: CreateOrUpdateModule = {
projectId: currentProjectId.value,
name: field,
parentId: 'none',
};
await createCaseModuleTree(params);
Message.success(t('featureTest.featureCase.addSuccess'));
caseTreeRef.value.initModules();
addSubVisible.value = false;
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
};
/**
* 设置根模块名称列表
* @param names 根模块名称列表
*/
function setRootModules(names: string[]) {
rootModulesName.value = names;
}
//
const tableFilterParams = ref<CaseModuleQueryParams>({
moduleIds: [],
projectId: '',
});
const modulesCount = computed(() => {
return featureCaseStore.modulesCount;
});
const recycleModulesCount = computed(() => {
return featureCaseStore.recycleModulesCount;
});
/**
* 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量
*/
function initModulesCount(params: CaseModuleQueryParams) {
featureCaseStore.getCaseModulesCountCount(params);
featureCaseStore.getRecycleMModulesCountCount(params);
tableFilterParams.value = { ...params };
}
//
function caseDetail() {
router.push({
name: FeatureTestRouteEnum.FEATURE_TEST_CASE_DETAIL,
});
}
//
router.beforeEach((to: any, from: any, next) => {
const routeEnumValues = Object.values(FeatureTestRouteEnum);
if (!routeEnumValues.includes(to.name)) {
//
featureCaseStore.setIsAlreadySuccess(false);
}
next();
});
onMounted(() => {
if (featureCaseStore.operatingState) {
[activeFolder.value] = featureCaseStore.moduleId;
}
});
</script>
<style scoped lang="less">
.pageWrap {
min-width: 1000px;
height: calc(100vh - 136px);
border-radius: var(--border-radius-large);
@apply bg-white;
.case {
padding: 8px 4px;
border-radius: var(--border-radius-small);
@apply flex cursor-pointer items-center justify-between;
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
.case-active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
.back {
margin-right: 8px;
width: 20px;
height: 20px;
border: 1px solid #ffffff;
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
.arco-icon {
color: rgb(var(--primary-5));
}
@apply flex cursor-pointer items-center rounded-full;
}
}
}
.recycle {
@apply absolute bottom-0 bg-white pb-4;
:deep(.arco-divider-horizontal) {
margin: 8px 0;
}
.recycle-bin {
@apply bottom-0 flex items-center bg-white;
.recycle-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
}
</style>

View File

@ -0,0 +1,108 @@
export default {
'featureTest.featureCase.creatingCase': 'Create Case',
'featureTest.featureCase.importCase': 'Import Case',
'featureTest.featureCase.publicCase': 'Public of Cases',
'featureTest.featureCase.allCase': 'All of Cases',
'featureTest.featureCase.searchTip': 'Please enter a group name',
'featureTest.featureCase.caseEmptyContent': 'No use case data yet, please click the button above to create or import',
'featureTest.featureCase.addSubModule': 'Add submodules',
'featureTest.featureCase.rename': 'rename',
'featureTest.featureCase.recycle': 'Recycle',
'featureTest.featureCase.versionPlaceholder': 'The default is the latest version',
'featureTest.featureCase.searchByNameAndId': 'Search by ID or name',
'featureTest.featureCase.filter': 'filter',
'featureTest.featureCase.setFilterCondition': 'Set filters',
'featureTest.featureCase.followingCondition': 'Conform to the following',
'featureTest.featureCase.condition': 'Condition',
'featureTest.featureCase.delete': 'delete',
'featureTest.featureCase.addSubModuleSuccess': 'Add submodule successfully',
'featureTest.featureCase.renameSuccess': 'Rename successful',
'featureTest.featureCase.nameNotNullTip': 'The name can not be null',
'featureTest.featureCase.deleteTipTitle': 'Do you want to delete: {name} use case?',
'featureTest.featureCase.deleteCaseTipContent':
'After the node is deleted, all resources under the node will be deleted. Exercise caution when performing this operation.',
'featureTest.featureCase.deleteConfirm': 'Confirm',
'featureTest.featureCase.deleteSuccess': 'Delete Successfully',
'featureTest.featureCase.addSuccess': 'Add Successfully',
'featureTest.featureCase.addGroupTip': 'Please enter the group name and press enter to save',
'featureTest.featureCase.tableColumnID': 'ID',
'featureTest.featureCase.tableColumnName': 'Case Name',
'featureTest.featureCase.tableColumnLevel': 'Case Level',
'featureTest.featureCase.tableColumnCaseState': 'Case State',
'featureTest.featureCase.tableColumnReviewResult': 'Review Result',
'featureTest.featureCase.tableColumnExecutionResult': 'Execution Result',
'featureTest.featureCase.tableColumnVersion': 'version',
'featureTest.featureCase.tableColumnModule': 'Module',
'featureTest.featureCase.tableColumnTag': 'Tag',
'featureTest.featureCase.tableColumnCreateUser': 'CreateUser',
'featureTest.featureCase.tableColumnCreateTime': 'CreateTime',
'featureTest.featureCase.tableColumnUpdateUser': 'UpdateUser',
'featureTest.featureCase.tableColumnUpdateTime': 'UpdateTime',
'featureTest.featureCase.tableColumnActions': 'operation',
'featureTest.featureCase.beforeDeleteCase':
'The deleted content will be put into the recycle bin, where data can be recovered',
'featureTest.featureCase.deleteCaseTitle': 'Are you sure to delete the {name} use case?',
'featureTest.featureCase.export': 'Export',
'featureTest.featureCase.exportExcel': 'Export spreadsheet (xlsx)',
'featureTest.featureCase.exportXMind': 'Exporting Mind (xmind)',
'featureTest.featureCase.moveTo': 'Move to',
'featureTest.featureCase.copyTo': 'Copy to',
'featureTest.featureCase.associatedDemand': 'Associated demand',
'featureTest.featureCase.generatingDependencies': 'Generative dependency',
'featureTest.featureCase.addToPublic': 'Add to public case',
'featureTest.featureCase.updateCase': 'Update Case',
'featureTest.featureCase.latestTemplate': 'The default is the latest template',
'featureTest.featureCase.copyStep': 'Copy Step',
'featureTest.featureCase.InsertStepsBefore': 'Insert steps before',
'featureTest.featureCase.afterInsertingSteps': 'After inserting steps',
'featureTest.featureCase.associatedFile': 'Associated File',
'featureTest.featureCase.associated': 'Associated',
'featureTest.featureCase.fileType': 'File Type',
'featureTest.featureCase.fileName': 'fileName',
'featureTest.featureCase.description': 'Description',
'featureTest.featureCase.tags': 'Tags',
'featureTest.featureCase.enableTags': `On: adds a label`,
'featureTest.featureCase.closeTags': 'Off: overwrites an existing label',
'featureTest.featureCase.appendTag': 'appendTag',
'featureTest.featureCase.batchEdit': 'Batch editing ({number} use cases selected)',
'featureTest.featureCase.selectAttrs': 'select attributes',
'featureTest.featureCase.batchUpdate': 'Batch update to',
'featureTest.featureCase.batchDelete': 'Are you sure to delete the use case {number} ?',
'featureTest.featureCase.batchMoveTitle': 'Batch Move',
'featureTest.featureCase.batchMove': '({number} selected use cases)',
'featureTest.featureCase.batchMoveSelectedModules': 'Batch Move',
'featureTest.featureCase.batchCopyTitle': 'Batch Copy',
'featureTest.featureCase.batchCopySelectedModules': 'Batch Copy',
'featureTest.featureCase.batchCopy': 'Copy Successfully!',
'featureTest.featureCase.editSuccess': 'update Successfully',
'featureTest.featureCase.PleaseSelect': 'Please select',
'featureTest.featureCase.PleaseInputTags': 'Please enter the update tag to add',
'featureTest.featureCase.expectedResult': 'Expected Result',
'featureTest.featureCase.remark': 'Remark',
'featureTest.featureCase.addAttachment': 'Add attachment',
'featureTest.featureCase.uploadFile': 'Upload File',
'featureTest.featureCase.storage': 'storage',
'featureTest.featureCase.download': 'download',
'featureTest.featureCase.cancelLink': 'Cancel link',
'featureTest.featureCase.SelectExportRange': 'Select export range',
'featureTest.featureCase.clear': 'Clear',
'featureTest.featureCase.baseField': 'Base Field',
'featureTest.featureCase.customField': 'Custom Field',
'featureTest.featureCase.otherFields': 'Other Fields',
'featureTest.featureCase.otherFieldsToolTip': 'Other fields do not support import after export',
'featureTest.featureCase.notReviewed': 'Not reviewed',
'featureTest.featureCase.reviewing': 'In the review',
'featureTest.featureCase.passed': 'Have passed',
'featureTest.featureCase.notPass': 'Not pass',
'featureTest.featureCase.retrial': 'retrial',
'featureTest.featureCase.nonExecution': 'non-execution',
'featureTest.featureCase.failure': 'Failure',
'featureTest.featureCase.chokeUp': 'Choke up',
'featureTest.featureCase.skip': 'skip',
'featureTest.featureCase.batchRecover': 'recover',
'featureTest.featureCase.batchCleanOut': 'Clean out',
'featureTest.featureCase.recoveredSuccessfully': 'Recovered successfully',
'featureTest.featureCase.cleanOutDeleteTip':
'After deletion, the data will not be recovered, please operate with caution!',
'featureTest.featureCase.pleaseEnterInputTags': 'Please enter content Enter add label',
};

View File

@ -0,0 +1,107 @@
export default {
'featureTest.featureCase.creatingCase': '创建用例',
'featureTest.featureCase.importCase': '导入用例',
'featureTest.featureCase.publicCase': '公共用例库',
'featureTest.featureCase.allCase': '全部用例',
'featureTest.featureCase.searchTip': '请输入分组名称',
'featureTest.featureCase.caseEmptyContent': '暂无用例数据,请点击上方按钮创建或导入',
'featureTest.featureCase.addSubModule': '添加子模块',
'featureTest.featureCase.rename': '重命名',
'featureTest.featureCase.recycle': '回收站',
'featureTest.featureCase.versionPlaceholder': '默认为最新版本',
'featureTest.featureCase.searchByNameAndId': '通过 ID 或名称搜索',
'featureTest.featureCase.filter': '筛选',
'featureTest.featureCase.setFilterCondition': '设置筛选条件',
'featureTest.featureCase.followingCondition': '符合以下',
'featureTest.featureCase.condition': '条件',
'featureTest.featureCase.delete': '删除',
'featureTest.featureCase.addSubModuleSuccess': '添加子模块成功',
'featureTest.featureCase.renameSuccess': '重命名成功',
'featureTest.featureCase.nameNotNullTip': '名称不能为空',
'featureTest.featureCase.deleteTipTitle': '是否删除 {name} 用例 ',
'featureTest.featureCase.deleteCaseTipContent': '删除后,此节点下的所有资源都会被删除,请谨慎操作。',
'featureTest.featureCase.deleteConfirm': '确认删除',
'featureTest.featureCase.deleteSuccess': '删除成功',
'featureTest.featureCase.addSuccess': '添加成功',
'featureTest.featureCase.addGroupTip': '请输入分组名称,按回车键保存',
'featureTest.featureCase.moduleMoveSuccess': '请输入分组名称,按回车键保存',
'featureTest.featureCase.tableColumnID': 'ID',
'featureTest.featureCase.tableColumnName': '用例名称',
'featureTest.featureCase.tableColumnLevel': '用例等级',
'featureTest.featureCase.tableColumnCaseState': '用例状态',
'featureTest.featureCase.tableColumnReviewResult': '评审结果',
'featureTest.featureCase.tableColumnExecutionResult': '执行结果',
'featureTest.featureCase.tableColumnVersion': '版本',
'featureTest.featureCase.tableColumnModule': '所属模块',
'featureTest.featureCase.tableColumnTag': '标签',
'featureTest.featureCase.tableColumnCreateUser': '创建人',
'featureTest.featureCase.tableColumnCreateTime': '创建时间',
'featureTest.featureCase.tableColumnUpdateUser': '更新人',
'featureTest.featureCase.tableColumnUpdateTime': '更新时间',
'featureTest.featureCase.tableColumnActions': '操作',
'featureTest.featureCase.beforeDeleteCase': '删除后的内容将放入回收站,可在回收站内进行恢复数据',
'featureTest.featureCase.deleteCaseTitle': '确认删除 {name} 用例 ',
'featureTest.featureCase.export': '导出',
'featureTest.featureCase.exportExcel': '导出 Excel 表格 (xlsx)',
'featureTest.featureCase.exportXMind': '导出思维导图 (xmind)',
'featureTest.featureCase.moveTo': '移动到',
'featureTest.featureCase.copyTo': '复制到',
'featureTest.featureCase.associatedDemand': '关联需求',
'featureTest.featureCase.generatingDependencies': '生成依赖关系',
'featureTest.featureCase.addToPublic': '添加到公共用例库',
'featureTest.featureCase.updateCase': '更新用例',
'featureTest.featureCase.latestTemplate': '默认为最新模版',
'featureTest.featureCase.copyStep': '复制该步骤',
'featureTest.featureCase.InsertStepsBefore': '在之前插入步骤',
'featureTest.featureCase.afterInsertingSteps': '在之后插入步骤',
'featureTest.featureCase.associatedFile': '关联文件',
'featureTest.featureCase.associated': '关联',
'featureTest.featureCase.fileType': '文件类型',
'featureTest.featureCase.fileName': '文件名',
'featureTest.featureCase.description': '描述',
'featureTest.featureCase.tags': '标签',
'featureTest.featureCase.enableTags': `开启:新增标签,关闭:覆盖原有标签`,
'featureTest.featureCase.closeTags': '关闭:覆盖原有标签',
'featureTest.featureCase.appendTag': '追加标签',
'featureTest.featureCase.batchEdit': '批量编辑 (已选 { number } 条用例)',
'featureTest.featureCase.selectAttrs': '选择属性',
'featureTest.featureCase.batchUpdate': '批量更新为',
'featureTest.featureCase.batchDelete': '确认删除 {number} 条用例吗?',
'featureTest.featureCase.batchMoveTitle': '批量移动',
'featureTest.featureCase.batchMove': '(已选 { number } 条用例)',
'featureTest.featureCase.batchMoveSelectedModules': '移动 { number } 个用例至已选模块',
'featureTest.featureCase.batchCopyTitle': '批量复制',
'featureTest.featureCase.batchCopySelectedModules': '复制 { number } 个用例至已选模块',
'featureTest.featureCase.batchCopySuccess': '复制成功',
'featureTest.featureCase.batchMoveSuccess': '移动成功',
'featureTest.featureCase.editSuccess': '更新成功',
'featureTest.featureCase.PleaseSelect': '请选择',
'featureTest.featureCase.PleaseInputTags': '请输入更新标签回车添加',
'featureTest.featureCase.expectedResult': '预期结果',
'featureTest.featureCase.remark': '备注',
'featureTest.featureCase.addAttachment': '添加附件',
'featureTest.featureCase.uploadFile': '上传文件',
'featureTest.featureCase.storage': '转存',
'featureTest.featureCase.download': '下载',
'featureTest.featureCase.cancelLink': '取消关联',
'featureTest.featureCase.SelectExportRange': '选择导出范围',
'featureTest.featureCase.clear': '清空',
'featureTest.featureCase.baseField': '基础字段',
'featureTest.featureCase.customField': '自定义字段',
'featureTest.featureCase.otherFields': '其他字段',
'featureTest.featureCase.otherFieldsToolTip': '其他字段 导出后不支持导入',
'featureTest.featureCase.notReviewed': '未评审',
'featureTest.featureCase.reviewing': '评审中',
'featureTest.featureCase.passed': '已通过',
'featureTest.featureCase.notPass': '未通过',
'featureTest.featureCase.retrial': '重新提审',
'featureTest.featureCase.nonExecution': '未执行',
'featureTest.featureCase.failure': '失败',
'featureTest.featureCase.chokeUp': '阻塞',
'featureTest.featureCase.skip': '跳过',
'featureTest.featureCase.batchRecover': '恢复',
'featureTest.featureCase.batchCleanOut': '彻底删除',
'featureTest.featureCase.recoveredSuccessfully': '恢复成功',
'featureTest.featureCase.cleanOutDeleteTip': '删除后,数据将无法恢复,请谨慎操作!',
'featureTest.featureCase.pleaseEnterInputTags': '请输入内容回车添加标签',
};

View File

@ -1,147 +0,0 @@
<template>
<div class="page-header h-[34px]">
<div class="text-[var(--color-text-1)]"
>{{ t('featureTest.featureCase.allCase') }}
<span class="text-[var(--color-text-4)]"> ({{ allCaseCount }})</span></div
>
<div class="flex w-[80%] items-center justify-end">
<a-select class="w-[240px]" :placeholder="t('featureTest.featureCase.versionPlaceholder')">
<a-option v-for="version of versionOptions" :key="version.id" :value="version.id">{{ version.name }}</a-option>
</a-select>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('featureTest.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
></a-input-search>
<MsTag
:type="isExpandFilter ? 'primary' : 'default'"
:theme="isExpandFilter ? 'lightOutLine' : 'outline'"
size="large"
class="-mt-[3px] min-w-[64px] cursor-pointer"
>
<span :class="!isExpandFilter ? 'text-[var(--color-text-4)]' : ''" @click="isExpandFilterHandler"
><icon-filter class="mr-[4px]" :style="{ 'font-size': '16px' }" />{{
t('featureTest.featureCase.filter')
}}</span
>
</MsTag>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type ml-[4px]">
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio>
<a-radio value="xmind" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_mindnote_outlined" /></a-radio>
</a-radio-group>
</div>
</div>
<FilterPanel v-show="isExpandFilter"></FilterPanel>
<MinderEditor
:import-json="importJson"
:tags="['模块', '用例', '前置条件', '备注', '步骤', '预期结果']"
tag-enable
sequence-enable
@node-click="handleNodeClick"
/>
<MsDrawer v-model:visible="visible" :width="480" :mask="false">
{{ nodeData.text }}
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MinderEditor from '@/components/pure/minder-editor/minderEditor.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import FilterPanel from '@/components/business/ms-filter-panel/searchForm.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const versionOptions = ref([
{
id: '1001',
name: 'v_1.0',
},
]);
const keyword = ref<string>();
const showType = ref<string>('list');
const allCaseCount = ref<number>(100);
const isExpandFilter = ref<boolean>(false);
// ||
const isExpandFilterHandler = () => {
isExpandFilter.value = !isExpandFilter.value;
};
const visible = ref<boolean>(false);
const nodeData = ref<any>({});
const importJson = ref<any>({});
function handleNodeClick(data: any) {
if (data.resource && data.resource.includes('用例')) {
visible.value = true;
nodeData.value = data;
}
}
onBeforeMount(() => {
importJson.value = {
root: {
data: {
text: '测试用例',
id: 'xxxx',
},
children: [
{
data: {
id: 'sdasdas',
text: '模块 1',
resource: ['模块'],
},
},
{
data: {
id: 'dasdasda',
text: '模块 2',
expandState: 'collapse',
},
children: [
{
data: {
id: 'frihofiuho3f',
text: '用例 1',
resource: ['用例'],
},
},
{
data: {
id: 'df09348f034f',
text: ' 用例 2',
resource: ['用例'],
},
},
],
},
],
},
template: 'default',
};
});
</script>
<style scoped lang="less">
.page-header {
@apply flex items-center justify-between;
}
.filter-panel {
background: var(--color-text-n9);
@apply mt-1 rounded-md p-3;
.condition-text {
color: var(--color-text-2);
}
}
</style>

View File

@ -1,293 +0,0 @@
<template>
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys"
:data="caseTree"
:keyword="groupKeyword"
:node-more-actions="caseMoreActions"
:expand-all="props.isExpandAll"
:empty-text="t('featureTest.featureCase.caseEmptyContent')"
draggable
:virtual-list-props="virtualListProps"
block-node
@select="caseNodeSelect"
@more-action-select="handleCaseMoreSelect"
@more-actions-close="moreActionsClose"
>
<template #title="nodeData">
<span class="text-[var(--color-text-1)]">{{ nodeData.title }}</span>
<span class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count }})</span>
</template>
<template #extra="nodeData">
<MsPopConfirm
:is-delete="false"
:all-names="[]"
:title="t('featureTest.featureCase.addSubModule')"
@cancel="resetFocusNodeKey"
>
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusKey(nodeData)">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</MsPopConfirm>
<MsPopConfirm
:title="t('featureTest.featureCase.rename')"
:all-names="[]"
:is-delete="false"
:field-config="{ field: renameCaseName }"
@cancel="resetFocusNodeKey"
>
<span :id="`renameSpan${nodeData.key}`" class="relative"></span>
</MsPopConfirm>
</template>
</MsTree>
<div class="recycle w-[88%]">
<a-divider class="mb-[16px]" />
<div class="recycle-bin pt-2">
<MsIcon type="icon-icon_delete-trash_outlined" size="16" class="mx-[10px] text-[var(--color-text-4)]" />
<div class="text-[var(--color-text-1)]">{{ t('featureTest.featureCase.recycle') }}</div>
<div class="recycle-count">({{ recycleCount }})</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
const { t } = useI18n();
const { openModal } = useModal();
const focusNodeKey = ref<string | number>('');
const props = defineProps<{
selectedKeys?: Array<string | number>; // key
isExpandAll: boolean; //
}>();
const emits = defineEmits(['update:selectedKeys', 'caseNodeSelect']);
const groupKeyword = ref<string>('');
const caseTree = ref([
{
title: 'Trunk',
key: 'node1',
count: 18,
children: [
{
title: 'Leaf',
key: 'node2',
count: 28,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
],
},
{
title: 'Trunk',
key: 'node3',
count: 180,
children: [
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
],
},
{
title: 'Trunk',
key: 'node6',
children: [],
count: 0,
},
]);
const caseMoreActions: ActionsItem[] = [
{
label: 'featureTest.featureCase.rename',
eventTag: 'rename',
},
{
label: 'featureTest.featureCase.delete',
eventTag: 'delete',
danger: true,
},
];
const renameCaseName = ref('');
const selectedNodeKeys = ref(props.selectedKeys || []);
const renamePopVisible = ref(false);
//
const caseNodeSelect = (selectedKeys: (string | number)[]) => {
emits('caseNodeSelect', selectedKeys);
};
//
const deleteHandler = (node: MsTreeNodeData) => {
openModal({
type: 'error',
title: t('featureTest.featureCase.deleteTipTitle', { name: node.title }),
content: t('featureTest.featureCase.deleteCaseTipContent'),
okText: t('featureTest.featureCase.deleteConfirm'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
try {
Message.success(t('featureTest.featureCase.deleteSuccess'));
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
};
function resetFocusNodeKey() {
focusNodeKey.value = '';
renamePopVisible.value = false;
renameCaseName.value = '';
}
//
const handleCaseMoreSelect = (item: ActionsItem, node: MsTreeNodeData) => {
switch (item.eventTag) {
case 'delete':
deleteHandler(node);
resetFocusNodeKey();
break;
case 'rename':
renameCaseName.value = node.title || '';
renamePopVisible.value = true;
document.querySelector(`#renameSpan${node.key}`)?.dispatchEvent(new Event('click'));
break;
default:
break;
}
};
const moreActionsClose = () => {
if (!renamePopVisible.value) {
resetFocusNodeKey();
}
};
const setFocusKey = (node: MsTreeNodeData) => {
focusNodeKey.value = node.key || '';
};
const recycleCount = ref<number>(100);
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 376px)',
};
});
watch(
() => props.selectedKeys,
(val) => {
selectedNodeKeys.value = val || [];
}
);
watch(
() => selectedNodeKeys.value,
(val) => {
emits('update:selectedKeys', val);
}
);
</script>
<style scoped lang="less">
.recycle {
@apply absolute bottom-0 bg-white pb-4;
:deep(.arco-divider-horizontal) {
margin: 8px 0;
}
.recycle-bin {
@apply bottom-0 flex items-center bg-white;
.recycle-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
}
</style>

View File

@ -1,173 +0,0 @@
<template>
<div class="mb-[16px]">
<a-button type="primary" class="mr-[12px]"> {{ t('featureTest.featureCase.creatingCase') }} </a-button>
<a-button type="outline"> {{ t('featureTest.featureCase.importCase') }} </a-button>
</div>
<div class="pageWrap">
<MsSplitBox>
<template #left>
<div class="p-[24px]">
<div class="feature-case">
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('public')" @click="selectActive('public')">
<MsIcon type="icon-icon_folder_outlined-1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.publicCase') }}</div>
<div class="folder-count">({{ publicCaseCount }})</div></div
>
<div class="back"><icon-arrow-right /></div>
</div>
<a-divider class="my-[8px]" />
<a-input-search class="mb-4" :placeholder="t('featureTest.featureCase.searchTip')" />
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('all')" @click="selectActive('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.allCase') }}</div>
<div class="folder-count">(100)</div></div
>
<div class="ml-auto flex items-center">
<a-tooltip
:content="
isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')
"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<MsPopConfirm
:is-delete="false"
:title="t('featureTest.featureCase.addSubModule')"
:all-names="[]"
:loading="confirmLoading"
@confirm="confirmHandler"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</MsPopConfirm>
</div>
</div>
<a-divider class="my-[8px]" />
<FeatureCaseTree
v-model:selected-keys="selectedKeys"
:is-expand-all="isExpandAll"
@case-node-select="caseNodeSelect"
></FeatureCaseTree>
</div>
</div>
</template>
<template #right>
<div class="p-[24px]">
<CaseTable></CaseTable>
</div>
</template>
</MsSplitBox>
</div>
</template>
<script setup lang="ts">
/**
* @description 功能测试-功能用例
*/
import { computed, ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsPopConfirm from '@/components/pure/ms-popconfirm/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import CaseTable from './components/caseTable.vue';
import FeatureCaseTree from './components/featureCaseTree.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const isExpandAll = ref(false);
const activeCase = ref<string | number>('public'); //
const publicCaseCount = ref<number>(100); //
//
const selectActive = (type: string) => {
activeCase.value = type;
};
//
const getActiveClass = (type: string) => {
return activeCase.value === type ? 'folder-text case-active' : 'folder-text';
};
const expandHandler = () => {
isExpandAll.value = !isExpandAll.value;
};
//
const selectedKeys = computed({
get: () => [activeCase.value],
set: (val) => val,
});
//
function caseNodeSelect(keys: (string | number)[]) {
[activeCase.value] = keys;
}
const confirmLoading = ref(false);
const confirmHandler = () => {
try {
confirmLoading.value = true;
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
};
</script>
<style scoped lang="less">
.pageWrap {
min-width: 1000px;
height: calc(100vh - 136px);
border-radius: var(--border-radius-large);
@apply bg-white;
.case {
@apply flex cursor-pointer items-center justify-between;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
.case-active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
.back {
margin-right: 8px;
width: 20px;
height: 20px;
border: 1px solid #ffffff;
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
.arco-icon {
color: rgb(var(--primary-5));
}
@apply flex cursor-pointer items-center rounded-full;
}
}
}
</style>

View File

@ -1,26 +0,0 @@
export default {
'featureTest.featureCase.creatingCase': 'Create Case',
'featureTest.featureCase.importCase': 'Import Case',
'featureTest.featureCase.publicCase': 'Public of Cases',
'featureTest.featureCase.allCase': 'All of Cases',
'featureTest.featureCase.searchTip': 'Please enter a group name',
'featureTest.featureCase.caseEmptyContent': 'No use case data yet, please click the button above to create or import',
'featureTest.featureCase.addSubModule': 'Add submodules',
'featureTest.featureCase.rename': 'rename',
'featureTest.featureCase.recycle': 'Recycle',
'featureTest.featureCase.versionPlaceholder': 'The default is the latest version',
'featureTest.featureCase.searchByNameAndId': 'Search by ID or name',
'featureTest.featureCase.filter': 'filter',
'featureTest.featureCase.setFilterCondition': 'Set filters',
'featureTest.featureCase.followingCondition': 'Conform to the following',
'featureTest.featureCase.condition': 'Condition',
'featureTest.featureCase.delete': 'delete',
'featureTest.featureCase.addSubModuleSuccess': 'Add submodule successfully',
'featureTest.featureCase.renameSuccess': 'Rename successful',
'featureTest.featureCase.nameNotNullTip': 'The name can not be null',
'featureTest.featureCase.deleteTipTitle': 'Do you want to delete: {name} use case?',
'featureTest.featureCase.deleteCaseTipContent':
'After the node is deleted, all resources under the node will be deleted. Exercise caution when performing this operation.',
'featureTest.featureCase.deleteConfirm': 'Confirm',
'featureTest.featureCase.deleteSuccess': 'Delete Successfully',
};

View File

@ -1,25 +0,0 @@
export default {
'featureTest.featureCase.creatingCase': '创建用例',
'featureTest.featureCase.importCase': '导入用例',
'featureTest.featureCase.publicCase': '公共用例库',
'featureTest.featureCase.allCase': '全部用例',
'featureTest.featureCase.searchTip': '请输入分组名称',
'featureTest.featureCase.caseEmptyContent': '暂无用例数据,请点击上方按钮创建或导入',
'featureTest.featureCase.addSubModule': '添加子模块',
'featureTest.featureCase.rename': '重命名',
'featureTest.featureCase.recycle': '回收站',
'featureTest.featureCase.versionPlaceholder': '默认为最新版本',
'featureTest.featureCase.searchByNameAndId': '通过 ID 或名称搜索',
'featureTest.featureCase.filter': '筛选',
'featureTest.featureCase.setFilterCondition': '设置筛选条件',
'featureTest.featureCase.followingCondition': '符合以下',
'featureTest.featureCase.condition': '条件',
'featureTest.featureCase.delete': '删除',
'featureTest.featureCase.addSubModuleSuccess': '添加子模块成功',
'featureTest.featureCase.renameSuccess': '重命名成功',
'featureTest.featureCase.nameNotNullTip': '名称不能为空',
'featureTest.featureCase.deleteTipTitle': '是否删除:{name} 用例?',
'featureTest.featureCase.deleteCaseTipContent': '删除后,此节点下的所有资源都会被删除,请谨慎操作。',
'featureTest.featureCase.deleteConfirm': '确认删除',
'featureTest.featureCase.deleteSuccess': '删除成功',
};

View File

@ -1,3 +0,0 @@
<template> Feature Test is waiting for development </template>
<script setup></script>

View File

@ -211,9 +211,9 @@
openModal({
type: 'error',
title: t('system.orgTemplate.deleteTemplateTitle', { name: characterLimit(record.name) }),
content: t('system.userGroup.beforeDeleteUserGroup'),
okText: t('system.userGroup.confirmDelete'),
cancelText: t('system.userGroup.cancel'),
content: t('system.orgTemplate.deleteProjectTemplateTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},

View File

@ -136,7 +136,7 @@
import { useAppStore, useTableStore } from '@/store';
import { characterLimit } from '@/utils';
import type { AddorUpdateMemberModel, BatchAddProjectModel, LinkList, MemberItem } from '@/models/setting/member';
import type { AddOrUpdateMemberModel, BatchAddProjectModel, LinkList, MemberItem } from '@/models/setting/member';
import { TableKeyEnum } from '@/enums/tableEnum';
const tableStore = useTableStore();
@ -153,7 +153,7 @@
showTooltip: true,
ellipsis: true,
sortIndex: 0,
showDrag: true,
showDrag: false,
},
{
title: 'organization.member.tableColunmName',
@ -243,7 +243,7 @@
const addMemberVisible = ref<boolean>(false);
const AddMemberRef = ref();
const addOrEditMember = (type: string, record: AddorUpdateMemberModel = {}) => {
const addOrEditMember = (type: string, record: AddOrUpdateMemberModel = {}) => {
addMemberVisible.value = true;
AddMemberRef.value.type = type;
if (type === 'edit') {

View File

@ -72,7 +72,7 @@
>
<div v-if="sceneType === 'BUG'" class="optionsKey">
<a-checkbox v-model="fieldForm.enableOptionKey"
>选项KEY值
>{{ t('system.orgTemplate.optionKeyValue') }}
<a-tooltip :content="t('system.orgTemplate.thirdPartyPlatforms')"
><icon-question-circle
:style="{ 'font-size': '16px' }"

View File

@ -118,7 +118,6 @@
() => props.cardItem,
(val) => {
if (val) {
debugger;
templateCardInfo.value = { ...props.cardItem };
}
},

View File

@ -164,4 +164,10 @@ export default {
'system.orgTemplate.defectContentTip':
'You can set the default value for the defect content and use it when creating',
'system.orgTemplate.templateNameRules': 'Please enter a template name',
'system.orgTemplate.deleteProjectTemplateTip':
'The existing use cases of this template will inherit the default template after deleting it. Are you sure to delete it',
'system.orgTemplate.moduleRuleTip': 'Please select module',
'system.orgTemplate.modules': 'module',
'system.orgTemplate.tags': 'tags',
'system.orgTemplate.optionKeyValue': 'Option KEY value',
};

View File

@ -156,4 +156,9 @@ export default {
'system.orgTemplate.defectNameTip': '可为缺陷名称设置默认值,创建时统一使用',
'system.orgTemplate.defectContentTip': '可为缺陷内容设置默认值,创建时统一使用',
'system.orgTemplate.templateNameRules': '请输入模板名称',
'system.orgTemplate.deleteProjectTemplateTip': '删除后,该模版已有的用例会继承默认模版,确认删除吗?',
'system.orgTemplate.moduleRuleTip': '请选择模块',
'system.orgTemplate.modules': '模块',
'system.orgTemplate.tags': '标签',
'system.orgTemplate.optionKeyValue': '选项KEY值',
};