Compare commits
11 Commits
v0.1.1
...
089b8e3943
| Author | SHA1 | Date | |
|---|---|---|---|
| 089b8e3943 | |||
| 5b6a0a804f | |||
| dddc65ae73 | |||
| da58d3c73e | |||
| 7e7bfce143 | |||
| 619d5ea652 | |||
| 98835a3225 | |||
| a5a3ee9bb5 | |||
| 9c1c39d9a9 | |||
| 4a50e0ef48 | |||
| 72c080230f |
@@ -1,7 +1,3 @@
|
||||
# Vue 3 + Vite
|
||||
# 测试管理平台
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
- web管理测试过程,在此平台写用例等内容
|
||||
|
||||
1143
cdTMP/package-lock.json
generated
1143
cdTMP/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "testplant",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -15,8 +15,8 @@
|
||||
"@arco-design/web-vue": "^2.57.0",
|
||||
"@tanstack/vue-query": "^5.92.9",
|
||||
"@tinymce/tinymce-vue": "^6.3.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"axios": "^1.13.4",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
@@ -28,32 +28,32 @@
|
||||
"pinia": "^3.0.4",
|
||||
"pinyin-match": "^1.2.10",
|
||||
"postcss-import": "^16.1.1",
|
||||
"qs": "^6.14.1",
|
||||
"qs": "^6.14.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tinymce": "^7.9.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vue": "^3.5.27",
|
||||
"vue": "^3.5.28",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-color-kit": "^1.0.6",
|
||||
"vue-data-ui": "^3.13.4",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-data-ui": "^3.14.10",
|
||||
"vue-router": "^5.0.2",
|
||||
"vuedraggable": "^2.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||
"@vue/babel-plugin-jsx": "^2.0.1",
|
||||
"browserslist": "^4.28.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-plugin-vue": "^10.7.0",
|
||||
"less": "^4.5.1",
|
||||
"less-loader": "^12.3.0",
|
||||
"less-loader": "^12.3.1",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.8.1",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,5 +110,127 @@ export default {
|
||||
id: projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 新增或者修改软件概述
|
||||
* @returns null
|
||||
*/
|
||||
postSoftSummary(data) {
|
||||
return request({
|
||||
url: "/testmanage/project/soft_summary/",
|
||||
method: "post",
|
||||
data: data
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 新增或者修改动态环境描述
|
||||
* @returns null
|
||||
*/
|
||||
postDynamicDescription(data) {
|
||||
return request({
|
||||
url: "/testmanage/project/dynamic_description/",
|
||||
method: "post",
|
||||
data: data
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取项目的软件概述
|
||||
* @returns 返回软件概述数据
|
||||
*/
|
||||
getSoftSummary(id) {
|
||||
return request({
|
||||
url: "/testmanage/project/get_soft_summary/",
|
||||
method: "get",
|
||||
params: { id: id }
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取动态环境描述
|
||||
* @returns 返回动态环境描述结构化数据
|
||||
*/
|
||||
getDynamicDescription(id) {
|
||||
return request({
|
||||
url: "/testmanage/project/dynamic_des/",
|
||||
method: "get",
|
||||
params: { id: id }
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 提交修改或新增软件接口图
|
||||
* @returns 返回新增或修改是否成功
|
||||
*/
|
||||
postInterfaceImage(id, data) {
|
||||
return request({
|
||||
url: "/testmanage/project/interface_image/",
|
||||
method: "post",
|
||||
params: { id: id },
|
||||
data
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 提交修改或新增软件接口图
|
||||
* @returns 返回新增或修改是否成功
|
||||
*/
|
||||
getInterfaceImage(id) {
|
||||
return request({
|
||||
url: "/testmanage/project/get_interface_image/",
|
||||
method: "get",
|
||||
params: { id: id }
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取静态软件项、静态硬件项、动态软件项、动态硬件项的数据
|
||||
* @returns 返回数据
|
||||
*/
|
||||
getStaticDynamicItems(id, category) {
|
||||
return request({
|
||||
url: "/testmanage/project/get_static_dynamic_items/",
|
||||
method: "get",
|
||||
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
|
||||
*/
|
||||
postStaticDynamicItems(data) {
|
||||
return request({
|
||||
url: "/testmanage/project/post_static_dynamic_item/",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取所有状态
|
||||
* @returns 返回是否填写软件概述等等是否已经填写
|
||||
*/
|
||||
getAllStatus(id) {
|
||||
return request({
|
||||
url: "/testmanage/project/project_info_status/",
|
||||
method: "get",
|
||||
params: { id: id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
64
cdTMP/src/components/GlobalLoading/index.vue
Normal file
64
cdTMP/src/components/GlobalLoading/index.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div v-if="visible" class="global-loading">
|
||||
<div class="spinner"></div>
|
||||
<div v-if="text" class="loading-text">{{ text }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const visible = ref(false)
|
||||
const text = ref("加载中...")
|
||||
const show = (loadingText = "加载中...") => {
|
||||
text.value = loadingText
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 暴露方法供外部调用
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.global-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: rgb(var(--blue-6));
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 10px;
|
||||
color: rgb(var(--blue-6));
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
178
cdTMP/src/layout/components/Influence/EffectModal/index.vue
Normal file
178
cdTMP/src/layout/components/Influence/EffectModal/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<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 () => {
|
||||
// Iterator Helper
|
||||
datas.value.values().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) {
|
||||
// 有影响域分析 - 注意使用了Iterator Helper,老版本浏览器可能会不支持
|
||||
datas.value = res2.data
|
||||
.values()
|
||||
.map((item: any) => ({
|
||||
...item,
|
||||
id: item.id || `loaded_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
||||
}))
|
||||
.toArray()
|
||||
} else {
|
||||
Message.info("暂未填写影响域分析,请填写")
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
cascaderLoading.value = false
|
||||
proxy?.$loading?.hide()
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
defineOptions({
|
||||
name: "EffectModal"
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface DataInterface {
|
||||
change_type: string
|
||||
change_des?: string
|
||||
effect_cases?: string[]
|
||||
change_influ?: string
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
47
cdTMP/src/layout/components/Influence/hooks/useColumns.tsx
Normal file
47
cdTMP/src/layout/components/Influence/hooks/useColumns.tsx
Normal 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 }
|
||||
}
|
||||
51
cdTMP/src/layout/components/Influence/index.vue
Normal file
51
cdTMP/src/layout/components/Influence/index.vue
Normal 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>
|
||||
6
cdTMP/src/layout/components/Influence/types.ts
Normal file
6
cdTMP/src/layout/components/Influence/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface NodeDataInterface {
|
||||
title: string
|
||||
key: string
|
||||
level: string
|
||||
children?: NodeDataInterface[]
|
||||
}
|
||||
@@ -5,31 +5,18 @@
|
||||
<div class="logo-container">
|
||||
<img src="@/assets/img/wxwx-logo.svg" class="logo" alt="logo" @click="handleClickLogo" />
|
||||
</div>
|
||||
<a-typography-title
|
||||
class="title"
|
||||
@click="handleClickLogo"
|
||||
:style="{ margin: 0, fontSize: '18px' }"
|
||||
:heading="5"
|
||||
>
|
||||
<div
|
||||
class="font-extrabold bg-clip-text text-transparent bg-linear-to-r from-blue-500 to-purple-600"
|
||||
>
|
||||
测试管理平台
|
||||
</div>
|
||||
<a-typography-title class="title" @click="handleClickLogo" :style="{ margin: 0, fontSize: '18px' }" :heading="5">
|
||||
<div class="font-extrabold bg-clip-text text-transparent bg-linear-to-r from-blue-500 to-purple-600">测试管理平台</div>
|
||||
</a-typography-title>
|
||||
<a-typography-title :heading="6" class="version">v{{ $version }}</a-typography-title>
|
||||
<icon-menu-fold
|
||||
v-if="!topMenu && appStore.device === 'mobile'"
|
||||
style="font-size: 22px; cursor: pointer"
|
||||
@click="toggleDrawerMenu"
|
||||
/>
|
||||
<icon-menu-fold v-if="!topMenu && appStore.device === 'mobile'" style="font-size: 22px; cursor: pointer" @click="toggleDrawerMenu" />
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="center-side flex items-center justify-center font-bold text-lg">
|
||||
<template v-if="title">
|
||||
<a-typography-title
|
||||
:style="{ margin: 0, fontSize: '1.1rem', fontWeight: 'bold' }"
|
||||
:heading="4"
|
||||
:style="{ margin: 0, fontSize: '1.2rem', fontWeight: 'bold' }"
|
||||
:heading="3"
|
||||
:ellipsis="{
|
||||
rows: 2
|
||||
}"
|
||||
@@ -102,6 +89,9 @@
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="fix-side" v-if="route.query.id">
|
||||
<project-info-other />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -114,6 +104,9 @@ import useUser from "@/hooks/logout"
|
||||
import { Message } from "@arco-design/web-vue"
|
||||
import Menu from "@/layout/components/menu.vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
// 项目信息录入组件
|
||||
import projectInfoOther from "./projectInfoOther/index.vue"
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
@@ -165,6 +158,14 @@ const handleClickLogo = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 项目管理悬浮定位
|
||||
.fix-side {
|
||||
position: absolute;
|
||||
left: 16%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
perspective: 1000px;
|
||||
.logo {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="interface-image-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>软件接口图</template>
|
||||
<div class="flex justify-center items-center">
|
||||
<ImageInput v-model="imageUrl" v-model:fontnote="fontnote" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, getCurrentInstance } from "vue"
|
||||
import ImageInput from "./projectModal/ImageInput/index.vue"
|
||||
import { Message } from "@arco-design/web-vue"
|
||||
import projectApi from "@/api/project/project"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
const route = useRoute()
|
||||
const visible = ref(false)
|
||||
|
||||
// props
|
||||
const { reset } = defineProps<{
|
||||
reset: () => void
|
||||
}>()
|
||||
|
||||
// 题注和图片数据
|
||||
const fontnote = ref("")
|
||||
const imageUrl = ref("")
|
||||
|
||||
const handleSyncOk = async () => {
|
||||
// 验证题注是否为空
|
||||
if (fontnote.value.trim().length <= 0 || !imageUrl.value.trim()) {
|
||||
Message.error("请输入题注和粘贴图片")
|
||||
return false
|
||||
}
|
||||
// 提交数据
|
||||
try {
|
||||
await projectApi.postInterfaceImage(route.query.id, { fontnote: fontnote.value, content: imageUrl.value, type: "image" })
|
||||
Message.success("保存成功")
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnClose = () => {
|
||||
// 用来清空数据
|
||||
fontnote.value = ""
|
||||
imageUrl.value = ""
|
||||
reset()
|
||||
}
|
||||
|
||||
const open = async () => {
|
||||
proxy?.$loading?.show("数据加载中...")
|
||||
try {
|
||||
const { data } = await projectApi.getInterfaceImage(route.query.id)
|
||||
fontnote.value = data.fontnote
|
||||
imageUrl.value = data.content
|
||||
visible.value = true
|
||||
} catch (e) {
|
||||
} finally {
|
||||
proxy?.$loading?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="static-dynamic-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>
|
||||
<WordLikeTable v-model="tableData" v-model:fontnote="fontnote" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from "@arco-design/web-vue"
|
||||
import { getCurrentInstance, ref } from "vue"
|
||||
import WordLikeTable from "./projectModal/wordLikeTable/index.vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import projectApi from "@/api/project/project"
|
||||
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
const route = useRoute()
|
||||
|
||||
// props
|
||||
const { reset } = defineProps<{
|
||||
reset: () => void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const theTitle = ref("")
|
||||
|
||||
const tableInitValue = [
|
||||
["", "", ""],
|
||||
["", "", ""],
|
||||
["", "", ""]
|
||||
]
|
||||
const tableData = ref(tableInitValue)
|
||||
const fontnote = ref("")
|
||||
|
||||
const handleSyncOk = async () => {
|
||||
// 验证题注是否为空
|
||||
if (tableData.value.length <= 0) {
|
||||
Message.error("请输入表格内容再提交")
|
||||
return false
|
||||
}
|
||||
try {
|
||||
// 请求接口
|
||||
await projectApi.postStaticDynamicItems({
|
||||
id: route.query.id,
|
||||
category: theTitle.value,
|
||||
table: tableData.value,
|
||||
fontnote: fontnote.value
|
||||
})
|
||||
Message.success("保存成功")
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnClose = () => {
|
||||
// 用来清空数据
|
||||
tableData.value = cloneDeep(tableInitValue)
|
||||
fontnote.value = ""
|
||||
reset()
|
||||
}
|
||||
|
||||
const open = async (title: string) => {
|
||||
proxy?.$loading?.show("数据加载中...")
|
||||
theTitle.value = title
|
||||
try {
|
||||
// 获取数据并赋值给tableData
|
||||
const res = await projectApi.getStaticDynamicItems(route.query.id, title)
|
||||
if (res.code === 25001) {
|
||||
const data = res.data
|
||||
tableData.value = data.table
|
||||
fontnote.value = data.fontnote
|
||||
}
|
||||
visible.value = true
|
||||
} catch (e) {
|
||||
} finally {
|
||||
proxy?.$loading?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
107
cdTMP/src/layout/components/projectInfoOther/TextAndTable.vue
Normal file
107
cdTMP/src/layout/components/projectInfoOther/TextAndTable.vue
Normal 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>
|
||||
159
cdTMP/src/layout/components/projectInfoOther/index.vue
Normal file
159
cdTMP/src/layout/components/projectInfoOther/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="project-info-other-container">
|
||||
<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 size="20px" />
|
||||
</span>
|
||||
<span class="text-red-500" v-else><icon-exclamation-circle-fill size="20px" /></span>
|
||||
</a-tooltip>
|
||||
</SettingButton>
|
||||
</a-space>
|
||||
<template #content>
|
||||
<template v-for="item in inputOptions" :key="item.name">
|
||||
<template v-if="!item.status">
|
||||
<a-tooltip :content="`还未录入${item.title}`">
|
||||
<a-doption @click="item.handler">
|
||||
<span class="mr-1">{{ item.title }}</span>
|
||||
<span class="text-red-500"><icon-exclamation-circle-fill /></span>
|
||||
</a-doption>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-doption @click="item.handler">
|
||||
<span class="mr-1">{{ item.title }}</span>
|
||||
<span class="text-green-500"><icon-check-circle-fill /></span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<!-- 软件概述... -->
|
||||
<project-modal ref="projectModalRef" :reset="fetchAllStatus" />
|
||||
<!-- 接口图 -->
|
||||
<InterfaceImage ref="interfaceImageRef" :reset="fetchAllStatus" />
|
||||
<!-- 静态软件项、静态硬件项、动态软件项、动态硬件项 -->
|
||||
<StaticDynamicTable ref="staticDynamiRef" :reset="fetchAllStatus" />
|
||||
<!-- 环境差异性分析 -->
|
||||
<TextAndTable ref="textAndTableRef" :reset="fetchAllStatus" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, useTemplateRef } from "vue"
|
||||
import projectApi from "@/api/project/project"
|
||||
import { useRoute } from "vue-router"
|
||||
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()
|
||||
|
||||
// ref
|
||||
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) => {
|
||||
projectModalRef.value?.open(category)
|
||||
}
|
||||
const clickInterfaceImage = async () => {
|
||||
interfaceImageRef.value?.open()
|
||||
}
|
||||
const clickStaticDynamic = async (title: string) => {
|
||||
staticDynamiRef.value?.open(title)
|
||||
}
|
||||
const clickTextAndTable = async (title: string) => {
|
||||
textAndTableRef.value?.open(title)
|
||||
}
|
||||
|
||||
// 进入页面时候请求知道各项目样式情况-ref
|
||||
const fetchAllStatus = async () => {
|
||||
try {
|
||||
const { data }: { data: Object } = await projectApi.getAllStatus(route.query.id)
|
||||
inputOptions.value = inputOptions.value.map((it) => {
|
||||
if (data.hasOwnProperty(it.name)) {
|
||||
it.status = data[it.name]
|
||||
}
|
||||
return { ...it }
|
||||
})
|
||||
} catch (e) {
|
||||
Message.error("查询项目级信息是否填写失败,请检查网络")
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAllStatus()
|
||||
})
|
||||
|
||||
const inputOptions = ref([
|
||||
{
|
||||
name: "soft_summary",
|
||||
title: "软件概述",
|
||||
status: false,
|
||||
handler: () => clickStuctDatas("软件概述")
|
||||
},
|
||||
{
|
||||
name: "interface_image",
|
||||
title: "接口图",
|
||||
status: false,
|
||||
handler: clickInterfaceImage
|
||||
},
|
||||
{
|
||||
name: "static_soft_item",
|
||||
title: "静态软件项表",
|
||||
status: false,
|
||||
handler: () => clickStaticDynamic("静态软件项")
|
||||
},
|
||||
{
|
||||
name: "static_soft_hardware",
|
||||
title: "静态硬件项表",
|
||||
status: false,
|
||||
handler: () => clickStaticDynamic("静态硬件项")
|
||||
},
|
||||
{
|
||||
name: "dynamic_des",
|
||||
title: "动态环境描述",
|
||||
status: false,
|
||||
handler: () => clickStuctDatas("动态环境描述")
|
||||
},
|
||||
{
|
||||
name: "dynamic_soft_item",
|
||||
title: "动态软件项表",
|
||||
status: false,
|
||||
handler: () => clickStaticDynamic("动态软件项")
|
||||
},
|
||||
{
|
||||
name: "dynamic_soft_hardware",
|
||||
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))
|
||||
|
||||
defineOptions({
|
||||
name: "ProjectInfoOther"
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<!-- 组件:该组件显示一个正方体,可粘贴图片内容进去并展示,并生成base64到数据里面 -->
|
||||
<div class="image-input-container flex flex-col gap-1">
|
||||
<div class="image-input-handle flex items-center justify-center" @paste="handlePaste">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading">
|
||||
<a-spin :size="32" />
|
||||
</div>
|
||||
<!-- 正常显示状态 -->
|
||||
<template v-else-if="imgData">
|
||||
<img :src="imgData" alt="粘贴的图片" class="preview-image img-container" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="placeholder">此处Ctrl+V粘贴图片</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 题注:fontnote -->
|
||||
<a-input v-model="fontnote" class="max-w-100" placeholder="请输入题注"></a-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from "@arco-design/web-vue"
|
||||
import { nextTick, ref } from "vue"
|
||||
|
||||
// 储存图片base64
|
||||
const imgData = defineModel<string>()
|
||||
// 储存题注
|
||||
const fontnote = defineModel<string>("fontnote")
|
||||
// 加载状态
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 处理粘贴事件
|
||||
const handlePaste = async (e: ClipboardEvent) => {
|
||||
e.preventDefault()
|
||||
// 处理没有粘贴内容
|
||||
if (!e.clipboardData) return
|
||||
const items = e.clipboardData!.items
|
||||
// 遍历粘贴板内容
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
// 判断是否是粘贴的图片
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
const file = item.getAsFile()
|
||||
if (!file) {
|
||||
Message.error("读取图片失败,请重新粘贴")
|
||||
break
|
||||
}
|
||||
// 判断大小不超过50M
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
Message.error("要求图片不超过50M")
|
||||
break
|
||||
}
|
||||
isLoading.value = true
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
||||
imgData.value = e.target!.result as string
|
||||
await nextTick() // 保证图片展示
|
||||
isLoading.value = false
|
||||
}
|
||||
// 加载失败处理
|
||||
reader.onerror = () => {
|
||||
Message.error("图片加载失败,请重试")
|
||||
isLoading.value = false
|
||||
}
|
||||
reader.readAsDataURL(file) // 可直接转为base64的url给img元素使用
|
||||
break
|
||||
} else {
|
||||
Message.error("请粘贴图片,无法粘贴文字或其他内容")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.image-input-handle {
|
||||
width: 200px;
|
||||
height: 180px;
|
||||
border: 1px solid #eee;
|
||||
cursor: alias;
|
||||
|
||||
.img-container{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
import { TableColumnData } from "@arco-design/web-vue"
|
||||
import { ref, reactive } from "vue"
|
||||
// 粘贴图片组件
|
||||
import ImageInput from "../ImageInput/index.vue"
|
||||
// wordlike组件
|
||||
import WordLikeTable from "../wordLikeTable/index.vue"
|
||||
|
||||
export default function useTable(reset: Function) {
|
||||
const columns = reactive<TableColumnData[]>([
|
||||
{
|
||||
title: "类型",
|
||||
width: 50,
|
||||
align: "center",
|
||||
dataIndex: "type",
|
||||
render: ({ record }) => {
|
||||
let item: string = "文"
|
||||
let color: string = "red"
|
||||
switch (record.type) {
|
||||
case "text":
|
||||
item = "文"
|
||||
color = "blue"
|
||||
break
|
||||
case "image":
|
||||
item = "图"
|
||||
color = "red"
|
||||
break
|
||||
case "table":
|
||||
item = "表"
|
||||
color = "orangered"
|
||||
break
|
||||
}
|
||||
return <a-tag color={color}>{item}</a-tag>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "内容",
|
||||
dataIndex: "content",
|
||||
slotName: "content",
|
||||
render({ record, rowIndex }) {
|
||||
const { type } = record
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <a-textarea v-model={data.value[rowIndex].content} placeholder="请输入段落" allow-clear />
|
||||
case "image":
|
||||
return <ImageInput v-model={data.value[rowIndex].content} v-model:fontnote={data.value[rowIndex].fontnote}></ImageInput>
|
||||
case "table":
|
||||
return <WordLikeTable v-model={data.value[rowIndex].content} v-model:fontnote={data.value[rowIndex].fontnote}></WordLikeTable>
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
align: "center",
|
||||
width: 80,
|
||||
render: ({ rowIndex }) => {
|
||||
return (
|
||||
<a-space>
|
||||
<a-tooltip content="注意:删除后数据丢失">
|
||||
<a-button type="primary" status="danger" onClick={() => deleteRow(rowIndex)}>
|
||||
{{ icon: () => <icon-delete /> }}
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 卸载时清空数据
|
||||
const handleOnClose = () => {
|
||||
data.value = [{ ...initalRowData }]
|
||||
// 更新展示状态
|
||||
reset()
|
||||
}
|
||||
|
||||
// 数据定义 - 测试
|
||||
const data = ref<
|
||||
{
|
||||
type: string
|
||||
content: string | string[][]
|
||||
fontnote: string
|
||||
}[]
|
||||
>([])
|
||||
|
||||
// 单行初始内容-并设置数据类型
|
||||
const initalRowData = {
|
||||
type: "text",
|
||||
content: "",
|
||||
fontnote: ""
|
||||
}
|
||||
|
||||
// 删除该行
|
||||
const deleteRow = async (rowIndex: number) => {
|
||||
data.value.splice(rowIndex, 1)
|
||||
}
|
||||
|
||||
// 拖拽
|
||||
const handleChange = (_data: typeof data.value) => {
|
||||
data.value = _data
|
||||
}
|
||||
|
||||
// 新增文
|
||||
const addTextRow = () => {
|
||||
data.value.push({ ...initalRowData })
|
||||
}
|
||||
|
||||
// 新增图片
|
||||
const addPicRow = () => {
|
||||
data.value.push({ type: "image", content: "", fontnote: "" })
|
||||
}
|
||||
|
||||
// 新增表格
|
||||
const addTableRow = () => {
|
||||
data.value.push({
|
||||
type: "table",
|
||||
content: [
|
||||
["", "", ""],
|
||||
["", "", ""],
|
||||
["", "", ""]
|
||||
],
|
||||
fontnote: ""
|
||||
})
|
||||
}
|
||||
|
||||
return { columns, data, handleChange, addTextRow, addPicRow, addTableRow, handleOnClose }
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="project-modal-container">
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
width="80%"
|
||||
draggable
|
||||
:on-before-ok="handleSyncOk"
|
||||
unmount-on-close
|
||||
ok-text="确认保存"
|
||||
cancel-text="关闭不保存"
|
||||
:maskClosable="false"
|
||||
@close="handleOnClose"
|
||||
>
|
||||
<template #title>{{ title }}</template>
|
||||
<div class="mb-2">
|
||||
<a-space>
|
||||
<a-dropdown>
|
||||
<a-button type="primary">新增元素<icon-plus /></a-button>
|
||||
<template #content>
|
||||
<a-doption @click="addTextRow">文字</a-doption>
|
||||
<a-doption @click="addPicRow">图片</a-doption>
|
||||
<a-doption @click="addTableRow">表格</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-alert type="warning" style="height: 32px">段落(文)会在word渲染时自动缩进2个字符</a-alert>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-table :columns="columns" :show-header="false" :data="data" @change="handleChange" :draggable="{ type: 'handle', width: 40 }"> </a-table>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, getCurrentInstance } from "vue"
|
||||
import useTable from "./hooks/useTable"
|
||||
import projectApi from "@/api/project/project"
|
||||
import { useRoute } from "vue-router"
|
||||
import { Message } from "@arco-design/web-vue"
|
||||
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { reset } = defineProps<{
|
||||
reset: () => void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const title = ref("")
|
||||
const category = ref("软件概述")
|
||||
|
||||
const { columns, data, handleChange, addTextRow, addPicRow, addTableRow, handleOnClose } = useTable(reset)
|
||||
|
||||
// enums - 维护一个obj来储存同类对应的内容
|
||||
const dictMap = {
|
||||
软件概述: {
|
||||
createTitle: "软件概述-新增",
|
||||
modifyTitle: "软件概述-修改",
|
||||
getFunc: projectApi.getSoftSummary,
|
||||
postFunc: projectApi.postSoftSummary,
|
||||
errorMsg: "获取软件概述失败"
|
||||
},
|
||||
动态环境描述: {
|
||||
createTitle: "动态环境描述-新增",
|
||||
modifyTitle: "动态环境描述-修改",
|
||||
errorMsg: "获取动态环境描述失败",
|
||||
getFunc: projectApi.getDynamicDescription,
|
||||
postFunc: projectApi.postDynamicDescription
|
||||
}
|
||||
}
|
||||
|
||||
// functions and events
|
||||
const handleSyncOk = async () => {
|
||||
try {
|
||||
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_str: string) => {
|
||||
proxy?.$loading?.show("数据加载中...")
|
||||
category.value = category_str
|
||||
const currentCate = dictMap[category.value]
|
||||
try {
|
||||
const res = await currentCate.getFunc(route.query.id)
|
||||
const code = res.code // 25001表示有数据,25002表示没有数据
|
||||
title.value = code === 25001 ? currentCate.modifyTitle : currentCate.createTitle
|
||||
data.value = res.data
|
||||
visible.value = true
|
||||
} catch (e) {
|
||||
Message.error(currentCate.errorMsg)
|
||||
} finally {
|
||||
proxy?.$loading?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
defineOptions({
|
||||
name: "ProjectModal"
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<!-- 完成自定义表格 -->
|
||||
<div class="fontnote">
|
||||
<a-space class="w-full">
|
||||
<span>题注:</span>
|
||||
<a-input v-model="fontnote" :style="{ width: '500px' }"></a-input>
|
||||
</a-space>
|
||||
</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" v-for="(_, colIndex) in datas![0]" :key="colIndex">
|
||||
<span class="arco-table-cell items-center justify-center">
|
||||
<a-tooltip content="此列后新增列">
|
||||
<a-button type="text" size="mini" @click="addColumn(colIndex)" class="delete-col-btn">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="删除该列">
|
||||
<a-button
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger"
|
||||
@click="deleteColumn(colIndex)"
|
||||
:disabled="datas![0].length <= 1"
|
||||
class="delete-col-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-close />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</th>
|
||||
<th class="arco-table-th" :style="{ textAlign: 'center' }">
|
||||
<span>操作</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="arco-table-tr" v-for="(row, rowIndex) in datas" :key="rowIndex">
|
||||
<td class="arco-table-td" v-for="(col, colIndex) in row" :key="colIndex">
|
||||
<span class="arco-table-cell">
|
||||
<a-textarea auto-size v-model="datas![rowIndex][colIndex]" />
|
||||
</span>
|
||||
</td>
|
||||
<td class="arco-table-td">
|
||||
<span class="arco-table-cell items-center justify-center">
|
||||
<a-tooltip content="此行后新增行">
|
||||
<a-button type="text" size="mini" @click="addRow(rowIndex)" class="delete-col-btn">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="删除该行">
|
||||
<a-button size="mini" type="text" status="danger" @click="deleteRow(rowIndex)" :disabled="datas!.length <= 1">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 该组件储存数据
|
||||
const fontnote = defineModel<string>("fontnote")
|
||||
|
||||
const datas = defineModel<string[][]>()
|
||||
|
||||
// 行列操作
|
||||
const deleteRow = (rowIndex: number) => {
|
||||
datas.value!.splice(rowIndex, 1)
|
||||
}
|
||||
const deleteColumn = (colIndex: number) => {
|
||||
datas.value!.forEach((row) => {
|
||||
row.splice(colIndex, 1)
|
||||
})
|
||||
}
|
||||
const addRow = (rowIndex: number) => {
|
||||
const newRow = new Array(datas.value![0].length).fill("")
|
||||
datas.value!.splice(rowIndex + 1, 0, newRow)
|
||||
}
|
||||
const addColumn = (colIndex: number) => {
|
||||
// 处理空表格的特殊情况
|
||||
if (datas.value!.length === 0) {
|
||||
datas.value!.push([""])
|
||||
return
|
||||
}
|
||||
// 新增后续列
|
||||
datas.value!.forEach((row) => {
|
||||
const insertPosition = colIndex === -1 ? row.length : colIndex + 1
|
||||
row.splice(insertPosition, 0, "")
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.fontnote {
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.arco-textarea {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.arco-table-cell {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,6 @@ export default function useSearchNodes() {
|
||||
const searchKey = ref("")
|
||||
// 点击搜索事件
|
||||
const handleSearchTreeDataClick = () => {
|
||||
console.log(searchKey.value)
|
||||
// 返回过滤后的treeData
|
||||
// treeDataStore.originTreeData
|
||||
if (searchKey.value) {
|
||||
|
||||
@@ -25,6 +25,10 @@ app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(globalComponents)
|
||||
|
||||
// 全局loading插件
|
||||
import loadingPlugin from "./plugins/loading"
|
||||
app.use(loadingPlugin)
|
||||
|
||||
// 使用服务端请求数据管理库
|
||||
import { VueQueryPlugin } from "@tanstack/vue-query"
|
||||
app.use(VueQueryPlugin)
|
||||
|
||||
16
cdTMP/src/plugins/loading.js
Normal file
16
cdTMP/src/plugins/loading.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createVNode, render } from "vue"
|
||||
import LoadingComponent from "@/components/GlobalLoading/index.vue"
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
// 创建虚拟节点并渲染
|
||||
const vnode = createVNode(LoadingComponent)
|
||||
render(vnode, document.body)
|
||||
|
||||
// 挂载到全局属性
|
||||
app.config.globalProperties.$loading = {
|
||||
show: (text) => vnode.component?.exposed?.show(text),
|
||||
hide: () => vnode.component?.exposed?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
0
cdTMP/xxx.dio
Normal file
0
cdTMP/xxx.dio
Normal file
@@ -1 +1,2 @@
|
||||
1. tinymce 7.9.1
|
||||
1. tinymce 7.9.1
|
||||
2. vue-router 4.6.4
|
||||
Reference in New Issue
Block a user