增加AI问答生成测试项

This commit is contained in:
2025-12-02 18:09:17 +08:00
parent 80ef6f7ce8
commit 03aed85d86
14 changed files with 563 additions and 238 deletions

View File

@@ -0,0 +1,23 @@
import { request } from "@/api/request"
const AI_API_BASE = import.meta.env.VUE_APP_AI_API_BASE || "http://192.168.0.63:8777"
interface DataRowType {
question: string
stream: boolean
}
export default {
/**
* 请求AI生成测试项
* @returns 可流式或一次性
*/
getAiTestItem(data: DataRowType) {
return request({
url: `${AI_API_BASE}/api/local_doc_qa/testing_item`,
timeout: 20000,
method: "post",
data
})
}
}

View File

@@ -0,0 +1,81 @@
<style scoped>
button {
position: relative;
padding: 10px 20px;
border-radius: 0px;
border: 1px solid rgb(61, 106, 255);
font-size: 14px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 2px;
background: transparent;
color: rgb(61, 106, 255);
overflow: hidden;
box-shadow: 0 0 0 0 transparent;
-webkit-transition: all 0.2s ease-in;
-moz-transition: all 0.2s ease-in;
transition: all 0.2s ease-in;
}
button:hover {
background: rgb(61, 106, 255);
box-shadow: 0 0 30px 5px rgba(0, 142, 236, 0.815);
-webkit-transition: all 0.2s ease-out;
-moz-transition: all 0.2s ease-out;
transition: all 0.2s ease-out;
color: #fff;
cursor: pointer;
}
button:hover::before {
-webkit-animation: sh02 0.5s 0s linear;
-moz-animation: sh02 0.5s 0s linear;
animation: sh02 0.5s 0s linear;
}
button::before {
content: "";
display: block;
width: 0px;
height: 86%;
position: absolute;
top: 7%;
left: 0%;
opacity: 0;
background: #fff;
box-shadow: 0 0 50px 30px #fff;
-webkit-transform: skewX(-20deg);
-moz-transform: skewX(-20deg);
-ms-transform: skewX(-20deg);
-o-transform: skewX(-20deg);
transform: skewX(-20deg);
}
@keyframes sh02 {
from {
opacity: 0;
left: 0%;
}
50% {
opacity: 1;
}
to {
opacity: 0;
left: 100%;
}
}
button:active {
box-shadow: 0 0 0 0 transparent;
-webkit-transition: box-shadow 0.2s ease-in;
-moz-transition: box-shadow 0.2s ease-in;
transition: box-shadow 0.2s ease-in;
}
</style>
<template>
<button>AI生成测试项</button>
</template>

View File

@@ -85,7 +85,7 @@
<template v-else>
<tr>
<td colspan="3">
<div class="flex justify-center items-center p-2 border-1">
<div class="flex justify-center items-center p-2 border">
<a-alert>暂无测试子项条目请添加</a-alert>
</div>
</td>

View File

@@ -91,4 +91,15 @@ tool.chnRoundNameArray = [
"第十六轮"
]
// 将html变为纯文本
tool.htmlToTextWithDOM = (htmlString) => {
// 1. 创建一个临时的div元素
const tempDiv = document.createElement("div")
// 2. 将HTML字符串设置为临时div的内容
tempDiv.innerHTML = htmlString
// 3. 使用innerText属性获取纯文本这会自动忽略所有HTML标签
const text = tempDiv.innerText || tempDiv.textContent
return text
}
export default tool

View File

