Compare commits

13 Commits
v0.1.1 ... main

38 changed files with 3425 additions and 6933 deletions

View File

@@ -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. - web管理测试过程在此平台写用例等内容
## 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).

2955
cdTMP/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "testplant", "name": "testplant",
"private": true, "private": true,
"version": "0.1.0", "version": "1.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -13,53 +13,53 @@
"dependencies": { "dependencies": {
"@arco-design/color": "^0.4.0", "@arco-design/color": "^0.4.0",
"@arco-design/web-vue": "^2.57.0", "@arco-design/web-vue": "^2.57.0",
"@tanstack/vue-query": "^5.92.9", "@tanstack/vue-query": "^5.99.0",
"@tinymce/tinymce-vue": "^6.3.0", "@tinymce/tinymce-vue": "^6.3.0",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.2.1",
"axios": "^1.13.4", "axios": "^1.15.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.20",
"file2md5": "^1.3.0", "file2md5": "^1.3.0",
"lodash-es": "^4.17.23", "lodash-es": "^4.18.1",
"mammoth": "^1.11.0", "mammoth": "^1.12.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinyin-match": "^1.2.10", "pinyin-match": "^1.2.10",
"postcss-import": "^16.1.1", "postcss-import": "^16.1.1",
"qs": "^6.14.1", "qs": "^6.15.1",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.5.0",
"tinymce": "^7.9.1", "tinymce": "^7.9.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vue": "^3.5.27", "vue": "^3.5.32",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-color-kit": "^1.0.6", "vue-color-kit": "^1.0.6",
"vue-data-ui": "^3.13.4", "vue-data-ui": "^3.17.13",
"vue-router": "^4.6.4", "vue-router": "^5.0.4",
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.2.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.10", "@types/node": "^25.6.0",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qs": "^6.14.0", "@types/qs": "^6.15.0",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.6",
"@vitejs/plugin-vue-jsx": "^5.1.3", "@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/babel-plugin-jsx": "^2.0.1", "@vue/babel-plugin-jsx": "^2.0.1",
"browserslist": "^4.28.1", "browserslist": "^4.28.2",
"eslint": "^9.39.2", "eslint": "^10.2.0",
"eslint-plugin-vue": "^10.7.0", "eslint-plugin-vue": "^10.8.0",
"less": "^4.5.1", "less": "^4.6.4",
"less-loader": "^12.3.0", "less-loader": "^12.3.2",
"postcss": "^8.5.6", "postcss": "^8.5.10",
"prettier": "^3.8.1", "prettier": "^3.8.3",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^7.0.1",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.2.2",
"typescript": "^5.9.3", "typescript": "^6.0.2",
"vite": "^7.3.1", "vite": "^8.0.8",
"vue-eslint-parser": "^10.2.0" "vue-eslint-parser": "^10.4.0"
} }
} }

View File

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

View File

@@ -110,5 +110,127 @@ export default {
id: projectId 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 }
})
} }
} }

View File

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

View File

@@ -0,0 +1,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>

View File

@@ -8,9 +8,8 @@
import { reactive, ref, watch, computed } from "vue" import { reactive, ref, watch, computed } from "vue"
import { useAppStore } from "@/store" import { useAppStore } from "@/store"
import Editor from "@tinymce/tinymce-vue"
import tinymce from "tinymce/tinymce.min.js" import tinymce from "tinymce/tinymce.min.js"
import Editor from "@tinymce/tinymce-vue"
import "tinymce/icons/default/icons.min.js" import "tinymce/icons/default/icons.min.js"
import "tinymce/models/dom/model.min.js" import "tinymce/models/dom/model.min.js"
import "tinymce/themes/silver/theme.min.js" import "tinymce/themes/silver/theme.min.js"

View File

