新增:影响域分析录入和修复editor问题

This commit is contained in:
2026-02-07 17:27:17 +08:00
parent 98835a3225
commit 619d5ea652
17 changed files with 770 additions and 296 deletions

View File

@@ -142,5 +142,16 @@ export default {
method: "post",
data
})
},
/**
* 请求上一轮次级联选择器的cases数据
* @returns
*/
getRelatedCases(id, round_key) {
return request({
url: "/project/case/getRelatedCase",
method: "get",
params: { id, round_key }
})
}
}

View File

@@ -113,7 +113,7 @@ export default {
},
/**
* 新增或者修改软件概述
* @returns 返回新增或修改是否成功
* @returns null
*/
postSoftSummary(data) {
return request({
@@ -122,6 +122,17 @@ export default {
data: data
})
},
/**
* 新增或者修改动态环境描述
* @returns null
*/
postDynamicDescription(data) {
return request({
url: "/testmanage/project/dynamic_description/",
method: "post",
data: data
})
},
/**
* 获取项目的软件概述
* @returns 返回软件概述数据
@@ -133,6 +144,17 @@ export default {
params: { id: id }
})
},
/**
* 获取动态环境描述
* @returns 返回动态环境描述结构化数据
*/
getDynamicDescription(id) {
return request({
url: "/testmanage/project/dynamic_des/",
method: "get",
params: { id: id }
})
},
/**
* 提交修改或新增软件接口图
* @returns 返回新增或修改是否成功
@@ -167,6 +189,28 @@ export default {
params: { id: id, category }
})
},
/**
* 获取环境差异性分析数据
* @returns 返回数据
*/
getEnvAnalysis(id) {
return request({
url: "/testmanage/project/get_env_analysis/",
method: "get",
params: { id: id }
})
},
/**
* 提交环境差异性数据
* @returns null
*/
postEnvAnalysis(data) {
return request({
url: "/testmanage/project/post_env_analysis/",
method: "post",
data
})
},
/**
* 提交修改或新增静态软件项、静态硬件项、动态软件项、动态硬件项
* @returns null

View File

@@ -35,7 +35,7 @@ export default {
})
},
/**
* 更新轮次
* 删除轮次
* @returns
*/
delete(project_id, data = {}) {
@@ -45,5 +45,38 @@ export default {
data,
params: { project_id }
})
},
/**
* 获取影响域分析
* @returns 获取数据或code=25002
*/
getInfluence(id, round_key) {
return request({
url: "project/round/get_influence",
method: "get",
params: { id, round_key }
})
},
/**
* 新增或修改影响域分析
* @returns null
*/
postInfluence(data) {
return request({
url: "project/round/create_influence",
method: "post",
data
})
},
/**
* 查看轮次的影响域分析是否有数据
* @returns data: boolean
*/
getInfluenceStatus(id, round_key) {
return request({
url: "project/round/get_status_influence",
method: "get",
params: { id, round_key }
})
}
}

View File