@@ -0,0 +1,219 @@
<template>
<div class="ai-modal-container">
<a-modal v-model:visible="visible" width="80%" unmount-on-close draggable>
<template #title> AI生成测试项 </template>
<div class="flex flex-col">
<a-button type="primary" :disabled="generateLoading" @click="generateClick">{{
generateLoading ? "AI正在生成测试项中..." : "点击生成测试项"
}}</a-button>
<a-progress
:percent="percent"
:style="{ width: '100%' }"
size="large"
:show-text="false"
:color="{
'0%': 'rgb(var(--primary-6))',
'100%': 'rgb(var(--success-6))'
}"
class="mb-2"
/>
<a-list :loading="listLoading" :data="dataList">
<template #header> 设计需求{{ designObj?.name ?? "暂无内容" }} </template>
<template #item="{ item, index }">
<a-list-item>
<div class="item-container">
<a-input-group>
<div class="index-hao">{{ indexTu[index] }}</div>
测试项
<a-input placeholder="测试项标识" v-model="item.ident" :style="{ width: '100px' }" @click.stop.prevent></a-input>
<a-input placeholder="测试项名称" v-model="item.title" :style="{ width: '200px' }" @click.stop.prevent></a-input>
<a-select placeholder="选择优先级" v-model="item.priority" :style="{ width: '100px' }">
<a-option value="1"></a-option>
<a-option value="2"></a-option>
<a-option value="3"></a-option>
</a-select>
<a-select placeholder="选择测试类型" v-model="item.testType" :style="{ width: '200px' }">
<a-option v-for="type in testType" :key="type.key" :value="type.key">
{{ type.title }}
</a-option>
</a-select>
<a-select placeholder="选择测试手段" multiple v-model="item.testMethod" :style="{ width: '250px' }">
<a-option v-for="method in testMethod" :key="method.key" :value="method.key">
{{ method.title }}
</a-option>
</a-select>
</a-input-group>
<div class="m-2 flex justify-start items-center">
<div class="label">测试项描述</div>
<div class="input flex-1">
<a-input v-model="item.demandDescription"></a-input>
</div>
</div>
<div class="arco-table arco-table-size-large arco-table-border arco-table-stripe arco-table-hover">
<div class="arco-table-container">
<table class="arco-table-element" cellpadding="0" cellspacing="0">
<thead>
<tr class="arco-table-tr">
<th class="arco-table-th" :width="100">
<span class="arco-table-cell arco-table-cell-align-center">
<span class="arco-table-th-title">子项序号</span>
</span>
</th>
<th class="arco-table-th" :width="400">
<span class="arco-table-cell arco-table-cell-align-center">
<span class="arco-table-th-title">测试子项描述</span>
</span>
</th>
<th class="arco-table-th" :width="800">
<span class="arco-table-cell arco-table-cell-align-center">
<span class="arco-table-th-title">测试子项步骤</span>
</span>
</th>
</tr>
</thead>
<tbody>
<!-- 这里tr要v-for渲染 -->
<tr class="arco-table-tr" v-for="(row, idx) in item.children" :key="idx">
<td class="arco-table-td">
<span class="arco-table-cell arco-table-cell-align-center">
{{ idx + 1 }}
</span>
</td>
<td class="arco-table-td">
<span class="arco-table-cell">
<a-textarea auto-size placeholder="请填写测试子项名称" v-model="row.name"></a-textarea>
</span>
</td>
<td class="arco-table-td">
<span class="arco-table-cell">
<OpeAndExpect v-model="row.subStep" />
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</a-list-item>
</template>
</a-list>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { onUnmounted, ref } from "vue"
import { useRoute } from "vue-router"
import designApi from "@/api/project/designDemand"
import dictApi from "@/api/common"
import OpeAndExpect from "./OpeAndExpect.vue" // 操作和预期子表格
import aiApi from "@/api/outs/aiApi"
import { Message } from "@arco-design/web-vue"
import tool from "@/utils/tool"
// 常量
const indexTu = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚"
// 初始化测试类型-一起请求处理错误
const testType = ref<any>([])
const testMethod = ref<any>([])
const fetchTestType = async () => {
try {
const [typeResponse, methodResponse] = await Promise.all([dictApi.getDict("testType"), dictApi.getDict("testMethod")])
testType.value = typeResponse.data
testMethod.value = methodResponse.data
} catch (e) {
Message.error("初始化测试类型或测试手段错误,请检查网络后重试!")
}
}
fetchTestType()
// 初始化设计需求
const route = useRoute()
const getDesign = async () => {
try {
const res = await designApi.getDesignDemandOne({ project_id: route.query.id, key: route.query.key })
designObj.value = res.data
} catch (e) {
Message.error("初始化设计需求信息错误,请检查网络后重试!")
}
}
getDesign()
const designObj: any = ref()
// 进度条和列表加载loading
const percent = ref(0.0)
const listLoading = ref(false)
// 根据测试项生成按钮
const generateLoading = ref(false)
const generateClick = async () => {
try {
generateLoading.value = true
listLoading.value = true
percent.value = 0.1 // 开始进度
startProgressSimulation()
// 变量给AI的问题
const question = tool.htmlToTextWithDOM(designObj.value?.description || "")
console.log("给AI的问题如下", question)
const res = await aiApi.getAiTestItem({ question: question, stream: false })
percent.value = 1.0 // 完成进度
console.log("AI接口返回如下", res)
Message.success("生成测试项成功,请完善信息后录入数据")
} catch (e) {
percent.value = 0.0
} finally {
stopProgressSimulation()
generateLoading.value = false
setTimeout(() => {
listLoading.value = false
}, 500)
}
}
// 生成的AI测试项数据
const dataList = ref([])
// 进度条模拟变量和函数
const progressInterval = ref<NodeJS.Timeout>()
const startProgressSimulation = () => {
progressInterval.value = setInterval(() => {
if (percent.value < 0.8) {
percent.value += (0.8 - percent.value) * 0.1
}
}, 200)
}
const stopProgressSimulation = () => {
if (progressInterval.value) {
clearInterval(progressInterval.value)
progressInterval.value = undefined
}
}
onUnmounted(() => {
stopProgressSimulation()
})
// defineModel
const visible = defineModel<boolean>("visible", { default: false })
</script>
<style scoped lang="less">
.index-hao {
font-size: 18px;
padding: 0 9px;
color: rgb(var(--primary-5));
}
:deep(.arco-list-item) {
border: 1px solid #999 !important;
}
:deep(.arco-progress-line-bar) {
border-radius: 0 !important;
}
:deep(.arco-progress-line) {
border-radius: 0 !important;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="arco-table arco-table-size-large arco-table-border arco-table-stripe arco-table-hover">
<div class="arco-table-container">
<table class="arco-table-element" cellpadding="0" cellspacing="0">
<thead>
<tr class="arco-table-tr">
<th class="arco-table-th" :width="400">
<span class="arco-table-cell arco-table-cell-align-center">
<span class="arco-table-th-title">操作</span>
</span>
</th>
<th class="arco-table-th" :width="400">
<span class="arco-table-cell arco-table-cell-align-center">
<span class="arco-table-th-title">预期</span>
</span>
</th>
</tr>
</thead>
<tbody>
<tr class="arco-table-tr" v-for="(record, idx) in modelValue" :key="idx">
<td class="arco-table-td">
<span class="arco-table-cell arco-table-cell-align-center">
<a-textarea auto-size placeholder="请填写步骤" v-model="record.operation"></a-textarea>
</span>
</td>
<td class="arco-table-td">
<span class="arco-table-cell arco-table-cell-align-center">
<a-textarea auto-size placeholder="请填写预期" v-model="record.expect"></a-textarea>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
const modelValue = defineModel<
{
operation: string
expect: string
}[]
>()
</script>
<style scoped></style>

View File

@@ -26,6 +26,8 @@
<a-button type="outline" @click="handleReplaceClick">批量替换</a-button>
<a-divider direction="vertical" type="double" />
<a-button type="outline" @click="handleOpenReplacePriority">批量修改优先级</a-button>
<a-divider direction="vertical" type="double" />
<AiButton @click="handleAiButtonClick" />
</a-space>
</template>
<!-- 版本字段的插槽 -->
@@ -66,6 +68,8 @@
/>
<!-- 批量修改优先级 -->
<ReplacePriority @modifySuccess="crudRef.refresh()" ref="replacePriorityRef" />
<!-- AI-Modal -->
<AiModal v-model:visible="ai_modal_visible"></AiModal>
</div>
</template>
@@ -74,6 +78,8 @@ import { ref } from "vue"
import commonApi from "@/api/common"
import { useRoute } from "vue-router"
import { Message } from "@arco-design/web-vue"
import AiButton from "@/components/ai-button/index.vue"
import AiModal from "./AiModal.vue"
// hooks
import useCrudOpMore from "./hooks/useCrudOpMore"
import useColumn from "./hooks/useColumns"
@@ -106,6 +112,7 @@ const handleOpenReplacePriority = () => {
// 根据传参获取key分别为轮次、设计需求的key
const { projectId, crudOptions, handleBeforeCancel } = useCrudOpMore(crudRef)
const crudColumns = useColumn(crudRef)
// 关联弹窗、关联的事件处理
const { visible, relatedData, options, cascaderLoading, computedRelatedData, handleOpenRelationCSX, handleRelatedOk } =
useRalateDemand(projectId)
@@ -127,6 +134,12 @@ const showType = (record) => {
}
}
// AI-MODAL
const ai_modal_visible = ref(false)
const handleAiButtonClick = () => {
ai_modal_visible.value = true
}
// 暴露给route-view的刷新表格函数
const refreshCrudTable = () => {
crudRef.value.refresh()