@@ -1,139 +1,135 @@
<template> <template>
<div <div ref="canvasContainerRef" :class="$props.class" aria-hidden="true">
ref="canvasContainerRef"
:class="$props.class"
aria-hidden="true"
>
<canvas ref="canvasRef"></canvas> <canvas ref="canvasRef"></canvas>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMouse, useDevicePixelRatio } from '@vueuse/core'; import { useMouse, useDevicePixelRatio } from "@vueuse/core"
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue'; import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from "vue"
type Circle = { type Circle = {
x: number; x: number
y: number; y: number
translateX: number; translateX: number
translateY: number; translateY: number
size: number; size: number
alpha: number; alpha: number
targetAlpha: number; targetAlpha: number
dx: number; dx: number
dy: number; dy: number
magnetism: number; magnetism: number
}; }
type Props = { type Props = {
color?: string; color?: string
quantity?: number; quantity?: number
staticity?: number; staticity?: number
ease?: number; ease?: number
class?: string; class?: string
}; }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
color: '#FFF', color: "#FFF",
quantity: 100, quantity: 100,
staticity: 50, staticity: 50,
ease: 50, ease: 50,
class: '', class: ""
}); })
const canvasRef = ref<HTMLCanvasElement | null>(null); const canvasRef = ref<HTMLCanvasElement | null>(null)
const canvasContainerRef = ref<HTMLDivElement | null>(null); const canvasContainerRef = ref<HTMLDivElement | null>(null)
const context = ref<CanvasRenderingContext2D | null>(null); const context = ref<CanvasRenderingContext2D | null>(null)
const circles = ref<Circle[]>([]); const circles = ref<Circle[]>([])
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 }); const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 }); const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
const { x: mouseX, y: mouseY } = useMouse(); const { x: mouseX, y: mouseY } = useMouse()
const { pixelRatio } = useDevicePixelRatio(); const { pixelRatio } = useDevicePixelRatio()
const color = computed(() => { const color = computed(() => {
// Remove the leading '#' if it's present // Remove the leading '#' if it's present
let hex = props.color.replace(/^#/, ''); let hex = props.color.replace(/^#/, "")
// If the hex code is 3 characters, expand it to 6 characters // If the hex code is 3 characters, expand it to 6 characters
if (hex.length === 3) { if (hex.length === 3) {
hex = hex hex = hex
.split('') .split("")
.map((char) => char + char) .map((char) => char + char)
.join(''); .join("")
} }
// Parse the r, g, b values from the hex string // Parse the r, g, b values from the hex string
const bigint = parseInt(hex, 16); const bigint = parseInt(hex, 16)
const r = (bigint >> 16) & 255; // Extract the red component const r = (bigint >> 16) & 255 // Extract the red component
const g = (bigint >> 8) & 255; // Extract the green component const g = (bigint >> 8) & 255 // Extract the green component
const b = bigint & 255; // Extract the blue component const b = bigint & 255 // Extract the blue component
// Return the RGB values as a string separated by spaces // Return the RGB values as a string separated by spaces
return `${r} ${g} ${b}`; return `${r} ${g} ${b}`
}); })
onMounted(() => { onMounted(() => {
if (canvasRef.value) { if (canvasRef.value) {
context.value = canvasRef.value.getContext('2d'); context.value = canvasRef.value.getContext("2d")
} }
initCanvas(); initCanvas()
animate(); animate()
window.addEventListener('resize', initCanvas); window.addEventListener("resize", initCanvas)
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', initCanvas); window.removeEventListener("resize", initCanvas)
}); })
watch([mouseX, mouseY], () => { watch([mouseX, mouseY], () => {
onMouseMove(); onMouseMove()
}); })
function initCanvas() { function initCanvas() {
resizeCanvas(); resizeCanvas()
drawParticles(); drawParticles()
} }
function onMouseMove() { function onMouseMove() {
if (canvasRef.value) { if (canvasRef.value) {
const rect = canvasRef.value.getBoundingClientRect(); const rect = canvasRef.value.getBoundingClientRect()
const { w, h } = canvasSize; const { w, h } = canvasSize
const x = mouseX.value - rect.left - w / 2; const x = mouseX.value - rect.left - w / 2
const y = mouseY.value - rect.top - h / 2; const y = mouseY.value - rect.top - h / 2
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
if (inside) { if (inside) {
mouse.x = x; mouse.x = x
mouse.y = y; mouse.y = y
} }
} }
} }
function resizeCanvas() { function resizeCanvas() {
if (canvasContainerRef.value && canvasRef.value && context.value) { if (canvasContainerRef.value && canvasRef.value && context.value) {
circles.value.length = 0; circles.value.length = 0
canvasSize.w = canvasContainerRef.value.offsetWidth; canvasSize.w = canvasContainerRef.value.offsetWidth
canvasSize.h = canvasContainerRef.value.offsetHeight; canvasSize.h = canvasContainerRef.value.offsetHeight
canvasRef.value.width = canvasSize.w * pixelRatio.value; canvasRef.value.width = canvasSize.w * pixelRatio.value
canvasRef.value.height = canvasSize.h * pixelRatio.value; canvasRef.value.height = canvasSize.h * pixelRatio.value
canvasRef.value.style.width = canvasSize.w + 'px'; canvasRef.value.style.width = canvasSize.w + "px"
canvasRef.value.style.height = canvasSize.h + 'px'; canvasRef.value.style.height = canvasSize.h + "px"
context.value.scale(pixelRatio.value, pixelRatio.value); context.value.scale(pixelRatio.value, pixelRatio.value)
} }
} }
function circleParams(): Circle { function circleParams(): Circle {
const x = Math.floor(Math.random() * canvasSize.w); const x = Math.floor(Math.random() * canvasSize.w)
const y = Math.floor(Math.random() * canvasSize.h); const y = Math.floor(Math.random() * canvasSize.h)
const translateX = 0; const translateX = 0
const translateY = 0; const translateY = 0
const size = Math.floor(Math.random() * 2) + 1; const size = Math.floor(Math.random() * 2) + 1
const alpha = 0; const alpha = 0
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
const dx = (Math.random() - 0.5) * 0.2; const dx = (Math.random() - 0.5) * 0.2
const dy = (Math.random() - 0.5) * 0.2; const dy = (Math.random() - 0.5) * 0.2
const magnetism = 0.1 + Math.random() * 4; const magnetism = 0.1 + Math.random() * 4
return { return {
x, x,
y, y,
@@ -144,92 +140,79 @@ function circleParams(): Circle {
targetAlpha, targetAlpha,
dx, dx,
dy, dy,
magnetism, magnetism
}; }
} }
function drawCircle(circle: Circle, update = false) { function drawCircle(circle: Circle, update = false) {
if (context.value) { if (context.value) {
const { x, y, translateX, translateY, size, alpha } = circle; const { x, y, translateX, translateY, size, alpha } = circle
context.value.translate(translateX, translateY); context.value.translate(translateX, translateY)
context.value.beginPath(); context.value.beginPath()
context.value.arc(x, y, size, 0, 2 * Math.PI); context.value.arc(x, y, size, 0, 2 * Math.PI)
context.value.fillStyle = `rgba(${color.value.split(' ').join(', ')}, ${alpha})`; context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`
context.value.fill(); context.value.fill()
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0); context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0)
if (!update) { if (!update) {
circles.value.push(circle); circles.value.push(circle)
} }
} }
} }
function clearContext() { function clearContext() {
if (context.value) { if (context.value) {
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h); context.value.clearRect(0, 0, canvasSize.w, canvasSize.h)
} }
} }
function drawParticles() { function drawParticles() {
clearContext(); clearContext()
const particleCount = props.quantity; const particleCount = props.quantity
for (let i = 0; i < particleCount; i++) { for (let i = 0; i < particleCount; i++) {
const circle = circleParams(); const circle = circleParams()
drawCircle(circle); drawCircle(circle)
} }
} }
function remapValue( function remapValue(value: number, start1: number, end1: number, start2: number, end2: number): number {
value: number, const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
start1: number, return remapped > 0 ? remapped : 0
end1: number,
start2: number,
end2: number,
): number {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
} }
function animate() { function animate() {
clearContext(); clearContext()
circles.value.forEach((circle, i) => { circles.value.forEach((circle, i) => {
// Handle the alpha value // Handle the alpha value
const edge = [ const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge canvasSize.h - circle.y - circle.translateY - circle.size // distance from bottom edge
]; ]
const closestEdge = edge.reduce((a, b) => Math.min(a, b)); const closestEdge = edge.reduce((a, b) => Math.min(a, b))
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2)); const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
if (remapClosestEdge > 1) { if (remapClosestEdge > 1) {
circle.alpha += 0.02; circle.alpha += 0.02
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha; if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
} else { } else {
circle.alpha = circle.targetAlpha * remapClosestEdge; circle.alpha = circle.targetAlpha * remapClosestEdge
} }
circle.x += circle.dx; circle.x += circle.dx
circle.y += circle.dy; circle.y += circle.dy
circle.translateX += circle.translateX += (mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease
(mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease; circle.translateY += (mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease
circle.translateY +=
(mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease;
// circle gets out of the canvas // circle gets out of the canvas
if ( if (circle.x < -circle.size || circle.x > canvasSize.w + circle.size || circle.y < -circle.size || circle.y > canvasSize.h + circle.size) {
circle.x < -circle.size ||
circle.x > canvasSize.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.h + circle.size
) {
// remove the circle from the array // remove the circle from the array
circles.value.splice(i, 1); circles.value.splice(i, 1)
// create a new circle // create a new circle
const newCircle = circleParams(); const newCircle = circleParams()
drawCircle(newCircle); drawCircle(newCircle)
// update the circle position // update the circle position
} else { } else {
drawCircle( drawCircle(
@@ -239,12 +222,12 @@ function animate() {
y: circle.y, y: circle.y,
translateX: circle.translateX, translateX: circle.translateX,
translateY: circle.translateY, translateY: circle.translateY,
alpha: circle.alpha, alpha: circle.alpha
}, },
true, true
); )
} }
}); })
window.requestAnimationFrame(animate); window.requestAnimationFrame(animate)
} }
</script> </script>

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,31 +5,18 @@
<div class="logo-container"> <div class="logo-container">
<img src="@/assets/img/wxwx-logo.svg" class="logo" alt="logo" @click="handleClickLogo" /> <img src="@/assets/img/wxwx-logo.svg" class="logo" alt="logo" @click="handleClickLogo" />
</div> </div>
<a-typography-title <a-typography-title class="title" @click="handleClickLogo" :style="{ margin: 0, fontSize: '18px' }" :heading="5">
class="title" <div class="font-extrabold bg-clip-text text-transparent bg-linear-to-r from-blue-500 to-purple-600">测试管理平台</div>
@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>
<a-typography-title :heading="6" class="version">v{{ $version }}</a-typography-title> <a-typography-title :heading="6" class="version">v{{ $version }}</a-typography-title>
<icon-menu-fold <icon-menu-fold v-if="!topMenu && appStore.device === 'mobile'" style="font-size: 22px; cursor: pointer" @click="toggleDrawerMenu" />
v-if="!topMenu && appStore.device === 'mobile'"
style="font-size: 22px; cursor: pointer"
@click="toggleDrawerMenu"
/>
</a-space> </a-space>
</div> </div>
<div class="center-side flex items-center justify-center font-bold text-lg"> <div class="center-side flex items-center justify-center font-bold text-lg">
<template v-if="title"> <template v-if="title">
<a-typography-title <a-typography-title
:style="{ margin: 0, fontSize: '1.1rem', fontWeight: 'bold' }" :style="{ margin: 0, fontSize: '1.2rem', fontWeight: 'bold' }"
:heading="4" :heading="3"
:ellipsis="{ :ellipsis="{
rows: 2 rows: 2
}" }"
@@ -102,6 +89,9 @@
</a-dropdown> </a-dropdown>
</li> </li>
</ul> </ul>
<div class="fix-side" v-if="route.query.id">
<project-info-other />
</div>
</div> </div>
</template> </template>
@@ -114,6 +104,9 @@ import useUser from "@/hooks/logout"
import { Message } from "@arco-design/web-vue" import { Message } from "@arco-design/web-vue"
import Menu from "@/layout/components/menu.vue" import Menu from "@/layout/components/menu.vue"
import { useRouter, useRoute } from "vue-router" import { useRouter, useRoute } from "vue-router"
// 项目信息录入组件
import projectInfoOther from "./projectInfoOther/index.vue"
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const appStore = useAppStore() const appStore = useAppStore()
@@ -165,6 +158,14 @@ const handleClickLogo = () => {
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
// 项目管理悬浮定位
.fix-side {
position: absolute;
left: 16%;
top: 50%;
transform: translateY(-50%);
}
.logo-container { .logo-container {
perspective: 1000px; perspective: 1000px;
.logo { .logo {

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }
}

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View File

@@ -34,7 +34,6 @@ export default function useSearchNodes() {
const searchKey = ref("") const searchKey = ref("")
// 点击搜索事件 // 点击搜索事件
const handleSearchTreeDataClick = () => { const handleSearchTreeDataClick = () => {
console.log(searchKey.value)
// 返回过滤后的treeData // 返回过滤后的treeData
// treeDataStore.originTreeData // treeDataStore.originTreeData
if (searchKey.value) { if (searchKey.value) {

View File

@@ -25,6 +25,10 @@ app.use(router)
app.use(pinia) app.use(pinia)
app.use(globalComponents) app.use(globalComponents)
// 全局loading插件
import loadingPlugin from "./plugins/loading"
app.use(loadingPlugin)
// 使用服务端请求数据管理库 // 使用服务端请求数据管理库
import { VueQueryPlugin } from "@tanstack/vue-query" import { VueQueryPlugin } from "@tanstack/vue-query"
app.use(VueQueryPlugin) app.use(VueQueryPlugin)

View 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()
}
}
}

View File

@@ -5,6 +5,8 @@ import setupPermissionGuard from "@/router/guard/permisstion"
import { setRouteTitle } from "@/utils/title" import { setRouteTitle } from "@/utils/title"
// 为了已登录用户直接进入login // 为了已登录用户直接进入login
import { useUserStore } from "@/store" import { useUserStore } from "@/store"
import NProgress from "nprogress" // progress bar
import "nprogress/nprogress.css"
function setupPageGuard(router) { function setupPageGuard(router) {
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
@@ -14,6 +16,7 @@ function setupPageGuard(router) {
// 设置站点document.title // 设置站点document.title
router.afterEach((to, from) => { router.afterEach((to, from) => {
setRouteTitle(to.meta.title) setRouteTitle(to.meta.title)
NProgress.done()
}) })
// 设置如果已登录用户进入login页面则直接进入工作台 // 设置如果已登录用户进入login页面则直接进入工作台
router.beforeEach((to) => { router.beforeEach((to) => {

View File

@@ -7,7 +7,7 @@ import { appRoutes } from "../routes"
import { WHITE_LIST, NOT_FOUND } from "../constants" import { WHITE_LIST, NOT_FOUND } from "../constants"
// 权限守卫 // 权限守卫
export default function setupPermissionGuard(router) { export default function setupPermissionGuard(router) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from) => {
const appStore = useAppStore() const appStore = useAppStore()
const userStore = useUserStore() const userStore = useUserStore()
const Permission = usePermission() const Permission = usePermission()
@@ -34,16 +34,18 @@ export default function setupPermissionGuard(router) {
} }
} }
if (exist && permissionsAllow) { if (exist && permissionsAllow) {
next() return true
} else next(NOT_FOUND) } else {
return NOT_FOUND
}
} else { } else {
// eslint-disable-next-line no-lonely-if // eslint-disable-next-line no-lonely-if
if (permissionsAllow) next() if (permissionsAllow) {
else { return true
} else {
const destination = Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND const destination = Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND
next(destination) return destination
} }
} }
NProgress.done()
}) })
} }

View File

@@ -2,12 +2,10 @@ import { Ref, computed } from "vue"
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia"
import { useAppStore } from "@/store" import { useAppStore } from "@/store"
// 单个每月项目数量对象格式
interface IData { interface IData {
mouth: string mouth: string
count: number count: number
} }
// 响应.data的数据格式
interface ResData { interface ResData {
data: IData[] data: IData[]
} }
@@ -15,7 +13,8 @@ interface ResData {
function useVueDataUI(data: Ref<ResData>) { function useVueDataUI(data: Ref<ResData>) {
const appStore = useAppStore() const appStore = useAppStore()
const { theme } = storeToRefs(appStore) const { theme } = storeToRefs(appStore)
// 结构pinia储存的主体响应式变量
// 基础数据集
const initialData = [ const initialData = [
{ {
name: "项目数量", name: "项目数量",
@@ -27,36 +26,38 @@ function useVueDataUI(data: Ref<ResData>) {
smooth: true smooth: true
} }
] ]
const chartData = computed<any[]>(() => {
const chartData = computed(() => {
if (data.value) { if (data.value) {
const countData = data.value.data.map((it) => it.count) const countData = data.value.data.map((it) => it.count)
initialData[0].series = countData // 注意:这里直接修改了 initialData[0].series为避免副作用建议每次返回新对象
return [
{
...initialData[0],
series: countData
}
]
} }
return initialData return initialData
}) })
// 暗黑模式识别(这是存在pinia的)
const darkMode = document.body.getAttribute("arco-theme") // 基础配置(不包含颜色相关部分)
const initialConfig = { const baseConfig = {
theme: darkMode === "dark" ? "celebrationNight" : "",
responsive: false, responsive: false,
customPalette: [], customPalette: [],
downsample: { threshold: 500 }, downsample: { threshold: 500 },
chart: { chart: {
fontFamily: "inherit", fontFamily: "inherit",
backgroundColor: "#FFFFFFff",
color: "#1A1A1Aff",
height: 300, height: 300,
width: 1200, width: 1200,
padding: { top: 36, right: 24, bottom: 48, left: 48 }, padding: { top: 36, right: 24, bottom: 48, left: 48 },
highlighter: { color: "#1A1A1Aff", opacity: 5, useLine: false, lineDasharray: 2, lineWidth: 1 }, highlighter: { opacity: 5, useLine: false, lineDasharray: 2, lineWidth: 1 },
grid: { grid: {
stroke: "#e1e5e8ff",
showVerticalLines: false, showVerticalLines: false,
showHorizontalLines: false, showHorizontalLines: false,
position: "middle", position: "middle",
frame: { frame: {
show: false, show: false,
stroke: "#E1E5E8ff",
strokeWidth: 2, strokeWidth: 2,
strokeLinecap: "round", strokeLinecap: "round",
strokeLinejoin: "round", strokeLinejoin: "round",
@@ -64,7 +65,6 @@ function useVueDataUI(data: Ref<ResData>) {
}, },
labels: { labels: {
show: true, show: true,
color: "#1A1A1Aff",
fontSize: 12, fontSize: 12,
axis: { yLabel: "", yLabelOffsetX: 0, xLabel: "", xLabelOffsetY: 14, fontSize: 12 }, axis: { yLabel: "", yLabelOffsetX: 0, xLabel: "", xLabelOffsetY: 14, fontSize: 12 },
zeroLine: { show: true }, zeroLine: { show: true },
@@ -81,22 +81,8 @@ function useVueDataUI(data: Ref<ResData>) {
scaleMax: 20 scaleMax: 20
}, },
xAxisLabels: { xAxisLabels: {
color: "#1A1A1Aff",
show: true, show: true,
values: [ values: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月"
],
fontSize: 12, fontSize: 12,
showOnlyFirstAndLast: false, showOnlyFirstAndLast: false,
showOnlyAtModulo: false, showOnlyAtModulo: false,
@@ -108,26 +94,22 @@ function useVueDataUI(data: Ref<ResData>) {
}, },
comments: { show: true, showInTooltip: true, width: 200, offsetX: 0, offsetY: 0 }, comments: { show: true, showInTooltip: true, width: 200, offsetX: 0, offsetY: 0 },
labels: { fontSize: 10, prefix: "", suffix: "" }, labels: { fontSize: 10, prefix: "", suffix: "" },
legend: { color: "#1A1A1Aff", show: false, fontSize: 16 }, legend: { show: false, fontSize: 16 },
title: { title: {
text: "项目每月统计", text: "项目每月统计",
color: "#1A1A1Aff",
fontSize: 18, fontSize: 18,
bold: true, bold: true,
textAlign: "left", textAlign: "left",
paddingLeft: 5, paddingLeft: 5,
paddingRight: 0, paddingRight: 0,
subtitle: { color: "#CCCCCCff", text: "", fontSize: 16, bold: false }, subtitle: { text: "", fontSize: 16, bold: false },
show: true show: true
}, },
tooltip: { tooltip: {
show: true, show: true,
color: "#1A1A1Aff",
backgroundColor: "#FFFFFFff",
fontSize: 14, fontSize: 14,
customFormat: null, customFormat: null,
borderRadius: 4, borderRadius: 4,
borderColor: "#e1e5e8",
borderWidth: 1, borderWidth: 1,
backgroundOpacity: 30, backgroundOpacity: 30,
position: "center", position: "center",
@@ -170,16 +152,73 @@ function useVueDataUI(data: Ref<ResData>) {
}, },
showTable: false showTable: false
} }
const chartConfig = computed(() => { const chartConfig = computed(() => {
const isDark = theme.value === "dark"
// 计算 Y 轴最大值
let scaleMax = 10
if (data.value) { if (data.value) {
const countData = data.value.data.map((it) => it.count) const countData = data.value.data.map((it) => it.count)
initialConfig.chart.grid.labels.yAxis.scaleMax = Math.max(...countData) ? Math.max(...countData) : 10 scaleMax = Math.max(...countData) || 10
} }
// 动态生成颜色相关配置
return { return {
...initialConfig, ...baseConfig,
theme: theme.value === "dark" ? "celebrationNight" : "" // 不设置 theme完全自定义
chart: {
...baseConfig.chart,
// 背景色
backgroundColor: isDark ? "#1A1A1Aff" : "#FFFFFFff",
// 全局文字颜色
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff",
highlighter: {
...baseConfig.chart.highlighter,
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff"
},
grid: {
...baseConfig.chart.grid,
stroke: isDark ? "#444444ff" : "#e1e5e8ff",
frame: {
...baseConfig.chart.grid.frame,
stroke: isDark ? "#444444ff" : "#E1E5E8ff"
},
labels: {
...baseConfig.chart.grid.labels,
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff",
yAxis: {
...baseConfig.chart.grid.labels.yAxis,
scaleMax
},
xAxisLabels: {
...baseConfig.chart.grid.labels.xAxisLabels,
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff"
}
}
},
legend: {
...baseConfig.chart.legend,
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff"
},
title: {
...baseConfig.chart.title,
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff",
subtitle: {
...baseConfig.chart.title.subtitle,
color: isDark ? "#AAAAAAff" : "#CCCCCCff"
}
},
tooltip: {
...baseConfig.chart.tooltip,
color: isDark ? "#FFFFFFFF" : "#1A1A1Aff",
backgroundColor: isDark ? "#333333ff" : "#FFFFFFff",
borderColor: isDark ? "#555555ff" : "#e1e5e8"
}
}
} }
}) })
return { chartData, chartConfig } return { chartData, chartConfig }
} }

View File

@@ -3,7 +3,7 @@
<a-spin class="chartContainer" :loading="isPending" tip="图标数据加载中..."> <a-spin class="chartContainer" :loading="isPending" tip="图标数据加载中...">
<div class="flex justify-center items-center" v-if="!isPending"> <div class="flex justify-center items-center" v-if="!isPending">
<template v-if="isError"> <template v-if="isError">
<img class="w-[200px] h-[300px]" src="@/assets/img/ErrorLoad.svg" alt="" /> <img class="w-50 h-75" src="@/assets/img/ErrorLoad.svg" alt="" />
</template> </template>
<template v-else> <template v-else>
<VueUiXy :dataset="chartData" :config="chartConfig" :style="{ padding: '10px' }" /> <VueUiXy :dataset="chartData" :config="chartConfig" :style="{ padding: '10px' }" />
@@ -28,6 +28,8 @@ const { isPending, data, isError } = useQuery({
// vue-data-ui图表 // vue-data-ui图表
const { chartData, chartConfig } = useVueDataUI(data) const { chartData, chartConfig } = useVueDataUI(data)
// 暗黑模式配置
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@ export default ({ mode }) => {
vueJsx(), vueJsx(),
visualizer({ visualizer({
open: true, open: true,
filename: "visualizer.html" // 分析图生成的文件名 filename: "./dist/visualizer.html" // 分析图生成的文件名
}), }),
tailwindcss() tailwindcss()
], ],
@@ -33,8 +33,25 @@ export default ({ mode }) => {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
}, },
build: { build: {
chunkSizeWarningLimit: 3000, chunkSizeWarningLimit: 3000
// assetsPublicPath: "./" // assetsPublicPath: "./"
// v8版本又报tinymce is not defined只有遗憾业务js大的问题
/**
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
test: (id) => id.includes("tinymce"),
name: "tinymce",
}
]
}
}
}
*/
// vite v7版本配置V8已经移除
/**
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: (id) => { manualChunks: (id) => {
@@ -43,6 +60,7 @@ export default ({ mode }) => {
} }
} }
} }
*/
}, },
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",

0
cdTMP/xxx.dio Normal file
View File

View File

@@ -1 +1,2 @@
1. tinymce 7.9.1 1. tinymce 7.9.1
2. vite 7.3.1 -> 8.0.0使用分块打包tinymce会找不到模块暂时取消分块