需求解析功能进一步完善

This commit is contained in:
2025-04-30 17:44:22 +08:00
parent 7fe6ecf765
commit 0a0beb3e91
32 changed files with 1073 additions and 618 deletions

View File

@@ -1,6 +1,8 @@
<template>
<div id="background" class="fixed"></div>
<div class="bg-backdrop-layout"></div>
<div class="bg-backdrop-layout">
<ParticlesBg class="h-full" :quantity="200" :ease="100" :color="'#000'" :staticity="10" refresh></ParticlesBg>
</div>
<div class="login-container">
<div class="login-width md:w-10/12 mx-auto flex justify-between h-full items-center">
<div class="w-6/12 mx-auto left-panel rounded-l pl-5 pr-5 hidden md:block">
@@ -107,6 +109,8 @@ import verifyCode from "@cps/ma-verifyCode/index.vue"
import { useUserStore } from "@/store"
import { useRouter, useRoute } from "vue-router"
import userApi from "@/api/system/user"
// 导入背景ui组件
import ParticlesBg from "@/components/ui/particles-bg/ParticlesBg.vue"
const router = useRouter()
const userStore = useUserStore()
// 绑定登录form的数据
@@ -157,7 +161,6 @@ const handleSubmit = async ({ values, errors }) => {
width: 100%;
height: 100%;
background-size: cover;
background-image: url("@/assets/BingWallpaper.jpg");
}
.bg-backdrop-layout {
top: 0;
@@ -176,13 +179,14 @@ const handleSubmit = async ({ values, errors }) => {
z-index: 3;
.login-width {
max-width: 950px;
background: #fff;
padding: 10px;
height: 500px;
position: relative;
top: 50%;
margin-top: -255px;
border-radius: var(--border-radius-small);
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 28px, rgba(0, 0, 0, 0.22) 0px 10px 10px;
backdrop-filter: blur(3px);
}
.left-panel {

View File

@@ -10,7 +10,44 @@ export default function useOptions(formRef: any) {
})
const crudColumns = useColumn(formRef)
const columnOptions = computed(() => {
return tool.renameKeyInArray(crudColumns.value, "commonRules", "rules")
const transformColumns = tool.renameKeyInArray(crudColumns.value, "commonRules", "rules")
// 针对测试项的布局优化
// 取出所有字段
const identColumn = transformColumns.find((item: any) => item.dataIndex === "ident")
const nameColumn = transformColumns.find((item: any) => item.dataIndex === "name")
const priorityColumn = transformColumns.find((item: any) => item.dataIndex === "priority")
const testTypeColumn = transformColumns.find((item: any) => item.dataIndex === "testType")
const testMethodColumn = transformColumns.find((item: any) => item.dataIndex === "testMethod")
// 组装表单布局
const identAndNameColumn = {
formType: "grid",
cols: [
{ span: 12, formList: [identColumn] },
{ span: 12, formList: [nameColumn] }
]
}
const priorityColumnNew = {
formType: "grid",
cols: [{ span: 24, formList: [priorityColumn] }]
}
const testTypeAndMethodColumn = {
formType: "grid",
cols: [
{ span: 12, formList: [testTypeColumn] },
{ span: 12, formList: [testMethodColumn] }
]
}
// 取除原数组里面的内容
const newColumnsArray = transformColumns.filter(
(it: any) =>
it.dataIndex !== "ident" &&
it.dataIndex !== "name" &&
it.dataIndex !== "priority" &&
it.dataIndex !== "testType" &&
it.dataIndex !== "testMethod"
)
newColumnsArray.unshift(identAndNameColumn, priorityColumnNew, testTypeAndMethodColumn)
return newColumnsArray
})
return { options, columnOptions }
}

View File

@@ -25,7 +25,7 @@ export default function (crudOrFormRef: any) {
{
title: "名称",
dataIndex: "name",
width: 120,
width: 150,
align: "center",
search: true,
commonRules: [{ required: true, message: "名称是必填" }],

View File

@@ -3,7 +3,7 @@
<a-modal
v-model:visible="modalVisible"
width="80%"
title="设计需求批量写入"
title="批量导入设计需求"
:unmount-on-close="true"
:mask-closable="false"
ok-text="录入"
@@ -11,20 +11,30 @@
:on-before-ok="handleModalSubmit"
>
<div class="uploadContainer">
<span :style="{ marginBottom: '10px', flex: '0 1 160px' }">上传设计需求.docx:</span>
<span :style="{ marginBottom: '10px', flex: '0 1 150px' }">上传需求.docx:</span>
<a-upload
:style="{ marginBottom: '10px' }"
:custom-request="handleRquest"
:limit="1"
accept=".docx"
:action="`/api/dut_upload/upload_xq_docx/?parseChapter=${parseChapter}`"
@change="handleUploadChange"
@success="handleUploadSuccess"
@error="handleUploadError"
:disabled="parseChapter.trim() === ''"
></a-upload>
</div>
<div class="flex items-center gap-3">
<span class="w-[350px]">要解析的章节名称</span>
<a-input placeholder="输入要解析的章节名称" v-model="parseChapter"></a-input>
<span class="w-[350px]">选择需求录入类型</span>
<a-select allow-search v-model="selectValue">
<a-option v-for="item in demandType" :key="item.key" :value="item.key">{{ item.title }}</a-option>
</a-select>
</div>
<a-alert :style="{ margin: '10px 0' }" type="warning">
请去除需求规格说明文件中不必要的部分再来此处解析注意本系统不支持word中emf格式图片如果使用了visio等图片请<span
class="important-text"
>在word变为普通图片上传</span
>
只能上传.docx<span class="important-text">如果visio图请替换为普通图片上传</span
>请在需求规格说明文档中操作 ->
<span class="important-text">引用 -> 目录 -> 自定义目录 -> 显示级别改为6</span>以上保存后上传
</a-alert>
<div class="operation-container">
<span :style="{ marginRight: '10px', fontWeight: 700 }">操作按钮:</span>
@@ -109,104 +119,39 @@
</template>
<script setup>
import { ref, nextTick, watch } from "vue"
import mammoth from "mammoth"
import { ref, watch } from "vue"
import dictApi from "@/api/system/dict"
import demandApi from "@/api/project/designDemand"
import { useRoute, useRouter } from "vue-router"
import { parseHtmlStringByDemandDut } from "@/views/project/dut/tools/parseHtmlString"
import { HtmlParser } from "@/views/project/dut/tools/parser"
import { useRoute } from "vue-router"
import { useTreeDataStore } from "@/store"
// 其他初始化数据
const route = useRoute()
const router = useRouter()
const treeDataStore = useTreeDataStore()
// ~导入editor组件~tinymce
import MaEditor from "@/components/ma-editor/index.vue"
import { Message } from "@arco-design/web-vue"
// 单个设计需求录入模版
const templateDemandObj = {
chapter: "",
title: "",
ident: "",
demandType: "",
content: ""
}
// 先请求设计需求的dict然后给demandType选项
import useUpload from "./useUpload"
import useListOperaton from "./useListOperation"
// 定义录入完毕的事件,给父组件刷新表格
const emit = defineEmits(["enterFinish"])
// 其他初始化数据
const route = useRoute()
const treeDataStore = useTreeDataStore()
// ~~~刚开始就加载字典数据,给选择框使用~~~
let demandType = []
const getDictDemandType = async function () {
const res = await dictApi.getDictByCode({ code: "demandType" })
demandType = res.data
}
getDictDemandType()
// 弹窗显示
// 弹窗显示ref
const modalVisible = ref(false)
// 全部html数据-循环创建折叠项
// 全部html数据-给a-list展示
const htmlData = ref([])
// 数据变化spin显示
const loading = ref(false)
// 当upload组件发生变化时候
const handleUploadChange = (files) => {
files.length ? (files.length = 1) : (htmlData.value = [])
}
// 上传行为函数
const handleRquest = function (options) {
const { onProgress, onError, onSuccess, fileItem } = options
onProgress(0.1)
// 让spin组件先转圈
loading.value = true
// 让Empty组件不显示
htmlData.value.push(templateDemandObj)
// 获取文件
let reader = new FileReader()
reader.readAsArrayBuffer(fileItem.file)
reader.onload = function (loadEvent) {
let arrayBuffer = loadEvent.target.result
mammoth
.convertToHtml({ arrayBuffer: arrayBuffer })
.then((res) => {
// 已经上传到浏览器了,需要解析为列表
onSuccess(1)
const rawHtml = res.value
const parser = new HtmlParser(rawHtml)
const finalData = parser.parseToArray()
htmlData.value = finalData
// ~~~~使用nextTick等待DOM更新完成~~~~
nextTick(() => {
loading.value = false
})
})
.catch(function (error) {
console.log("处理错误导致失败,请检查前端代码")
console.log("错误如下:", error)
onError(error)
})
}
}
// 上方按钮:直接在最下新增一条
const handleCreateAtLatest = () => {
const newDemand = JSON.parse(JSON.stringify(templateDemandObj))
htmlData.value.push(newDemand)
}
// 上方按钮:重置数据,点击页面不卡段
const handleResetData = () => {
htmlData.value = []
}
// 点击单条右侧按钮:下方新增 - 深拷贝,并插入到下方
const handledownCreate = (index) => {
const newDemand = JSON.parse(JSON.stringify(templateDemandObj))
htmlData.value.splice(index + 1, 0, newDemand)
}
// 因为a-list限制必须知道当前页码和页容量
const currentPage = ref(1)
const handlePageChange = (page) => {
currentPage.value = page
}
// 点击单条右侧按钮:删除 - 需要根据currentPage动态觉得因为a-list每页都是这样计算的
const handleDelete = (index) => {
const currentIndex = index + (currentPage.value - 1) * 15
htmlData.value.splice(currentIndex, 1)
}
// ~~~~1.list~~~~
const { loading, handleCreateAtLatest, handleResetData, handledownCreate, handlePageChange, handleDelete } =
useListOperaton(htmlData)
// ~~~~2.upload~~~~
const { handleUploadSuccess, handleUploadError, parseChapter, selectValue } = useUpload(htmlData)
// 打开弹窗并初始化form数据
const open = function () {
// 打开时候传入对象可初始化from
@@ -234,11 +179,19 @@ const handleModalSubmit = async () => {
})
if (res.code === 200) {
treeDataStore.updateDesignDemandTreeData(res.data, route.query.id)
// 给父元素说明我已经完成了
emit("enterFinish")
Message.success("批量新增设计需求成功...")
return true
}
return false
}
// 当upload组件发生变化时候-性能配置
const handleUploadChange = (files) => {
files.length ? (files.length = 1) : (htmlData.value = [])
}
// 暴露该组件ref的方法
defineExpose({ open })
</script>

View File

@@ -0,0 +1,39 @@
import { ref } from "vue"
const templateDemandObj = {
chapter: "",
title: "",
ident: "",
demandType: "",
content: ""
}
export default function useListOperaton(htmlData: any) {
// 数据变化spin显示
const loading = ref(false)
// 上方按钮:直接在最下新增一条
const handleCreateAtLatest = () => {
const newDemand = JSON.parse(JSON.stringify(templateDemandObj))
htmlData.value.push(newDemand)
}
// 上方按钮:重置数据,点击页面不卡段
const handleResetData = () => {
htmlData.value = []
}
// 点击单条右侧按钮:下方新增 - 深拷贝,并插入到下方
const handledownCreate = (index: number) => {
const newDemand = JSON.parse(JSON.stringify(templateDemandObj))
htmlData.value.splice(index + 1, 0, newDemand)
}
// 因为a-list限制必须知道当前页码和页容量
const currentPage = ref(1)
const handlePageChange = (page) => {
currentPage.value = page
}
// 点击单条右侧按钮:删除 - 需要根据currentPage动态觉得因为a-list每页都是这样计算的
const handleDelete = (index: number) => {
const currentIndex = index + (currentPage.value - 1) * 15
htmlData.value.splice(currentIndex, 1)
}
return { loading, handleCreateAtLatest, handleResetData, handledownCreate, handlePageChange, handleDelete }
}

View File

@@ -0,0 +1,130 @@
import { ref } from "vue"
import { Message, Notification } from "@arco-design/web-vue"
const templateDemandObj = {
chapter: "",
title: "",
ident: "",
demandType: "",
content: ""
}
// 判断是否为{"__type__": "image","format": "base64","data": "base64数据"}
function isImageObject(obj: any) {
return (
obj !== null &&
typeof obj === "object" &&
!Array.isArray(obj) &&
Object.hasOwn(obj, "__type__") &&
Object.hasOwn(obj, "format") &&
Object.hasOwn(obj, "data")
)
}
export default function useUpload(htmlData: any) {
const parseChapter = ref("") // 定义用户想解析的章节名称
const selectValue = ref("1") // 定义选择框
const handleUploadSuccess = (fileItem: any) => {
const data = fileItem.response.data
if (!data.children) {
Notification.error({
content: "解析失败请确认上传文件目录级别包含6以上章节名称正确",
duration: 3000,
closable: true
})
parseChapter.value = ""
return
}
// 这里就是解析出东西了
const parsedData = getObjFromChapter(data)
if (parsedData) {
enterDemand(parsedData)
}
// 上传成功后清空解析章节名称
parseChapter.value = ""
}
const handleUploadError = (fileItem: any) => {
console.log(fileItem.response)
}
const getObjFromChapter = (parseObj: any) => {
if (parseObj.title === parseChapter.value) return parseObj
if (Array.isArray(parseObj.children) && parseObj.children.length > 0) {
for (const child of parseObj.children) {
const found = getObjFromChapter(child)
if (found) return found // 递归返回obj或null
}
}
return null // 如果都没找到返回null
}
// 辅助函数递归将parseObj转为templateDemandObj录入递归录入
const enterDemand = (parseObj: any) => {
// 下面是录入
const demandObj = JSON.parse(JSON.stringify(templateDemandObj))
demandObj.chapter = parseObj.number
demandObj.title = parseObj.title
demandObj.ident = parseObj.ordinal ? parseObj.ordinal : "" // 设计需求标识如果没有则为空
demandObj.demandType = selectValue.value
// 解析数组,然后添加到内容中
const content = formatContentForTinyMCE(JSON.parse(parseObj.content))
demandObj.content = content
htmlData.value.push(demandObj)
// 如果有子对象,则再运行一次即可
if (Array.isArray(parseObj.children) && parseObj.children.length > 0) {
for (const childObj of parseObj.children) {
enterDemand(childObj)
}
}
}
// 上面函数的辅助函数将数组变为HTML给tinymce使用
function formatContentForTinyMCE(data: any) {
let htmlContent = ""
// 处理普通文本行
for (const item of data) {
// 如果是普通文本
if (typeof item === "string") {
// 处理表头标记
if (item.includes("见表") || item.includes("如表") || item.includes("如下表")) {
htmlContent += `<p>${item.replace(/见表\d+/, "见下表").replace(/见图\d+/, "见下图")}</p>`
} else if (item.includes("见图") || item.includes("如图") || item.includes("如下图")) {
htmlContent += `<p>${item.replace(/见图\d+/, "见下图").replace(/见表\d+/, "见下表")}</p>`
} else if (/表\d+/.test(item)) {
htmlContent += ``
} else if (/图\d+/.test(item)) {
htmlContent += `${item.replace(/图\d+/, "下图")}`
} else {
htmlContent += `<p>${item}</p>`
}
}
// 如果是对象(根据后端逻辑是图片)
else if (isImageObject(item)) {
// item现在是对象其data是base64字符串
htmlContent += `<p><img src="data:image/png;base64,${item.data}" style="max-width:100%; height:auto; margin: 0 auto; display: block;"></img></p>`
}
// 处理表格数据
else if (Array.isArray(item)) {
htmlContent += '<table border="1" style="margin: 0 auto;max-width:70%; border-collapse:collapse;">'
// 处理表头
const headers = item[0].split("\t")
htmlContent += "<thead><tr>"
for (const header of headers) {
htmlContent += `<th style="padding:8px; border:1px solid #ddd;">${header}</th>`
}
htmlContent += "</tr></thead>"
// 处理表格内容
htmlContent += "<tbody>"
for (let i = 1; i < item.length; i++) {
const cells = item[i].split("\t")
htmlContent += "<tr>"
for (const cell of cells) {
// 处理单元格内的换行符
const formattedCell = cell.replace(/\n/g, "<br>")
htmlContent += `<td style="padding:8px; border:1px solid #ddd;">${formattedCell}</td>`
}
htmlContent += "</tr>"
}
htmlContent += "</tbody></table>"
}
}
return htmlContent
}
return { handleUploadSuccess, handleUploadError, parseChapter, selectValue }
}

View File

@@ -19,7 +19,7 @@
<template #inputPrepend-ident> SJ-XX- </template>
</ma-crud>
</div>
<file-input-modal ref="fileInputRef"></file-input-modal>
<file-input-modal ref="fileInputRef" @enterFinish="crudRef.refresh()"></file-input-modal>
</div>
</template>

View File

@@ -1,148 +0,0 @@
/**
* 辅助函数给每个h2增加个自定义属性用来储存章节号返回功能/接口h2节点对象【非DOM有dom属性表示DOM】
*/
function getNeedH2NodeList(h1h2Node) {
const h2ObjList = []
// 首先将h1和h2节点都组成一个数组
let h1Index = 0,
h2Index = 0
h1h2Node.forEach((hDom) => {
if (hDom.tagName === "H1") {
// 找到h1那么其index+1
h1Index += 1
h2Index = 0
} else if (hDom.tagName === "H2") {
// 按顺序找到h2了
h2Index += 1
let h2Obj = {
chapter: h1Index + "." + h2Index,
dom: hDom,
text: hDom.innerText
}
h2ObjList.push(h2Obj)
}
})
// 1.~~~~TODO:可以从这里修改识别范围~~~~
return h2ObjList.filter(
(item) =>
item.text.includes("CSCI功能需求") ||
item.text.includes("CSCI外部接口需求") ||
item.text.includes("CSCI能力需求")
)
}
/**
* 思路因为mammoth解析后变为html字符串使用new DOMParser()转换为DOM进行解析
* 作用创建DOMParser()对象然后解析需求规格说明的html字符串目前仅支持功能和接口需求
* 返回Array[Object[String,String]]
*/
export function parseHtmlStringByDemandDut(htmlString) {
const parser = new DOMParser()
const doc = parser.parseFromString(htmlString, "text/html")
const h1h2NodeList = doc.querySelectorAll("h1,h2")
// 这一步得到功能需求、接口需求的h2对象里面有dom、text、chapter
const h2ObjArray = getNeedH2NodeList(h1h2NodeList)
// 这里开始就要获取全部有用信息:
// 2.遍历全部DOM
const allArray = Array.from(doc.body.children)
const demandArray = []
let h2Index = 0
let locker = false
// 3.将H3和H4的索引增加
let currentH3ele = {
initChapter: "",
index: 0,
title: "",
ident: "",
isIn: false
}
let currentH4ele = {
initChapter: "",
index: 0,
title: "",
ident: ""
}
let adpterIndex = 0
allArray.forEach((element) => {
// 2.1.找到h2ObjArray的位置
if (h2ObjArray[h2Index] && element === h2ObjArray[h2Index].dom) {
h2Index += 1
currentH3ele.index = 0
locker = true
} else if (element.tagName === "H1" || element.tagName === "H2") {
locker = false
} else if (locker && element.tagName !== "H2") {
// 就是从H3开始需求的
if (element.tagName === "H3") {
// 按顺序解析到H3
currentH3ele.index += 1
currentH4ele.index = 0
const splitString = element.innerText.split(/[()]/)
currentH3ele.title = splitString[0]
currentH3ele.ident = splitString[1] ? splitString[1] : ""
currentH3ele.initChapter = h2ObjArray[h2Index - 1].chapter + "." + currentH3ele.index
// 将isIn变为true说明当前解析在这里面
currentH3ele.isIn = true
// 段落索引设置0
adpterIndex = 0
} else if (element.tagName === "H4") {
// 按顺序解析到H4
currentH4ele.index += 1
const splitString = element.innerText.split(/[()]/)
currentH4ele.title = splitString[0]
currentH4ele.ident = splitString[1] ? splitString[1] : ""
// 将H3的isIn变为false说明在H4里面不在H3了
currentH3ele.isIn = false
// chapter
currentH4ele.initChapter = currentH3ele.initChapter + "." + currentH4ele.index
// 段落索引
adpterIndex = 0
} else {
// 当currentH3ele的title有值的时候开始解析
if (currentH3ele.title) {
const demandObj = {
chapter: "",
title: "",
ident: "",
demandType: "",
content: ""
}
if (currentH3ele.isIn) {
demandObj.chapter = currentH3ele.initChapter
demandObj.title = currentH3ele.title
demandObj.ident = currentH3ele.ident
demandObj.demandType = demandObj.title.includes("接口") ? "3" : "1"
} else {
demandObj.chapter = currentH4ele.initChapter
demandObj.title = currentH4ele.title
demandObj.ident = currentH4ele.ident
demandObj.demandType = demandObj.title.includes("接口") ? "3" : "1"
}
// 1.解析table元素
if (element.tagName === "TABLE") {
demandObj.content = element.outerHTML
adpterIndex += 1
demandObj.ident = demandObj.ident + `-t${adpterIndex}`
demandArray.push(demandObj)
}
// 2.解析p元素-注意排除图片元素
if (element.tagName === "P" && !element.querySelector("img")) {
demandObj.content = element.innerText
adpterIndex += 1
demandObj.ident = demandObj.ident + `-p${adpterIndex}`
demandArray.push(demandObj)
}
// 3.解析ol和ul元素
if (element.tagName === "OL" || element.tagName === "UL") {
demandObj.content = element.innerHTML
adpterIndex += 1
demandObj.ident = demandObj.ident + `-u${adpterIndex}`
demandArray.push(demandObj)
}
}
}
}
})
return demandArray
}

View File

@@ -1,174 +0,0 @@
// 根据后端定义,不能更改的枚举
enum DemandType {
gn = "1",
xn = "2",
jk = "3",
kkx = "4",
aqx = "5",
other = "6"
}
// 数据每一项的结构
interface DemandObj {
chapter: string
title: string
ident?: string // 如果其标题有()则放入
demandType: DemandType
content: string
}
// 定义h元素对象接口
interface IHobj {
level: number // h1 -> 1
title: string // 标题文字
ident?: string // 如果有()则放入
index: number // 该标题的索引先map的时候不设置后续再排列
demandType: DemandType
}
// h元素对象带计算出的章节号的对象
interface IHobjWithChapter extends IHobj {
chapter: string
}
export class HtmlParser {
domArray: Element[] // 初始化得到一个元素的集合
hWithChapter: IHobjWithChapter[]
constructor(htmlText: string) {
const parser = new DOMParser()
const doc = parser.parseFromString(htmlText, "text/html")
this.domArray = Array.from(doc.body.children)
// 解析domArray将h元素加入
const HDomArray = this.domArray.filter((domItem) => domItem.tagName.startsWith("H"))
const storeArray: IHobj[] = []
HDomArray.forEach((it, i) => {
!it.textContent && (it.textContent = "")
// 这里判断是什么类型的设计需求 -> 后续可以添加
let type: DemandType = DemandType.gn
if (it.textContent.includes("接口")) {
type = DemandType.jk
} else if (it.textContent.includes("性能")) {
type = DemandType.xn
} else if (it.textContent.includes("可靠")) {
type = DemandType.kkx
} else if (it.textContent.includes("安全")) {
type = DemandType.aqx
}
// 获取章节号
const level = +it.tagName.slice(1)
// 1.判断当前的章节级别的前一个级别的level是否一样/小于/大于
let index = 0
if (i > 0) {
// 找上一个标题对象的level
if (level === storeArray[i - 1].level) {
index = storeArray[i - 1].index + 1
} else if (level < storeArray[i - 1].level) {
// 如果等级小于上一个标题对象,找storeArray里面level一样的长度
for (let j = storeArray.length - 1; j >= 0; j--) {
if (storeArray[j].level === level) {
index = storeArray[j].index + 1
break
}
}
}
}
storeArray.push({
level: level,
title: it.textContent,
ident: it.textContent.match(/[\(](.*?)[\)]/)?.[1],
index: index,
demandType: type
})
})
// 直接将storeArray计算出章节号
const chapterArray: any[] = storeArray.map((it, indx) => {
let str = it.index + 1 + ""
for (let i = it.level - 1; i > 0; i--) {
for (let j = indx - 1; j >= 0; j--) {
if (storeArray[j].level === i) {
str = storeArray[j].index + 1 + "." + str
break
}
}
}
return {
...it,
chapter: str
}
})
this.hWithChapter = chapterArray
}
/**
* 将所有Element元素遍历,输出列表格式
*/
parseToArray(): DemandObj[] {
const resArr: DemandObj[] = []
let index = 0
let currentHElement: IHobjWithChapter = this.hWithChapter[index]
let adapterIndex: number = 1
this.domArray.forEach((item) => {
// 1.如果循环到H元素将其存入
if (item.tagName.startsWith("H")) {
const text = item.textContent
if (text !== currentHElement.title) {
currentHElement = this.hWithChapter[index + 1]
index++
adapterIndex = 1
}
} else {
// 2.构造每一项放入 - chapter和content计算
let content = ""
let ident = currentHElement.ident
if (!ident) {
ident = ""
}
// 这里对图片进行处理
if (item.querySelector("img")) {
const img = item.querySelector("img")
if (img) {
const strblob = img.src + ""
const blob = strblob.split(",")[1]
content = `<img src="data:image/png;base64,${blob}" />`
}
ident += `-g${adapterIndex}`
adapterIndex++
resArr.push({
chapter: currentHElement.chapter,
title: currentHElement.title,
ident: ident,
demandType: currentHElement.demandType,
content
})
} else {
// 如果不是图片进行判断
if (item.textContent) {
if (item.tagName === "TABLE") {
content = item.outerHTML
ident += `-t${adapterIndex}`
adapterIndex++
}
if (item.tagName === "P" && !item.querySelector("img")) {
content = item.innerHTML.trim()
ident += `-p${adapterIndex}`
adapterIndex++
}
if (item.tagName === "OL" || item.tagName === "UL") {
content = item.innerHTML.trim()
ident += `-u${adapterIndex}`
adapterIndex++
}
// title要将括号去除
resArr.push({
chapter: currentHElement.chapter,
title: currentHElement.title,
ident: ident,
demandType: currentHElement.demandType,
content
})
}
}
}
})
return resArr
}
}

View File

@@ -9,7 +9,7 @@ const beiceType: BeiceTypeT[] = [
{ label: "设计说明", value: "SJ" },
{ label: "需求文档", value: "XQ" },
{ label: "通信协议", value: "XY" },
{ label: "研制总要求", value: "YZ" }
{ label: "研制总要求/技术协议等", value: "YZ" }
]
export default beiceType

View File

@@ -1,6 +1,8 @@
import { ref } from "vue"
import { ref, shallowRef } from "vue"
import { useRoute } from "vue-router"
import beiceType from "@/views/project/round/beiceType"
// 导入自定义组件
import UploadInput from "@/components/UploadInput/index.vue"
export default function (crudOrFormRef: any) {
// global
@@ -8,10 +10,8 @@ export default function (crudOrFormRef: any) {
// 计算注释率计算crud/form的数据判断
const calcPercent = () => {
const formData = crudOrFormRef.value.getFormData()
const { code_line, comment_line, mix_line, black_line } = formData
const total_line = +black_line + +code_line + +comment_line + +mix_line
const comment_total = +comment_line + +mix_line
formData.comment_percent = `${(comment_total / total_line).toFixed(2).toString()}%`
const { total_lines, comment_lines } = formData
formData.comment_percent = `${(comment_lines / total_lines).toFixed(2).toString()}%`
}
const crudColumns = ref([
{
@@ -51,28 +51,24 @@ export default function (crudOrFormRef: any) {
translation: true,
tagColors: { XQ: "blue", SO: "green", SJ: "orangered", XY: "pinkpurple", YZ: "red" }
},
onControl: (value) => {
onControl: (value: string) => {
if (value === "SO") {
return {
black_line: { display: true },
code_line: { display: true },
mix_line: { display: true },
comment_line: { display: true },
total_code_line: { display: true },
total_line: { display: true },
total_lines: { display: true },
effective_lines: { display: true },
comment_lines: { display: true },
comment_percent: { display: true },
upload: { display: true },
release_date: { display: false }
}
} else {
// 其他数据清除
return {
black_line: { display: false },
code_line: { display: false },
mix_line: { display: false },
comment_line: { display: false },
total_code_line: { display: false },
total_line: { display: false },
total_lines: { display: false },
effective_lines: { display: false },
comment_lines: { display: false },
comment_percent: { display: false },
upload: { display: false },
release_date: { display: true }
}
}
@@ -123,49 +119,36 @@ export default function (crudOrFormRef: any) {
formType: "date"
},
{
title: "空行",
title: "总行数",
hide: true,
align: "center",
dataIndex: "black_line",
dataIndex: "total_lines",
formType: "input-number",
commonRules: [{ required: true, message: "行数必填" }],
commonRules: [{ required: true, message: "行数必填" }],
min: 0,
onControl: () => {
calcPercent()
}
},
{
title: "纯代码行",
title: "有效行数",
hide: true,
align: "center",
dataIndex: "code_line",
dataIndex: "effective_lines",
formType: "input-number",
commonRules: [{ required: true, message: "纯代码行数必填" }],
commonRules: [{ required: true, message: "有效行数必填" }],
min: 0,
onControl: () => {
calcPercent()
}
},
{
title: "注释行",
title: "注释行",
hide: true,
align: "center",
dataIndex: "comment_line",
dataIndex: "comment_lines",
formType: "input-number",
commonRules: [{ required: true, message: "注释行数必填" }],
min: 0,
onControl: () => {
calcPercent()
}
},
{
title: "混合行",
hide: true,
align: "center",
dataIndex: "mix_line",
formType: "input-number",
help: "混合行是指:代码中一行即包含代码也包含注释",
commonRules: [{ required: true, message: "混合行数必填" }],
commonRules: [{ required: true, message: "注释行数必填" }],
min: 0,
onControl: () => {
calcPercent()
@@ -180,6 +163,15 @@ export default function (crudOrFormRef: any) {
addDisabled: true,
editDisabled: true,
disabled: true
},
{
title: "上传源代码",
align: "center",
dataIndex: "upload",
placeholder: "上传源代码",
hide: true,
formType: "component",
component: shallowRef(UploadInput)
}
])
return crudColumns

View File

@@ -10,7 +10,78 @@ export default function useOptions(formRef: any) {
})
const crudColumns = useColumn(formRef)
const columnOptions = computed(() => {
return tool.renameKeyInArray(crudColumns.value, "commonRules", "rules")
// 处理表单布局
const transformColumns = tool.renameKeyInArray(crudColumns.value, "commonRules", "rules")
// 取出字段column对象
const identColumn = transformColumns.find((item: any) => item.dataIndex === "ident")
const nameColumn = transformColumns.find((item: any) => item.dataIndex === "name")
const designPersonColumn = transformColumns.find((item: any) => item.dataIndex === "designPerson")
const testPersonColumn = transformColumns.find((item: any) => item.dataIndex === "testPerson")
const monitorPersonColumn = transformColumns.find((item: any) => item.dataIndex === "monitorPerson")
const summarizeColumn = transformColumns.find((item: any) => item.dataIndex === "summarize")
const initializationColumn = transformColumns.find((item: any) => item.dataIndex === "initialization")
const premiseColumn = transformColumns.find((item: any) => item.dataIndex === "premise")
const exe_timeColumn = transformColumns.find((item: any) => item.dataIndex === "exe_time")
// 组装表单布局
const identAndNameColumn = {
formType: "grid",
cols: [
{ span: 12, formList: [identColumn] },
{ span: 12, formList: [nameColumn] }
]
}
const cardColumn = {
formType: "card",
customClass: ["ml-5", "mb-3", "py-0", "px-0"],
title: "人员信息",
formList: [
{
formType: "grid",
cols: [
{ span: 8, formList: [designPersonColumn] },
{ span: 8, formList: [testPersonColumn] },
{ span: 8, formList: [monitorPersonColumn] }
]
}
]
}
const summarizeColumnNew = {
formType: "grid",
cols: [{ span: 24, formList: [summarizeColumn] }]
}
const initializationColumnNew = {
formType: "grid",
cols: [{ span: 24, formList: [initializationColumn] }]
}
const premiseAndExeColumn = {
formType: "grid",
cols: [
{ span: 12, formList: [premiseColumn] },
{ span: 12, formList: [exe_timeColumn] }
]
}
// 取除原数组里面的内容
const newColumnsArray = transformColumns.filter(
(it: any) =>
it.dataIndex !== "ident" &&
it.dataIndex !== "name" &&
it.dataIndex !== "designPerson" &&
it.dataIndex !== "testPerson" &&
it.dataIndex !== "monitorPerson" &&
it.dataIndex !== "summarize" &&
it.dataIndex !== "initialization" &&
it.dataIndex !== "premise" &&
it.dataIndex !== "exe_time"
)
newColumnsArray.unshift(
identAndNameColumn,
cardColumn,
summarizeColumnNew,
initializationColumnNew,
premiseAndExeColumn
)
return newColumnsArray
})
return { options, columnOptions }
}

View File

@@ -140,9 +140,20 @@ export default function (crudOrFormRef: any, problemFormRef?: any) {
},
{
title: "执行时间",
align: "center",
dataIndex: "exe_time",
formType: "date",
customRender: ({ record }) => {
// 如果不存在exe_time则显示为“没有设置”
return record.exe_time ? record.exe_time : <a-tag color="red"></a-tag>
}
},
{
title: "时序图(cpu不填写此字段)",
hide: true,
formType: "date"
dataIndex: "timing_diagram",
addDefaultValue: "",
formType: "editor"
},
{
title: "测试步骤",

View File

@@ -123,7 +123,7 @@ export default function (crudRef: Ref<InstanceType<typeof MaCrud>>) {
},
{
formType: "card",
customClass: ["ml-10", "mb-3", "py-0", "px-0"],
customClass: ["ml-5", "mb-3", "py-0", "px-0"],
title: "人员信息",
formList: [
{

View File

@@ -32,14 +32,12 @@ const useGenerateSecond = function () {
dgGenerateApi.createTechYiju({ id }), // 技术依据文件
dgGenerateApi.createContact({ id }), // 生成联系人和方式
dgGenerateApi.createTimeaddress({ id }), // 生成测评时间和地点
dgGenerateApi.createFuncList({ id }), // 生成被测软件功能列表
dgGenerateApi.createSoftComposition({ id }), // 生成测评对象-软件组成
dgGenerateApi.createAdequacyEffectiveness({ id }), // 生成测试充分性adequancy和有效性effectiveness说明
dgGenerateApi.createGroup({ id }), // 生成测评组织及分工
dgGenerateApi.createGuarantee({ id }), // 生成测评保障
dgGenerateApi.createAbbreviation({ id }), // 生成缩略语
dgGenerateApi.createInterface({ id }), // 生成-被测软件接口
dgGenerateApi.createPerformance({ id }), // 生成-被测软件性能
dgGenerateApi.createBaseInformation({ id }), // 生成-被测软件基本信息
dgGenerateApi.createLevelAndType({ id }), // 生成-测试级别和测试类型 -【修改】
dgGenerateApi.createStrategy({ id }), // 生成-测试策略 -【新增】
@@ -47,6 +45,8 @@ const useGenerateSecond = function () {
dgGenerateApi.createXqComparison({ id }), // 生成-需求规格说明-测试项对照表
dgGenerateApi.createFanXqComparison({ id }), // 生成-反向测试项-需求规格说明对照表
dgGenerateApi.createCodeQuality({ id }), // 生成-代码质量度量分析表
// 2025年4月29日新增 - 顶层技术文件
dgGenerateApi.createTopFile({ id }), // 生成顶层技术文件
// 新增拆分接口
dgGenerateApi.createStaticEnvironment({ id }), // 生成-静态测试环境说明
dgGenerateApi.createStaticSoft({ id }), // 生成-静态软件项
@@ -69,9 +69,7 @@ const useGenerateSecond = function () {
isSmLoading.value = true
await Promise.all([
dgGenerateApi.createSoftComposition({ id }), // 生成测评对象 - 和大纲一样
dgGenerateApi.createFuncList({ id }), // 生成被测软件功能 - 和大纲重复
dgGenerateApi.createInterface({ id }), // 生成被测软件接口 - 和大纲重复 - 可能会删除
dgGenerateApi.createPerformance({ id }), // 生成被测软件性能 - 和大纲重复 - 可能会删除
dgGenerateApi.createBaseInformation({ id }), // 生成被测软件基本信息 - 和大纲重复 - 可能会删除
dgGenerateApi.createYiju({ id }), // 生成标准类引用文档 - 和大纲重复 - 可能会删除
smGenerateApi.createSMTechyiju({ id }), // 生成技术类引用文档列表 -> 在大纲基础上添加《测评大纲》
@@ -97,7 +95,12 @@ const useGenerateSecond = function () {
const createJLItem = async (id: number) => {
isGenerating.value = true
isJlloading.value = true
await jlGenerateApi.createJLcaserecord({ id }).finally(() => {
await Promise.all([
// 生成-被测软件基本信息 - 和大纲一样
dgGenerateApi.createBaseInformation({ id }),
// 记录相关片段
jlGenerateApi.createJLcaserecord({ id })
]).finally(() => {
isGenerating.value = false
isJlloading.value = false
})
@@ -116,7 +119,10 @@ const useGenerateSecond = function () {
hsmGenerateApi.createCaseListDesc({ id }),
hsmGenerateApi.createCaseList({ id }),
hsmGenerateApi.createTrack({ id }),
// 拆分大纲软硬件环境
// 拆分大纲软硬件环境-大纲内容
dgGenerateApi.createSoftComposition({ id }), // 生成测评对象-软件组成
dgGenerateApi.createYiju({ id }), // 生成依据文件
dgGenerateApi.createInterface({ id }), // 生成-被测软件接口 - 和大纲一样
dgGenerateApi.createStaticEnvironment({ id }), // 生成-静态测试环境说明
dgGenerateApi.createStaticSoft({ id }), // 生成-静态软件项
dgGenerateApi.createStaticHard({ id }), // 生成-静态硬件和固件项
@@ -171,7 +177,15 @@ const useGenerateSecond = function () {
bgGenerateApi.createBgEntire({ id }),
bgGenerateApi.createBgYzxqTrack({ id }),
bgGenerateApi.createBgProblemsSummary({ id }),
// 拆分软硬件环境
// 2025年4月27日新增软件问题统计
bgGenerateApi.createProblemStatistics({ id }), // 生成软件问题统计
// 2025年4月28日新增摸底清单
bgGenerateApi.createBgModiList({ id }), // 生成摸底清单
// 拆分软硬件环境-大纲内容
dgGenerateApi.createSoftComposition({ id }), // 生成测评对象 - 大纲内容
dgGenerateApi.createYiju({ id }), // 生成依据文件
dgGenerateApi.createInterface({ id }), // 生成-被测软件接口 - 大纲内容
dgGenerateApi.createAbbreviation({ id }), // 生成缩略语 - 大纲内容
dgGenerateApi.createStaticEnvironment({ id }), // 生成-静态测试环境说明
dgGenerateApi.createStaticSoft({ id }), // 生成-静态软件项
dgGenerateApi.createStaticHard({ id }), // 生成-静态硬件和固件项
@@ -179,7 +193,9 @@ const useGenerateSecond = function () {
dgGenerateApi.createDynamicSoft({ id }), // 生成-动态软件项
dgGenerateApi.createDynamicHard({ id }), // 生成-动态硬件和固件项
dgGenerateApi.createTestData({ id }), // 生成-测评数据
dgGenerateApi.createEnvDiff({ id }) // 生成-环境差异性分析
dgGenerateApi.createEnvDiff({ id }), // 生成-环境差异性分析
// 2025年4月29日新增 - 顶层技术文件
dgGenerateApi.createTopFile({ id }) // 生成顶层技术文件
]).finally(() => {
isGenerating.value = false
isBgLoading.value = false