@@ -0,0 +1,174 @@
<template>
<div class="effect-modal-container">
<a-modal
v-model:visible="visible"
width="90%"
:title="modalTitle"
unmount-on-close
ok-text="提交保存"
cancel-text="取消保存"
:on-before-ok="handleOk"
draggable
@close="handleCloseEnd"
>
<div class="mb-1 flex items-center justify-center">
<a-button type="primary" @click="() => addRow(-1)">+ 新增一行</a-button>
<a-alert type="warning" style="height: 32px">表格为空提交则清除数据储存</a-alert>
</div>
<a-table
:columns="columns"
row-key="id"
:bordered="{
wrapper: true,
cell: true,
headerCell: true,
bodyCell: true
}"
@change="handleChange"
:draggable="{ type: 'handle', width: 40 }"
:data="datas"
>
<template #index="{ rowIndex }">
{{ rowIndex + 1 }}
</template>
<template #changeType="{ rowIndex }">
<a-input v-model="datas[rowIndex].change_type"></a-input>
</template>
<template #changeDes="{ rowIndex, record }">
<ma-editor v-model="datas[rowIndex].change_des" :key="`editor_${record.id}`"></ma-editor>
</template>
<template #changeInflu="{ rowIndex }">
<a-textarea v-model="datas[rowIndex].change_influ" auto-size></a-textarea>
</template>
<template #effectCases="{ rowIndex }">
<a-cascader
v-model="datas[rowIndex].effect_cases"
placeholder="请选择关联用例"
size="mini"
allow-search
allow-clear
:loading="cascaderLoading"
:options="casOptions"
:style="{ width: '180px' }"
multiple
:tag-nowrap="true"
:format-label="format"
:field-names="{
value: 'key',
label: 'label',
children: 'children'
}"
></a-cascader>
</template>
<template #operator="{ rowIndex }">
<a-button type="text" status="danger" @click="() => deleteRow(rowIndex)">删除</a-button>
</template>
</a-table>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, inject, getCurrentInstance, nextTick } from "vue"
import useColumns from "../hooks/useColumns"
import { Message, type CascaderOption } from "@arco-design/web-vue"
import MaEditor from "@/components/ma-editor/index.vue"
import caseApi from "@/api/project/case"
import { useRoute } from "vue-router"
import { NodeDataInterface } from "../types"
import { DataInterface } from "./types"
import roundApi from "@/api/project/round"
import useDataOperation from "./useDataOperator"
// props
const { reset } = defineProps<{ reset: () => void }>()
// global
const { proxy } = getCurrentInstance() as any
const route = useRoute()
const nodeData: NodeDataInterface | undefined = inject("nodeData")
// hooks
const { columns } = useColumns()
// vars
const visible = ref(false)
const modalTitle = ref("影响域分析")
// datas
const datas = ref<DataInterface[]>([])
const { handleChange, addRow, deleteRow } = useDataOperation(datas)
// events
const handleOk = async () => {
// 判断是否change_type是否填写
if (!datas.value.every((item) => item.change_type.trim().length > 0)) {
Message.error("请至少填写变更类型")
return false
}
try {
await roundApi.postInfluence({
id: route.query.id,
round_key: nodeData?.key,
item_list: datas.value
})
Message.success("新增或修改成功")
} catch (e) {
return false
}
}
const handleCloseEnd = async () => {
datas.value.forEach((item) => {
// 安全地检查并重置 change_des
if (item && typeof item === "object" && "change_des" in item) {
item.change_des = ""
}
})
await nextTick()
// 关闭清除数据
datas.value = []
casOptions.value = []
reset()
}
// component functions
const cascaderLoading = ref(false)
const casOptions = ref([])
const format = (options: CascaderOption[]) => {
return options.at(-1).label || "未获取用例名称"
}
const open = async () => {
proxy?.$loading?.show("数据加载中...")
// 打开时请求级联选择器数据
try {
cascaderLoading.value = true
const res = await caseApi.getRelatedCases(route.query.id, nodeData?.key)
casOptions.value = res.data
// 获取影响域分析数据
const res2 = await roundApi.getInfluence(route.query.id, nodeData?.key)
if (res2.code !== 25002) {
// 有影响域分析
datas.value = res2.data.map((item: any) => ({
...item,
id: item.id || `loaded_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}))
} else {
Message.info("暂未填写影响域分析,请填写")
}
} catch (e) {
} finally {
cascaderLoading.value = false
proxy?.$loading?.hide()
}
visible.value = true
}
defineExpose({ open })
defineOptions({
name: "EffectModal"
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,6 @@
export interface DataInterface {
change_type: string
change_des?: string
effect_cases?: string[]
change_influ?: string
}

View File

@@ -0,0 +1,27 @@
import type { Ref } from "vue"
import { cloneDeep } from "lodash-es"
export default function useDataOperation(datas: Ref<any[]>) {
const newRow = {
id: Date.now().toString(),
change_type: "",
change_des: "",
effect_cases: []
}
const handleChange = (_data: any) => {
datas.value = _data
}
const addRow = (rowIndex: number) => {
const insertIndex = rowIndex === -1 ? datas.value!.length : rowIndex + 1
const rowToAdd = {
...cloneDeep(newRow),
id: `row_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
}
datas.value!.splice(insertIndex, 0, rowToAdd)
}
const deleteRow = (rowIndex: number) => {
if (rowIndex < 0 || rowIndex >= datas.value!.length) return
datas.value!.splice(rowIndex, 1)
}
return { handleChange, addRow, deleteRow }
}

View File

@@ -0,0 +1,47 @@
import type { TableColumnData } from "@arco-design/web-vue/es/table/interface"
export default function useColumns() {
const columns: TableColumnData[] = [
{
title: "序号",
dataIndex: "index",
width: 80,
align: "center",
slotName: "index"
},
{
title: "更改类型",
dataIndex: "change_type",
width: 180,
align: "center",
slotName: "changeType"
},
{
title: "更改内容描述",
dataIndex: "change_des",
align: "left",
slotName: "changeDes"
},
{
title: "影响域分析",
dataIndex: "change_influ",
align: "left",
slotName: "changeInflu"
},
{
title: "影响用例",
dataIndex: "effect_cases",
align: "center",
slotName: "effectCases",
width: 180
},
{
title: "操作",
dataIndex: "operator",
align: "center",
slotName: "operator",
width: 80
}
]
return { columns }
}

View File

@@ -0,0 +1,51 @@
<template>
<div class="influence-container">
<a-tooltip content="影响域分析">
<icon-fire
style="position: absolute; right: 95px; font-size: 12px; top: 8px"
:style="{ color: isInfluence ? '#00b42a' : '#ff7d00' }"
@click="handleInfluence"
/>
</a-tooltip>
<EffectModal ref="effectRef" :reset="fetchInfluenceExist" />
</div>
</template>
<script setup lang="ts">
import { useTemplateRef, provide, onMounted, ref } from "vue"
import type { NodeDataInterface } from "./types"
import EffectModal from "./EffectModal/index.vue"
import roundApi from "@/api/project/round"
import { useRoute } from "vue-router"
// globals
const route = useRoute()
// 树状传递的轮次节点数据
const { nodeData } = defineProps<{ nodeData: NodeDataInterface }>()
provide("nodeData", nodeData)
const effectRef = useTemplateRef("effectRef")
const handleInfluence = () => {
effectRef.value?.open()
}
// 在挂载时查询影响域分析
const isInfluence = ref(false)
const fetchInfluenceExist = async () => {
try {
const res = await roundApi.getInfluenceStatus(route.query.id, nodeData.key)
isInfluence.value = res.data
} catch {}
}
onMounted(async () => {
await fetchInfluenceExist()
})
defineOptions({
name: "Influence"
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,6 @@
export interface NodeDataInterface {
title: string
key: string
level: string
children?: NodeDataInterface[]
}

View File

@@ -0,0 +1,107 @@
<template>
<div class="text-table-container">
<a-modal
v-model:visible="visible"
width="50%"
draggable
:on-before-ok="handleSyncOk"
unmount-on-close
ok-text="确认保存"
cancel-text="关闭不保存"
:maskClosable="false"
@close="handleOnClose"
>
<template #title>{{ theTitle }}</template>
<a-space direction="vertical" fill>
<a-card title="差异性段落描述" hoverable>
<a-textarea auto-size placeholder="请填写差异性分析和'见下表所示'" v-model="description"></a-textarea>
</a-card>
<a-card title="表格" hoverable>
<WordLikeTable v-model="tableDatas" v-model:fontnote="fontnote" />
</a-card>
</a-space>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { Message } from "@arco-design/web-vue"
import { getCurrentInstance, ref } from "vue"
import { useRoute } from "vue-router"
import WordLikeTable from "./projectModal/wordLikeTable/index.vue"
import { cloneDeep } from "lodash-es"
import projectApi from "@/api/project/project"
const { proxy } = getCurrentInstance() as any
const route = useRoute()
// datas
const description = ref("")
const initialTableData = [
["", "", ""],
["", "", ""],
["", "", ""]
]
const fontnote = ref("")
const tableDatas = ref(initialTableData)
// props
const { reset } = defineProps<{
reset: () => void
}>()
const visible = ref(false)
const theTitle = ref("")
const handleSyncOk = async () => {
// 验证输入文字是否为空
if (description.value.trim().length <= 0) {
Message.error("请输入分析内容再提交")
return false
}
try {
// 请求接口
await projectApi.postEnvAnalysis({
id: route.query.id,
table: tableDatas.value,
fontnote: fontnote.value,
description: description.value
})
Message.success("保存成功")
} catch (e) {
return false
}
}
const handleOnClose = () => {
// 用来清空数据
fontnote.value = ""
description.value = ""
tableDatas.value = cloneDeep(initialTableData)
reset()
}
const open = async (category_str: string) => {
proxy?.$loading?.show("数据加载中...")
theTitle.value = category_str
try {
// 获取数据并赋值给tableData
const res = await projectApi.getEnvAnalysis(route.query.id)
if (res.code === 25001) {
tableDatas.value = res.data.table
fontnote.value = res.data.fontnote
description.value = res.data.description
}
visible.value = true
} catch (e) {
} finally {
proxy?.$loading?.hide()
}
}
defineExpose({
open
})
</script>
<style scoped></style>

View File

@@ -1,20 +1,16 @@
<template>
<div class="project-info-other-container">
<a-dropdown>
<a-button class="nav-btn">
<template #icon>
<icon-settings />
</template>
<a-space>
<span>项目设置</span>
<a-dropdown :popup-max-height="false">
<a-space>
<SettingButton label="项目设置">
<a-tooltip :content="allStatus ? '您已全部填写' : '还有未填写项目'">
<span class="text-green-500" v-if="allStatus">
<icon-check-circle-fill />
<icon-check-circle-fill size="20px" />
</span>
<span class="text-red-500" v-else><icon-exclamation-circle-fill /></span>
<span class="text-red-500" v-else><icon-exclamation-circle-fill size="20px" /></span>
</a-tooltip>
</a-space>
</a-button>
</SettingButton>
</a-space>
<template #content>
<template v-for="item in inputOptions" :key="item.name">
<template v-if="!item.status">
@@ -40,6 +36,8 @@
<InterfaceImage ref="interfaceImageRef" :reset="fetchAllStatus" />
<!-- 静态软件项静态硬件项动态软件项动态硬件项 -->
<StaticDynamicTable ref="staticDynamiRef" :reset="fetchAllStatus" />
<!-- 环境差异性分析 -->
<TextAndTable ref="textAndTableRef" :reset="fetchAllStatus" />
</div>
</template>
@@ -51,6 +49,8 @@ import { Message } from "@arco-design/web-vue"
import ProjectModal from "./projectModal/index.vue"
import InterfaceImage from "./InterfaceImage.vue"
import StaticDynamicTable from "./StaticDynamicTable.vue"
import TextAndTable from "./TextAndTable.vue"
import SettingButton from "./settingButton/index.vue"
const route = useRoute()
@@ -58,6 +58,7 @@ const route = useRoute()
const projectModalRef = ref<InstanceType<typeof ProjectModal> | null>(null)
const interfaceImageRef = ref<InstanceType<typeof InterfaceImage> | null>(null)
const staticDynamiRef = useTemplateRef("staticDynamiRef")
const textAndTableRef = useTemplateRef("textAndTableRef")
// events
const clickStuctDatas = async (category: string) => {
@@ -69,6 +70,9 @@ const clickInterfaceImage = async () => {
const clickStaticDynamic = async (title: string) => {
staticDynamiRef.value?.open(title)
}
const clickTextAndTable = async (title: string) => {
textAndTableRef.value?.open(title)
}
// 进入页面时候请求知道各项目样式情况-ref
const fetchAllStatus = async () => {
@@ -131,6 +135,18 @@ const inputOptions = ref([
title: "动态硬件项表",
status: false,
handler: () => clickStaticDynamic("动态硬件项")
},
{
name: "evaluate_data",
title: "测评数据",
status: false,
handler: () => clickStaticDynamic("测评数据")
},
{
name: "env_analysis",
title: "环境差异性分析",
status: false,
handler: () => clickTextAndTable("环境差异性分析")
}
])
const allStatus = computed(() => inputOptions.value.every((item) => item.status))

View File

@@ -90,6 +90,6 @@ const handlePaste = async (e: ClipboardEvent) => {
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: cover;
object-fit: contain;
}
</style>

View File

@@ -47,6 +47,7 @@ const { reset } = defineProps<{
const visible = ref(false)
const title = ref("")
const category = ref("软件概述")
const { columns, data, handleChange, addTextRow, addPicRow, addTableRow, handleOnClose } = useTable(reset)
@@ -62,25 +63,29 @@ const dictMap = {
动态环境描述: {
createTitle: "动态环境描述-新增",
modifyTitle: "动态环境描述-修改",
errorMsg: "获取动态环境描述失败"
errorMsg: "获取动态环境描述失败",
getFunc: projectApi.getDynamicDescription,
postFunc: projectApi.postDynamicDescription
}
}
// functions and events
const handleSyncOk = async () => {
const handleSyncOk = async () => {
try {
await projectApi.postSoftSummary({ id: route.query.id, data: data.value })
await dictMap[category.value].postFunc({ id: route.query.id, data: data.value })
visible.value = false
Message.success("保存成功")
} catch (e) {
console.log(e);
Message.error("提交时发送错误,请联系管理员")
}
return false
}
const open = async (category: string) => {
const open = async (category_str: string) => {
proxy?.$loading?.show("数据加载中...")
const currentCate = dictMap[category]
category.value = category_str
const currentCate = dictMap[category.value]
try {
const res = await currentCate.getFunc(route.query.id)
const code = res.code // 25001表示有数据25002表示没有数据

View File

@@ -0,0 +1,68 @@
<template>
<button class="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="0 0 20 20" height="20" fill="none" class="svg-icon">
<g stroke-width="1.5" stroke-linecap="round" stroke="#5d41de">
<circle r="2.5" cy="10" cx="10"></circle>
<path
fill-rule="evenodd"
d="m8.39079 2.80235c.53842-1.51424 2.67991-1.51424 3.21831-.00001.3392.95358 1.4284 1.40477 2.3425.97027 1.4514-.68995 2.9657.82427 2.2758 2.27575-.4345.91407.0166 2.00334.9702 2.34248 1.5143.53842 1.5143 2.67996 0 3.21836-.9536.3391-1.4047 1.4284-.9702 2.3425.6899 1.4514-.8244 2.9656-2.2758 2.2757-.9141-.4345-2.0033.0167-2.3425.9703-.5384 1.5142-2.67989 1.5142-3.21831 0-.33914-.9536-1.4284-1.4048-2.34247-.9703-1.45148.6899-2.96571-.8243-2.27575-2.2757.43449-.9141-.01669-2.0034-.97028-2.3425-1.51422-.5384-1.51422-2.67994.00001-3.21836.95358-.33914 1.40476-1.42841.97027-2.34248-.68996-1.45148.82427-2.9657 2.27575-2.27575.91407.4345 2.00333-.01669 2.34247-.97026z"
clip-rule="evenodd"
></path>
</g>
</svg>
<span class="lable">{{ label }}</span>
<slot></slot>
</button>
</template>
<script setup lang="ts">
const { label } = defineProps<{
label: string
}>()
defineOptions({
name: "SettingButton"
})
</script>
<style scoped>
.button {
display: flex;
justify-content: center;
align-items: center;
padding: 6px 12px;
gap: 8px;
height: 36px;
width: 200px;
border: none;
background: #5e41de33;
border-radius: 20px;
cursor: pointer;
}
.lable {
line-height: 20px;
font-size: 17px;
color: #5d41de;
font-family: sans-serif;
letter-spacing: 1px;
}
.button:hover {
background: #5e41de4d;
}
.button:hover .svg-icon {
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -85,6 +85,9 @@
}
"
/></a-tooltip>
<template v-if="nodeData.key !== '0'">
<Influence :node-data="nodeData"></Influence>
</template>
</template>
</template>
<!-- 设计节点的图标 -->
@@ -248,6 +251,8 @@ import { provide, ref, watch } from "vue"
import NavBar from "@/layout/components/navbar.vue"
import PageLayout from "@/layout/page-layout.vue"
import MaFormModal from "@/components/ma-form-modal/index.vue"
// 影响域分析
import Influence from "@/layout/components/Influence/index.vue"
// 轮次的右键菜单,单独一个组件 -> 在treeComponents里面
import roundRight from "./treeComponents/roundRight.vue"
// 问题单ma-crud