Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
V
Vue-Dashboard
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
高源
Vue-Dashboard
Commits
54b22571
Commit
54b22571
authored
Jun 20, 2025
by
Neo Turing
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完善比对测试功能:更新TestDataItem接口、实现点击编辑模式、优化删除按钮样式
parent
8cbb38c2
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
1160 additions
and
387 deletions
+1160
-387
components.d.ts
src/typings/components.d.ts
+1
-0
TestPlanDetailSelector.vue
src/views/Lac/codes/TestPlanDetailSelector.vue
+272
-146
comparisonTest.vue
src/views/Lac/codes/comparisonTest.vue
+873
-230
fileListView.vue
src/views/Lac/codes/fileListView.vue
+0
-1
fileReview.vue
src/views/Lac/codes/fileReview.vue
+12
-7
mainSupplier.vue
src/views/Lac/codes/mainSupplier.vue
+2
-3
No files found.
src/typings/components.d.ts
View file @
54b22571
...
...
@@ -78,6 +78,7 @@ declare module 'vue' {
NTag
:
typeof
import
(
'naive-ui'
)[
'NTag'
]
NText
:
typeof
import
(
'naive-ui'
)[
'NText'
]
NTooltip
:
typeof
import
(
'naive-ui'
)[
'NTooltip'
]
NTree
:
typeof
import
(
'naive-ui'
)[
'NTree'
]
NUpload
:
typeof
import
(
'naive-ui'
)[
'NUpload'
]
NWatermark
:
typeof
import
(
'naive-ui'
)[
'NWatermark'
]
PinToggler
:
typeof
import
(
'./../components/common/pin-toggler.vue'
)[
'default'
]
...
...
src/views/Lac/codes/TestPlanDetailSelector.vue
View file @
54b22571
<
script
setup
lang=
"ts"
>
import
{
computed
,
defineEmits
,
defineProps
,
onMounted
,
ref
,
watch
}
from
'vue'
;
import
{
computed
,
defineEmits
,
defineProps
,
h
,
onMounted
,
ref
,
watch
}
from
'vue'
;
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
const
axios
=
(
window
as
any
).
$axios
;
// 获取主题状态
...
...
@@ -22,11 +22,14 @@ const cssVars = computed(() => ({
'--border-color'
:
isDarkMode
.
value
?
'#434343'
:
'#e8e8e8'
}));
interface
PlanDetailItem
{
id
:
number
;
testItem
:
string
;
standardValue
:
string
;
allowableRange
:
string
;
interface
CustomTreeOption
{
key
:
string
|
number
;
label
:
string
;
children
?:
CustomTreeOption
[];
origin
?:
any
;
// 保存原始API数据
isLeaf
?:
boolean
;
disabled
?:
boolean
;
[
key
:
string
]:
any
;
// 添加索引签名以兼容 Naive UI
}
const
props
=
defineProps
({
...
...
@@ -38,10 +41,12 @@ const props = defineProps({
const
emit
=
defineEmits
([
'cancel'
,
'confirm'
]);
// 当前可用的方案明细数据
const
planDetails
=
ref
<
PlanDetailItem
[]
>
([]);
// 已选中的行键数组
const
checkedRowKeys
=
ref
<
(
string
|
number
)[]
>
([]);
// 树形数据
const
treeData
=
ref
<
CustomTreeOption
[]
>
([]);
// 已选中的节点键数组
const
checkedKeys
=
ref
<
(
string
|
number
)[]
>
([]);
// 展开的节点键数组
const
expandedKeys
=
ref
<
(
string
|
number
)[]
>
([]);
// 加载状态
const
loading
=
ref
<
boolean
>
(
false
);
...
...
@@ -51,15 +56,33 @@ async function fetchTestDetails() {
try
{
loading
.
value
=
true
;
const
response
=
await
axios
.
post
(
'/Restful/Kivii.Standards.Entities.Detection/Query.json'
,
{
StandardKvid
:
props
.
standardKvid
});
const
queryParams
:
any
=
{
StandardKvid
:
props
.
standardKvid
,
OrderBy
:
'SortId'
};
const
response
=
await
axios
.
post
(
'/Restful/Kivii.Standards.Entities.DetectionEx/DetectionWithPriceQuery.json'
,
queryParams
);
if
(
response
.
data
&&
response
.
data
.
Results
)
{
// 处理树形结构数据
const
treeData
=
processTreeData
(
response
.
data
.
Results
);
planDetails
.
value
=
treeData
;
const
treeStructure
=
processTreeData
(
response
.
data
.
Results
);
treeData
.
value
=
treeStructure
;
// 收集所有有子节点的节点的key,用于默认展开
const
collectExpandableKeys
=
(
nodes
:
CustomTreeOption
[]):
(
string
|
number
)[]
=>
{
const
keys
:
(
string
|
number
)[]
=
[];
for
(
const
node
of
nodes
)
{
if
(
node
.
children
&&
node
.
children
.
length
>
0
)
{
keys
.
push
(
node
.
key
);
keys
.
push
(...
collectExpandableKeys
(
node
.
children
));
}
}
return
keys
;
};
expandedKeys
.
value
=
collectExpandableKeys
(
treeStructure
);
message
.
success
(
`已加载
${
response
.
data
.
Results
.
length
}
个测试项目`
);
}
else
{
...
...
@@ -67,49 +90,52 @@ async function fetchTestDetails() {
}
}
catch
(
error
)
{
message
.
error
(
'获取测试详情失败'
);
//
console.error(error);
console
.
error
(
error
);
}
finally
{
loading
.
value
=
false
;
}
}
// 处理树形结构数据
function
processTreeData
(
data
:
any
[])
{
function
processTreeData
(
data
:
any
[])
:
CustomTreeOption
[]
{
// 创建ID到节点的映射
const
idMap
:
Record
<
string
,
any
>
=
{};
const
result
:
any
[]
=
[];
const
result
:
CustomTreeOption
[]
=
[];
// 首先将所有节点添加到映射中
data
.
forEach
((
item
,
index
)
=>
{
const
node
=
{
id
:
index
+
1
,
key
:
item
.
Kvid
||
`item-
${
index
}
`
,
// 使用Kvid作为唯一标识,如果没有则使用索引
testItem
:
item
.
Title
||
'未命名项目'
,
standardValue
:
item
.
StandardValue
||
item
.
QualifiedValue
||
'未设置'
,
allowableRange
:
item
.
AllowableRange
||
'±5%'
,
const
node
:
any
=
{
key
:
item
.
Kvid
||
`item-
${
index
}
`
,
// 使用Kvid作为唯一标识
label
:
item
.
Title
||
'未命名项目'
,
parentId
:
item
.
ParentKvid
||
null
,
// 假设API返回ParentKvid表示父节点ID
children
:
[]
children
:
[],
origin
:
item
,
// 保存原始数据
level
:
0
// 初始层级为0,后续会更新
};
idMap
[
node
.
key
]
=
node
;
});
// 构建树形结构
Object
.
values
(
idMap
).
forEach
(
node
=>
{
Object
.
values
(
idMap
).
forEach
(
(
node
:
any
)
=>
{
if
(
node
.
parentId
&&
idMap
[
node
.
parentId
])
{
// 如果有父节点,将当前节点添加到父节点的children中
node
.
level
=
idMap
[
node
.
parentId
].
level
+
1
;
// 设置层级
idMap
[
node
.
parentId
].
children
.
push
(
node
);
}
else
{
// 如果没有父节点,则是顶级节点
node
.
level
=
0
;
// 顶级节点层级为0
result
.
push
(
node
);
}
});
// 处理空children数组
// 处理空children数组
并设置isLeaf
const
cleanEmptyChildren
=
(
nodes
:
any
[])
=>
{
for
(
const
node
of
nodes
)
{
if
(
node
.
children
.
length
===
0
)
{
node
.
isLeaf
=
true
;
delete
node
.
children
;
}
else
{
node
.
isLeaf
=
false
;
cleanEmptyChildren
(
node
.
children
);
}
}
...
...
@@ -126,35 +152,60 @@ watch(
if
(
newValue
)
{
fetchTestDetails
();
// 重置选中状态
checkedRowKeys
.
value
=
[];
checkedKeys
.
value
=
[];
// 重置展开状态
expandedKeys
.
value
=
[];
}
else
{
planDetails
.
value
=
[];
treeData
.
value
=
[];
checkedKeys
.
value
=
[];
expandedKeys
.
value
=
[];
}
},
{
immediate
:
true
}
);
// 处理
行选择
变化
function
handleChecked
Row
KeysChange
(
keys
:
(
string
|
number
)[])
{
checked
Row
Keys
.
value
=
keys
;
// 处理
选中状态
变化
function
handleCheckedKeysChange
(
keys
:
(
string
|
number
)[])
{
checkedKeys
.
value
=
keys
;
}
// 处理展开状态变化
function
handleExpandedKeysChange
(
keys
:
(
string
|
number
)[])
{
expandedKeys
.
value
=
keys
;
}
// 自定义节点渲染 - 显示详细信息
function
renderLabel
({
option
}:
any
)
{
const
origin
=
option
.
origin
;
if
(
!
origin
)
{
return
option
.
label
;
}
// 判断是父级还是子级,根据是否有children来确定
const
isParent
=
option
.
children
&&
option
.
children
.
length
>
0
;
const
icon
=
isParent
?
'📁'
:
'🧪'
;
// 构建显示内容
const
standardValue
=
origin
.
StandardValue
||
origin
.
QualifiedValue
||
'未设置'
;
const
allowableRange
=
origin
.
AllowableRange
||
'±5%'
;
return
h
(
'div'
,
{
class
:
'tree-node-content-inline'
},
[
h
(
'span'
,
{
class
:
'tree-node-icon'
,
style
:
'margin-right: 8px;'
},
icon
),
h
(
'span'
,
{
class
:
'tree-node-title-inline'
},
option
.
label
),
h
(
'span'
,
{
class
:
'tree-node-separator'
},
' '
),
h
(
'span'
,
{
class
:
'tree-node-detail-inline'
},
`📏 标准值:
${
standardValue
}
`
),
h
(
'span'
,
{
class
:
'tree-node-separator'
},
' '
),
h
(
'span'
,
{
class
:
'tree-node-detail-inline'
},
`⚖️ 允许偏差:
${
allowableRange
}
`
)
]);
}
// 计算测试方案名称
const
planName
=
computed
(()
=>
{
// 现在从API获取数据,planName可能需要从父组件传递或使用其他方式获取
return
`测试方案 (
${
props
.
standardKvid
}
)`
;
});
// 表格列定义
const
columns
=
[
{
type
:
'selection'
},
{
title
:
'测试项目'
,
key
:
'testItem'
,
align
:
'center'
},
{
title
:
'标准值'
,
key
:
'standardValue'
,
align
:
'center'
},
{
title
:
'允许偏差范围'
,
key
:
'allowableRange'
,
align
:
'center'
}
];
// 取消选择
function
cancel
()
{
emit
(
'cancel'
);
...
...
@@ -162,127 +213,110 @@ function cancel() {
// 确认选择
function
confirm
()
{
if
(
checked
Row
Keys
.
value
.
length
===
0
)
{
if
(
checkedKeys
.
value
.
length
===
0
)
{
message
.
warning
(
'请至少选择一个测试项目'
);
return
;
}
// 提取完整树形结构中被选中的节点和它们的子节点
const
cloneTreeData
=
JSON
.
parse
(
JSON
.
stringify
(
planDetails
.
value
));
// 过滤并标记选中的树节点
const
filterSelectedNodes
=
(
nodes
:
any
[])
=>
{
// 收集选中节点的原始数据
const
collectSelectedOriginData
=
(
nodes
:
CustomTreeOption
[]):
any
[]
=>
{
const
result
:
any
[]
=
[];
for
(
const
node
of
nodes
)
{
const
isSelected
=
checkedRowKeys
.
value
.
includes
(
node
.
key
);
// 如果当前节点被选中或者有被选中的子节点,则保留
if
(
isSelected
)
{
// 创建一个节点副本,只保留需要的属性
const
newNode
=
{
id
:
node
.
id
,
key
:
node
.
key
,
testItem
:
node
.
testItem
,
standardValue
:
node
.
standardValue
,
allowableRange
:
node
.
allowableRange
};
// 如果有子节点,递归处理
if
(
node
.
children
&&
node
.
children
.
length
>
0
)
{
const
selectedChildren
=
filterSelectedNodes
(
node
.
children
);
if
(
selectedChildren
.
length
>
0
)
{
newNode
.
children
=
selectedChildren
;
}
}
const
isSelected
=
checkedKeys
.
value
.
includes
(
node
.
key
);
result
.
push
(
newNode
);
}
else
if
(
node
.
children
&&
node
.
children
.
length
>
0
)
{
// 如果当前节点未被选中,但可能有被选中的子节点
const
selectedChildren
=
filterSelectedNodes
(
node
.
children
);
if
(
selectedChildren
.
length
>
0
)
{
// 创建一个节点副本,并添加被选中的子节点
const
newNode
=
{
id
:
node
.
id
,
key
:
node
.
key
,
testItem
:
node
.
testItem
,
standardValue
:
node
.
standardValue
,
allowableRange
:
node
.
allowableRange
,
children
:
selectedChildren
};
result
.
push
(
newNode
);
}
// 检查是否有子节点被选中
let
hasSelectedChildren
=
false
;
const
selectedChildren
:
any
[]
=
[];
if
(
node
.
children
&&
node
.
children
.
length
>
0
)
{
const
childResults
=
collectSelectedOriginData
(
node
.
children
);
selectedChildren
.
push
(...
childResults
);
// 检查子节点中是否有被选中的
hasSelectedChildren
=
node
.
children
.
some
((
child
:
CustomTreeOption
)
=>
checkedKeys
.
value
.
includes
(
child
.
key
)
||
(
child
.
children
&&
hasAnyChildSelected
(
child
))
);
}
// 如果当前节点被选中,或者有子节点被选中但当前节点未被选中时也要包含父节点
if
(
isSelected
&&
node
.
origin
)
{
result
.
push
(
node
.
origin
);
}
else
if
(
!
isSelected
&&
hasSelectedChildren
&&
node
.
origin
)
{
// 父节点未被选中但有子节点被选中,也要包含父节点
result
.
push
(
node
.
origin
);
}
// 添加选中的子节点
result
.
push
(...
selectedChildren
);
}
return
result
;
};
const
selectedTreeData
=
filterSelectedNodes
(
cloneTreeData
);
// 计算选中的项目总数
const
countSelectedItems
=
(
nodes
:
any
[]):
number
=>
{
let
count
=
0
;
for
(
const
node
of
nodes
)
{
count
+=
1
;
// 计算当前节点
if
(
node
.
children
&&
node
.
children
.
length
>
0
)
{
count
+=
countSelectedItems
(
node
.
children
);
// 递归计算子节点
}
// 递归检查节点是否有任何子节点被选中
const
hasAnyChildSelected
=
(
node
:
CustomTreeOption
):
boolean
=>
{
if
(
!
node
.
children
||
node
.
children
.
length
===
0
)
{
return
false
;
}
return
count
;
return
node
.
children
.
some
((
child
:
CustomTreeOption
)
=>
checkedKeys
.
value
.
includes
(
child
.
key
)
||
hasAnyChildSelected
(
child
)
);
};
const
selectedCount
=
countSelectedItems
(
selectedTreeData
);
// 获取选中节点的原始数据
const
selectedOriginData
=
collectSelectedOriginData
(
treeData
.
value
);
// 去重处理(避免重复添加)
const
uniqueData
=
selectedOriginData
.
filter
((
item
,
index
,
arr
)
=>
arr
.
findIndex
(
t
=>
t
.
Kvid
===
item
.
Kvid
)
===
index
);
// 将
处理后的树形结构
传递给父组件
emit
(
'confirm'
,
selectedTre
eData
);
message
.
success
(
`已选择
${
selectedCount
}
个测试项目
`
);
// 将
原始数据
传递给父组件
emit
(
'confirm'
,
uniqu
eData
);
message
.
success
(
`已选择
${
uniqueData
.
length
}
个测试项目(包含必要的父级项目)
`
);
}
// 页面加载时获取数据
onMounted
(()
=>
{
// 删除此处的fetchTestDetails调用,避免重复调用API
// watch中的immediate:true已经确保了初始加载数据
});
</
script
>
<
template
>
<div
class=
"plan-detail-selector"
>
<!--
<div
class=
"header"
>
<div
class=
"title"
>
测试方案明细选择
</div>
<n-space
style=
"display: flex; align-items: center;"
>
<span
class=
"plan-name"
>
{{
planName
}}
</span>
<n-tag
type=
"info"
>
{{
checkedRowKeys
.
length
}}
项已选择
</n-tag>
</n-space>
</div>
-->
<div
class=
"plan-detail-selector"
:style=
"cssVars"
>
<NAlert
type=
"info"
style=
"margin-bottom: 16px"
>
请选择需要进行比对测试的项目,选中的项目将被添加到测试数据表格中。
</NAlert>
<NDataTable
:columns=
"columns"
:data=
"planDetails"
:bordered=
"false"
:row-key=
"row => row.key"
:checked-row-keys=
"checkedRowKeys"
children-key=
"children"
:indent=
"20"
:max-height=
"300"
:min-height=
"300"
:pagination=
"false"
striped
:loading=
"loading"
@
update:checked-row-keys=
"handleCheckedRowKeysChange"
></NDataTable>
<div
class=
"tree-container"
>
<NTree
:data=
"treeData"
:checked-keys=
"checkedKeys"
:expanded-keys=
"expandedKeys"
checkable
cascade
check-strategy=
"all"
block-line
:render-label=
"renderLabel"
virtual-scroll
style=
"max-height: 300px; min-height: 300px;"
@
update:checked-keys=
"handleCheckedKeysChange"
@
update:expanded-keys=
"handleExpandedKeysChange"
/>
<div
v-if=
"loading"
class=
"loading-overlay"
>
<NSpin
size=
"medium"
/>
</div>
</div>
<div
class=
"footer"
>
<NSpace
justify=
"end"
>
<NButton
@
click=
"cancel"
>
取消
</NButton>
<NButton
type=
"primary"
:disabled=
"checked
Row
Keys.length === 0"
@
click=
"confirm"
>
确认选择(
{{
checked
Row
Keys
.
length
}}
)
<NButton
type=
"primary"
:disabled=
"checkedKeys.length === 0"
@
click=
"confirm"
>
确认选择(
{{
checkedKeys
.
length
}}
)
</NButton>
</NSpace>
</div>
...
...
@@ -291,34 +325,126 @@ onMounted(() => {
<
style
scoped
>
.plan-detail-selector
{
/* padding: 0 0 16px 0; */
height
:
450px
;
display
:
flex
;
flex-direction
:
column
;
}
.header
{
.tree-container
{
position
:
relative
;
flex
:
1
;
border
:
1px
solid
var
(
--border-color
);
border-radius
:
6px
;
overflow
:
hidden
;
}
.loading-overlay
{
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
rgba
(
255
,
255
,
255
,
0.8
);
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
24px
;
justify-content
:
center
;
z-index
:
10
;
}
.
title
{
font-size
:
18
px
;
font-weight
:
bold
;
color
:
#333
;
.
footer
{
padding-top
:
16
px
;
border-top
:
1px
solid
var
(
--border-color
)
;
margin-top
:
16px
;
}
.plan-name
{
font-weight
:
bold
;
color
:
#2080f0
;
/* 树节点样式 */
:deep
(
.n-tree-node-content
)
{
padding
:
8px
12px
!important
;
}
.footer
{
padding-top
:
10px
;
/* padding-bottom: 16px; */
border-top
:
1px
solid
#f0f0f0
;
margin-top
:
auto
;
:deep
(
.n-tree-node--pending
)
{
background-color
:
transparent
!important
;
}
:deep
(
.n-tree-node--selected
)
{
background-color
:
rgba
(
24
,
160
,
88
,
0.1
)
!important
;
}
:deep
(
.n-tree-node--highlighted
)
{
background-color
:
rgba
(
24
,
160
,
88
,
0.05
)
!important
;
}
/* 自定义节点内容样式 - 单行显示 */
.tree-node-content-inline
{
width
:
100%
;
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
flex-wrap
:
nowrap
;
}
.tree-node-title-inline
{
font-weight
:
600
;
color
:
var
(
--text-color
);
font-size
:
14px
;
min-width
:
120px
;
flex-shrink
:
0
;
}
.tree-node-separator
{
color
:
#ccc
;
font-size
:
12px
;
margin
:
0
4px
;
flex-shrink
:
0
;
}
.tree-node-detail-inline
{
font-size
:
12px
;
color
:
#666
;
background
:
#f5f5f5
;
padding
:
2px
6px
;
border-radius
:
3px
;
white-space
:
nowrap
;
flex-shrink
:
0
;
}
.tree-node-icon
{
font-size
:
14px
;
display
:
inline-block
;
vertical-align
:
middle
;
}
/* 父级节点样式 */
:deep
(
.n-tree-node
[
aria-level
=
"1"
])
{
background-color
:
#f8f9fa
!important
;
}
:deep
(
.n-tree-node
[
aria-level
=
"1"
]
:hover
)
{
background-color
:
#e9ecef
!important
;
}
/* 子级节点样式 */
:deep
(
.n-tree-node
[
aria-level
=
"2"
])
{
background-color
:
#fff
!important
;
}
:deep
(
.n-tree-node
[
aria-level
=
"2"
]
:hover
)
{
background-color
:
#f5f5f5
!important
;
}
/* 选中状态样式 */
:deep
(
.n-tree-node--checked
)
{
background-color
:
rgba
(
24
,
160
,
88
,
0.1
)
!important
;
}
:deep
(
.n-tree-node--checked
:hover
)
{
background-color
:
rgba
(
24
,
160
,
88
,
0.15
)
!important
;
}
/* 暗色主题适配 */
:deep
(
.n-tree.n-tree--block-line
.n-tree-node
)
{
border-bottom
:
1px
solid
var
(
--border-color
);
}
</
style
>
src/views/Lac/codes/comparisonTest.vue
View file @
54b22571
<
script
setup
lang=
"ts"
>
import
{
computed
,
defineEmits
,
defineProps
,
h
,
onMounted
,
ref
,
resolveComponent
}
from
'vue'
;
import
TestPlanDetailSelector
from
'./TestPlanDetailSelector.vue'
;
// '/codes/TestPlanDetailSelector.vue';
import
{
computed
,
defineEmits
,
defineProps
,
h
,
onMounted
,
reactive
,
ref
,
resolveComponent
,
watch
,
withDefaults
}
from
'vue'
;
import
TestPlanDetailSelector
from
'./TestPlanDetailSelector.vue'
;
//'/codes/Vue3/Lab/TestPlanDetailSelector.vue'
interface
TestItem
{
id
:
number
;
name
:
string
;
selected
:
boolean
;
// 根据API文档定义完整的数据接口
interface
TestDataItem
{
Kvid
:
string
;
// 业务相关字段
BizKvid
?:
string
;
BizId
?:
string
;
BizType
?:
string
;
RootKvid
?:
string
;
ReportKvid
?:
string
;
SampleKvid
?:
string
;
SampleRootKvid
?:
string
;
// 标准相关字段
StandardKvid
?:
string
;
StandardCode
?:
string
;
StandardTitle
?:
string
;
// 检测项目相关字段
DetectionKvid
?:
string
;
DetectionTitle
?:
string
;
DetectionReferenceKvid
?:
string
;
DetectionCodeActual
?:
string
;
DetectionUnit
?:
string
;
DetectionValueQualified
?:
string
;
// 货币和价格相关字段
Currency
?:
string
;
GoodsKvid
?:
string
;
PackageKvid
?:
string
;
PackageUnit
?:
string
;
AmountPlan
?:
number
;
Amount
?:
number
;
// 测试相关字段
TestKvid
?:
string
;
TestRootKvid
?:
string
;
TestValue
?:
string
;
TestOriginalFilePath
?:
string
;
TestCreatorKvid
?:
string
;
TestCreatorName
?:
string
;
TestReviewerKvid
?:
string
;
TestReviewerName
?:
string
;
TestCreateTime
?:
string
;
TestReviewTime
?:
string
;
// 层级和基本信息字段
ParentKvid
?:
string
;
Type
?:
string
;
Title
:
string
;
SortId
?:
number
;
SortIdEx
?:
number
;
Judgement
?:
string
;
Remark
?:
string
;
// 工作组和人员字段
WorkGroupKvid
?:
string
;
WorkGroupName
?:
string
;
WorkerKvid
?:
string
;
WorkerName
?:
string
;
CreatorKvid
?:
string
;
CreatorName
?:
string
;
UpdaterKvid
?:
string
;
UpdaterName
?:
string
;
// 状态相关字段(由前端计算)
StatusType
?:
string
;
Status
?:
string
;
StatusColor
?:
string
;
// 前端扩展字段(用于界面交互)
measuredValue
?:
string
;
verifier
?:
string
;
result
?:
string
;
// 兼容旧字段
Unit
?:
string
;
QualifiedValue
?:
string
;
[
key
:
string
]:
any
;
}
interface
TestDataItem
{
key
:
string
;
testItem
:
string
;
standardValue
:
string
;
allowableRange
:
string
;
measuredValue
:
string
|
null
;
verifier
:
string
|
null
;
result
:
string
;
children
?:
TestDataItem
[];
// 定义Item对象的接口
interface
ItemProps
{
Kvid
?:
string
;
[
key
:
string
]:
any
;
}
//
eslint-disable-next-line @typescript-eslint/no-unused-var
s
const
props
=
defineProps
({
testItems
:
{
type
:
Array
as
()
=>
TestItem
[],
required
:
true
},
saveAsDraft
:
{
type
:
Boolean
,
required
:
tru
e
//
定义prop
s
const
props
=
withDefaults
(
defineProps
<
{
item
?:
ItemProps
;
active
?:
boolean
;
}
>
()
,
{
item
:
()
=>
({})
,
active
:
fals
e
}
}
);
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const
emit
=
defineEmits
([
'update:testItems'
,
'update:saveAsDraft'
]);
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
const
axios
=
(
window
as
any
).
$axios
;
// 获取主题状态
const
themeStore
=
(
window
as
any
).
$themeStore
;
// 获取消息提示
const
message
=
(
window
as
any
).
$message
;
const
dialog
=
(
window
as
any
).
$dialog
;
// 通过计算属性确定当前的主体色
const
isDarkMode
=
computed
(()
=>
themeStore
?.
darkMode
||
false
);
// 根据主题计算文字颜色
const
textColor
=
computed
(()
=>
(
isDarkMode
.
value
?
'rgba(255, 255, 255, 0.85)'
:
'rgba(0, 0, 0, 0.85)'
));
const
legendTextColor
=
computed
(()
=>
(
isDarkMode
.
value
?
'rgba(255, 255, 255, 0.65)'
:
'rgba(0, 0, 0, 0.65)'
));
const
borderColor
=
computed
(()
=>
(
isDarkMode
.
value
?
'#303030'
:
'#ffffff'
));
// CSS变量
const
cssVars
=
computed
(()
=>
({
'--text-color'
:
textColor
.
value
,
'--bg-color'
:
isDarkMode
.
value
?
'#1f1f1f'
:
'#f5f5f5'
,
'--panel-bg'
:
isDarkMode
.
value
?
'#262626'
:
'#ffffff'
,
'--card-bg'
:
isDarkMode
.
value
?
'#303030'
:
'#ffffff'
,
'--border-color'
:
isDarkMode
.
value
?
'#434343'
:
'#e8e8e8'
}));
// 表单数据
/**
* 存储从props传入的详细数据
*
* 保存父组件传递的item数据的响应式副本
*/
const
detailedData
=
reactive
<
ItemProps
>
({});
/**
* 比对测试数据列表
*
* 存储当前显示的所有比对测试记录,数据结构遵循API文档规范
*/
const
testData
=
ref
<
TestDataItem
[]
>
([]);
/**
* 加载状态标识
*
* 控制各种异步操作的加载状态显示
*/
const
loading
=
ref
<
boolean
>
(
false
);
/**
* 测试方案选择器选项
*
* 存储可选的测试方案列表
*/
const
titleOptions
=
ref
<
any
[]
>
([]);
/**
* 当前选中的测试方案
*
* 存储用户选择的测试方案ID
*/
const
testPlan
=
ref
(
''
);
const
testDate
=
ref
(
null
);
const
sampleCode
=
ref
(
''
);
const
testResult
=
ref
(
''
);
const
testComment
=
ref
(
''
);
/**
* 控制测试方案明细选择弹窗显示状态
*
* 控制TestPlanDetailSelector模态框的开关
*/
const
showDetailSelector
=
ref
(
false
);
// 状态变量
const
loading
=
ref
<
boolean
>
(
false
);
const
titleOptions
=
ref
<
Array
<
{
label
:
string
;
value
:
string
}
>>
([]);
/**
* 计算是否存在比对测试数据
*
* 用于控制测试方案选择器的禁用状态和清空按钮的显示
*
* @returns {boolean} 是否有现有的比对测试数据
* @usage 控制NSelect的disabled状态和清空按钮的显示条件
*/
const
hasExistingData
=
computed
(()
=>
{
return
testData
.
value
.
length
>
0
;
});
/**
* 状态类型映射函数
*
* 将API返回的StatusType字符串映射为中文显示状态
*
* @param {string} statusType - API返回的状态类型字符串
* @returns {string} 对应的中文状态显示名称
* @usage 在数据处理和状态显示时使用
*/
const
mapStatusType
=
(
statusType
:
string
)
=>
{
const
statusMap
:
{
[
key
:
string
]:
string
}
=
{
Unsupported
:
'草稿'
,
BeforeTest
:
'待检测'
,
Asigning
:
'检测中'
,
Testing
:
'检测中'
,
BeforeReview
:
'待复核'
,
TestFinished
:
'不通过'
,
TestCollected
:
'已通过'
};
return
statusMap
[
statusType
]
||
'草稿'
;
};
/**
* 状态颜色获取函数
*
* 根据状态类型返回对应的UI显示颜色
*
* @param {string} status - 状态类型字符串
* @returns {string} 对应状态的十六进制颜色值
* @usage 在表格和标签组件中设置状态颜色
*/
const
getStatusColor
=
(
status
:
string
)
=>
{
const
statusColorMap
:
{
[
key
:
string
]:
string
}
=
{
BeforeTest
:
'#f56a00'
,
Asigning
:
'#1890ff'
,
Testing
:
'#722ed1'
,
BeforeReview
:
'#eb2f96'
,
TestFinished
:
'#13c2c2'
,
TestCollected
:
'#52c41a'
};
return
statusColorMap
[
status
]
||
'#666'
;
};
// 监听props.item变化,同步到detailedData
watch
(
()
=>
props
.
item
,
newItem
=>
{
if
(
newItem
&&
newItem
.
Kvid
&&
newItem
.
Kvid
!==
detailedData
.
Kvid
)
{
fetchDataByReportKvid
(
newItem
.
Kvid
);
}
// 保持detailedData为props.item的响应式副本
Object
.
assign
(
detailedData
,
newItem
||
{});
},
{
immediate
:
true
,
deep
:
true
}
);
watch
(
()
=>
props
.
active
,
newActive
=>
{
if
(
newActive
)
{
if
(
props
.
item
&&
props
.
item
.
Kvid
&&
props
.
item
.
Kvid
!==
detailedData
.
Kvid
)
{
fetchDataByReportKvid
(
props
.
item
.
Kvid
);
}
}
},
{
immediate
:
true
}
);
/**
* 根据报告Kvid获取比对测试数据
*
* 使用报告ID查询已存在的比对测试数据记录
*
* @param {string} reportKvid - 报告的唯一标识符
* @returns {Promise<void>} 无返回值,结果存储在testData.value中
* @usage 在组件初始化和props变化时调用,获取现有的比对测试数据
*/
async
function
fetchDataByReportKvid
(
reportKvid
:
string
)
{
if
(
!
reportKvid
)
return
;
if
(
reportKvid
===
detailedData
.
Kvid
&&
testData
.
value
.
length
>
0
)
{
return
;
}
// 获取测试方案选项
async
function
fetchTestPlanOptions
()
{
try
{
loading
.
value
=
true
;
// 调用标准实体查询接口
const
response
=
await
axios
.
post
(
'/Restful/Kivii.Standards.Entities.Standard/Query.json?Type=比对测试'
);
// console.log('测试方案响应数据:', response.data);
const
response
=
await
axios
.
get
(
'/Restful/Kivii.Lims.Entities.ReportItem/QueryEx.json'
,
{
params
:
{
ReportKvid
:
reportKvid
,
OrderBy
:
'SortId,SortIdEx,Kvid'
,
WorkGroupName
:
'比对测试'
}
});
if
(
response
.
data
&&
response
.
data
.
Results
&&
response
.
data
.
Results
.
length
>
0
)
{
testData
.
value
=
response
.
data
.
Results
.
map
((
item
:
any
)
=>
({
...
item
,
Status
:
mapStatusType
(
item
.
StatusType
),
StatusColor
:
getStatusColor
(
item
.
StatusType
)
}));
message
.
success
(
'比对测试数据加载成功'
);
}
else
{
message
.
info
(
'暂无比对测试数据'
);
}
}
catch
(
error
)
{
message
.
error
(
'获取比对测试数据失败,请稍后重试'
);
}
finally
{
loading
.
value
=
false
;
}
}
// 添加数据存在性检查
/**
* 获取文件类型选项数据
*
* 从标准实体接口查询比对测试类型数据,用于下拉选择器
*
* @returns {Promise<void>} 无返回值,结果存储在titleOptions.value中
* @usage 点击下拉框时懒加载,有数据则不重复加载
*/
async
function
fetchTitleOptions
()
{
if
(
titleOptions
.
value
.
length
>
0
)
{
return
;
}
try
{
loading
.
value
=
true
;
const
response
=
await
axios
.
get
(
'/Restful/Kivii.Standards.Entities.Standard/Query.json'
,
{
params
:
{
Type
:
'比对测试'
}
});
if
(
response
.
data
&&
response
.
data
.
Results
)
{
// 直接使用Results数组
titleOptions
.
value
=
response
.
data
.
Results
.
map
((
item
:
any
)
=>
({
label
:
item
.
Title
,
value
:
item
.
Kvid
value
:
item
.
Kvid
,
fullObject
:
item
}));
}
else
{
// console.error('无效的数据格式:', response.data);
message
.
error
(
'数据格式不正确'
);
}
}
catch
(
error
)
{
message
.
error
(
'获取测试方案失败'
);
// console.error('获取测试方案失败:', error);
}
finally
{
loading
.
value
=
false
;
}
}
// 表格列定义
const
columns
=
[
{
title
:
'测试项目'
,
key
:
'testItem'
},
{
title
:
'标准值'
,
key
:
'standardValue'
,
align
:
'center'
as
const
},
{
title
:
'允许偏差范围'
,
key
:
'allowableRange'
,
align
:
'center'
as
const
},
{
title
:
'实测值'
,
key
:
'measuredValue'
,
align
:
'center'
as
const
,
render
:
(
row
:
TestDataItem
)
=>
{
return
h
(
resolveComponent
(
'n-input'
),
{
value
:
row
.
measuredValue
,
placeholder
:
'请输入测试值'
,
onUpdateValue
:
(
value
:
string
)
=>
{
row
.
measuredValue
=
value
;
checkTestResult
(
row
);
}
});
}
// 树形数据结构定义
interface
TreeDataItem
{
key
:
string
;
label
:
string
;
children
?:
TreeDataItem
[];
origin
?:
TestDataItem
;
// 保存原始数据
isLeaf
?:
boolean
;
[
key
:
string
]:
any
;
}
// 树形数据
const
treeData
=
ref
<
TreeDataItem
[]
>
([]);
// 编辑状态管理
const
editingStates
=
ref
<
Record
<
string
,
{
measuredValue
:
boolean
;
verifier
:
boolean
}
>>
({});
// 获取编辑状态
function
getEditingState
(
kvid
:
string
,
field
:
'measuredValue'
|
'verifier'
)
{
if
(
!
editingStates
.
value
[
kvid
])
{
editingStates
.
value
[
kvid
]
=
{
measuredValue
:
false
,
verifier
:
false
};
}
return
editingStates
.
value
[
kvid
][
field
];
}
// 设置编辑状态
function
setEditingState
(
kvid
:
string
,
field
:
'measuredValue'
|
'verifier'
,
value
:
boolean
)
{
if
(
!
editingStates
.
value
[
kvid
])
{
editingStates
.
value
[
kvid
]
=
{
measuredValue
:
false
,
verifier
:
false
};
}
editingStates
.
value
[
kvid
][
field
]
=
value
;
}
// 监听testData变化,更新treeData
watch
(
testData
,
(
newTestData
)
=>
{
treeData
.
value
=
convertToTreeData
(
newTestData
);
},
{
title
:
'授信人'
,
key
:
'verifier'
,
align
:
'center'
as
const
,
render
:
(
row
:
TestDataItem
)
=>
{
return
h
(
resolveComponent
(
'n-input'
),
{
value
:
row
.
verifier
,
placeholder
:
'请输入授信人'
,
onUpdateValue
:
(
value
:
string
)
=>
{
row
.
verifier
=
value
;
}
});
{
immediate
:
true
,
deep
:
true
}
);
// 将测试数据转换为树形结构
function
convertToTreeData
(
data
:
TestDataItem
[]):
TreeDataItem
[]
{
const
idMap
:
Record
<
string
,
any
>
=
{};
const
result
:
TreeDataItem
[]
=
[];
// 首先将所有节点添加到映射中
data
.
forEach
((
item
)
=>
{
const
node
:
any
=
{
key
:
item
.
Kvid
,
label
:
item
.
Title
,
parentId
:
item
.
ParentKvid
||
null
,
children
:
[],
origin
:
item
,
// 保存原始数据
level
:
0
};
idMap
[
node
.
key
]
=
node
;
});
// 构建树形结构
Object
.
values
(
idMap
).
forEach
((
node
:
any
)
=>
{
if
(
node
.
parentId
&&
idMap
[
node
.
parentId
])
{
node
.
level
=
idMap
[
node
.
parentId
].
level
+
1
;
idMap
[
node
.
parentId
].
children
.
push
(
node
);
}
else
{
node
.
level
=
0
;
result
.
push
(
node
);
}
},
{
title
:
'结果'
,
key
:
'result'
,
align
:
'center'
as
const
,
render
:
(
_row
:
TestDataItem
)
=>
{
return
h
(
resolveComponent
(
'n-tag'
),
{
type
:
'default'
},
{
default
:
()
=>
'待测试'
});
});
// 处理空children数组并设置isLeaf
const
cleanEmptyChildren
=
(
nodes
:
any
[])
=>
{
for
(
const
node
of
nodes
)
{
if
(
node
.
children
.
length
===
0
)
{
node
.
isLeaf
=
true
;
delete
node
.
children
;
}
else
{
node
.
isLeaf
=
false
;
cleanEmptyChildren
(
node
.
children
);
}
}
},
{
title
:
'操作'
,
key
:
'actions'
,
width
:
200
,
align
:
'center'
,
render
(
row
:
any
)
{
return
h
(
'div'
,
{
style
:
'display: flex; gap: 8px; justify-content: center;'
},
[
h
(
resolveComponent
(
'n-button'
),
{
};
cleanEmptyChildren
(
result
);
return
result
;
}
// 自定义节点渲染函数
function
renderTreeLabel
({
option
}:
any
)
{
const
origin
=
option
.
origin
;
if
(
!
origin
)
{
return
option
.
label
;
}
// 判断是父级还是子级
const
isParent
=
option
.
children
&&
option
.
children
.
length
>
0
;
const
icon
=
isParent
?
'📁'
:
'🧪'
;
// 构建显示内容
const
qualifiedValue
=
origin
.
DetectionValueQualified
||
'未设置'
;
const
unit
=
origin
.
DetectionUnit
||
origin
.
Unit
||
''
;
const
measuredValue
=
origin
.
TestValue
||
origin
.
measuredValue
||
''
;
const
verifier
=
origin
.
TestReviewerName
||
origin
.
verifier
||
''
;
const
status
=
origin
.
Status
||
'待测试'
;
return
h
(
'div'
,
{
style
:
'width: 100%; display: flex; align-items: center; justify-content: space-between; min-height: 40px; gap: 16px;'
},
[
// 左侧内容
h
(
'div'
,
{
style
:
'display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0;'
},
[
h
(
'span'
,
{
style
:
'margin-right: 8px; font-size: 14px;'
},
icon
),
h
(
'span'
,
{
style
:
'font-weight: 600; color: #333; font-size: 14px;'
},
option
.
label
),
h
(
'span'
,
{
style
:
'color: #ccc; margin: 0 8px;'
},
'|'
),
h
(
'span'
,
{
style
:
'font-size: 12px; color: #666; background: #f5f5f5; padding: 4px 8px; border-radius: 4px; white-space: nowrap;'
},
`📏 标准值:
${
qualifiedValue
}${
unit
?
` (
${
unit
}
)`
:
''
}
`
)
]),
// 右侧内容
h
(
'div'
,
{
style
:
'display: flex; align-items: center; gap: 8px; flex-shrink: 0;'
},
[
// 实测值编辑区域
h
(
'span'
,
{
style
:
'display: flex; align-items: center; gap: 4px;'
},
[
h
(
'span'
,
{
style
:
'font-size: 12px; color: #666; white-space: nowrap;'
},
'🔬 实测值:'
),
getEditingState
(
origin
.
Kvid
,
'measuredValue'
)
?
h
(
resolveComponent
(
'n-input'
),
{
value
:
measuredValue
,
placeholder
:
'请输入'
,
size
:
'small'
,
style
:
'width: 100px;'
,
autofocus
:
true
,
onUpdateValue
:
(
value
:
string
)
=>
{
origin
.
TestValue
=
value
;
origin
.
measuredValue
=
value
;
// 保持兼容
checkTestResult
(
origin
);
},
onBlur
:
()
=>
{
setEditingState
(
origin
.
Kvid
,
'measuredValue'
,
false
);
},
onKeydown
:
(
e
:
KeyboardEvent
)
=>
{
if
(
e
.
key
===
'Enter'
)
{
setEditingState
(
origin
.
Kvid
,
'measuredValue'
,
false
);
}
}
})
:
h
(
'span'
,
{
style
:
'min-width: 100px; padding: 4px 8px; background: #f5f5f5; border-radius: 4px; cursor: pointer; font-size: 12px; color: #333;'
,
onClick
:
(
e
:
Event
)
=>
{
e
.
stopPropagation
();
setEditingState
(
origin
.
Kvid
,
'measuredValue'
,
true
);
}
},
measuredValue
||
'点击输入'
)
]),
h
(
'span'
,
{
style
:
'color: #ccc; margin: 0 4px;'
},
'|'
),
// 授信人编辑区域
h
(
'span'
,
{
style
:
'display: flex; align-items: center; gap: 4px;'
},
[
h
(
'span'
,
{
style
:
'font-size: 12px; color: #666; white-space: nowrap;'
},
'👤 授信人:'
),
getEditingState
(
origin
.
Kvid
,
'verifier'
)
?
h
(
resolveComponent
(
'n-input'
),
{
value
:
verifier
,
placeholder
:
'请输入'
,
size
:
'small'
,
type
:
'error'
,
onClick
:
()
=>
deleteTestPlanDetail
(
row
)
},
{
default
:
()
=>
'删除'
}
)
]);
style
:
'width: 100px;'
,
autofocus
:
true
,
onUpdateValue
:
(
value
:
string
)
=>
{
origin
.
TestReviewerName
=
value
;
origin
.
verifier
=
value
;
// 保持兼容
},
onBlur
:
()
=>
{
setEditingState
(
origin
.
Kvid
,
'verifier'
,
false
);
},
onKeydown
:
(
e
:
KeyboardEvent
)
=>
{
if
(
e
.
key
===
'Enter'
)
{
setEditingState
(
origin
.
Kvid
,
'verifier'
,
false
);
}
}
})
:
h
(
'span'
,
{
style
:
'min-width: 100px; padding: 4px 8px; background: #f5f5f5; border-radius: 4px; cursor: pointer; font-size: 12px; color: #333;'
,
onClick
:
(
e
:
Event
)
=>
{
e
.
stopPropagation
();
setEditingState
(
origin
.
Kvid
,
'verifier'
,
true
);
}
},
verifier
||
'点击输入'
)
]),
h
(
'span'
,
{
style
:
'color: #ccc; margin: 0 4px;'
},
'|'
),
h
(
'span'
,
{
style
:
'display: flex; align-items: center; gap: 4px;'
},
[
h
(
'span'
,
{
style
:
'margin-right: 4px;'
},
'✅'
),
h
(
resolveComponent
(
'n-tag'
),
{
type
:
status
===
'待测试'
?
'default'
:
'success'
,
size
:
'small'
},
{
default
:
()
=>
status
})
]),
h
(
'span'
,
{
style
:
'color: #ccc; margin: 0 4px;'
},
'|'
),
// 删除按钮
h
(
resolveComponent
(
'n-button'
),
{
size
:
'small'
,
type
:
'error'
,
style
:
'background-color: #ff4757; border-color: #ff4757; color: white; border-radius: 4px; font-size: 12px; padding: 4px 12px; height: 28px;'
,
onClick
:
(
e
:
Event
)
=>
{
e
.
stopPropagation
();
deleteTestPlanDetail
(
origin
);
}
},
{
default
:
()
=>
'删除'
})
])
]);
}
/**
* 批量删除实体数据
*
* 通用的删除方法,支持批量删除多个文件审核记录
*
* @param {string[]} kvids - 要删除的记录ID数组
* @returns {Promise<boolean>} 删除操作是否成功
* @usage 被deleteFileRecord和clearAllFileData方法调用
*/
async
function
deleteEntities
(
kvids
:
string
[])
{
if
(
!
kvids
||
kvids
.
length
===
0
)
{
message
.
error
(
'请选择要删除的项目'
);
return
false
;
}
try
{
loading
.
value
=
true
;
const
response
=
await
axios
.
post
(
'/Restful/Kivii.Lims.Entities.ReportItem/Delete.json'
,
{
Kvids
:
kvids
});
if
(
response
.
data
&&
response
.
data
.
Results
&&
response
.
data
.
Results
.
length
>
0
)
{
message
.
success
(
`成功删除
${
kvids
.
length
}
个文件`
);
return
true
;
}
message
.
error
(
response
.
data
?.
Message
||
'删除失败'
);
return
false
;
}
catch
(
error
)
{
message
.
error
(
'删除文件失败,请稍后重试'
);
return
false
;
}
finally
{
loading
.
value
=
false
;
}
];
}
function
deleteTestPlanDetail
(
row
:
TestDataItem
)
{
async
function
deleteTestPlanDetail
(
row
:
TestDataItem
)
{
dialog
.
warning
({
title
:
'确认删除'
,
content
:
`是否确认删除测试项"
${
row
.
testItem
}
"?`
,
content
:
`是否确认删除测试项"
${
row
.
Title
}
"?`
,
positiveText
:
'确认'
,
negativeText
:
'取消'
,
onPositiveClick
:
()
=>
{
// 递归查找并删除行项目
const
removeItem
=
(
items
:
TestDataItem
[],
key
:
string
):
boolean
=>
{
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
if
(
items
[
i
].
key
===
key
)
{
items
.
splice
(
i
,
1
);
return
true
;
}
if
(
items
[
i
].
children
&&
items
[
i
].
children
.
length
>
0
)
{
if
(
removeItem
(
items
[
i
].
children
,
key
))
{
return
true
;
}
}
onPositiveClick
:
async
()
=>
{
// 根据ParentKvid递归收集所有需要删除的子项Kvid
const
collectKvidsToDelete
=
(
parentKvid
:
string
):
string
[]
=>
{
const
kvidsToDelete
:
string
[]
=
[];
// 查找直接子项
const
directChildren
=
testData
.
value
.
filter
(
item
=>
item
.
ParentKvid
===
parentKvid
);
for
(
const
child
of
directChildren
)
{
kvidsToDelete
.
push
(
child
.
Kvid
);
// 递归查找该子项的所有后代
const
descendantKvids
=
collectKvidsToDelete
(
child
.
Kvid
);
kvidsToDelete
.
push
(...
descendantKvids
);
}
return
false
;
return
kvidsToDelete
;
};
if
(
removeItem
(
testData
.
value
,
row
.
key
))
{
message
.
success
(
'删除成功'
);
try
{
// 收集所有需要删除的Kvid,包括当前项和所有子项
const
kvidsToDelete
=
[
row
.
Kvid
,
...
collectKvidsToDelete
(
row
.
Kvid
)];
if
(
kvidsToDelete
.
length
===
0
)
{
message
.
error
(
'未找到需要删除的项目'
);
return
;
}
// 调用deleteEntities方法批量删除
const
success
=
await
deleteEntities
(
kvidsToDelete
);
if
(
success
)
{
// 从testData中移除已删除的项目
testData
.
value
=
testData
.
value
.
filter
(
item
=>
!
kvidsToDelete
.
includes
(
item
.
Kvid
));
message
.
success
(
`成功删除
${
kvidsToDelete
.
length
}
个项目`
);
}
}
catch
(
error
)
{
// console.error('删除过程中发生错误:', error);
message
.
error
(
'删除失败,请稍后重试'
);
}
}
});
}
// 测试数据
const
testData
=
ref
<
TestDataItem
[]
>
([]);
// 处理测试方案变化
function
handlePlanChange
(
value
:
string
)
{
if
(
value
)
{
...
...
@@ -201,64 +629,110 @@ function handlePlanChange(value: string) {
}
// 处理方案明细确认事件
function
handlePlanDetailsConfirm
(
selectedItems
:
any
[])
{
// 获取当前最大测试项索引,用于生成唯一key
let
maxIndex
=
0
;
if
(
testData
.
value
.
length
>
0
)
{
// 从现有key中提取数字部分
const
keyNumbers
=
testData
.
value
.
map
(
item
=>
{
const
matches
=
item
.
key
.
match
(
/test-
(\d
+
)
/
);
return
matches
?
Number
.
parseInt
(
matches
[
1
],
10
)
:
0
;
});
maxIndex
=
Math
.
max
(...
keyNumbers
);
async
function
handlePlanDetailsConfirm
(
selectedItems
:
any
[])
{
if
(
!
testPlan
.
value
)
{
message
.
error
(
'请先选择测试方案'
);
return
;
}
if
(
!
detailedData
.
Kvid
)
{
message
.
error
(
'缺少报告信息'
);
return
;
}
if
(
!
selectedItems
||
selectedItems
.
length
===
0
)
{
message
.
error
(
'请选择要添加的测试项目'
);
return
;
}
// 处理新选择的测试项,添加正确的key
const
processedItems
=
processTestItems
(
selectedItems
,
maxIndex
);
// 将处理后的数据添加到现有数据中
testData
.
value
=
[...
testData
.
value
,
...
processedItems
];
showDetailSelector
.
value
=
false
;
message
.
success
(
`已添加
${
selectedItems
.
length
}
个测试项目`
);
}
// 处理测试项目数据,添加必要的字段并构建树形结构
function
processTestItems
(
items
:
any
[],
startId
=
0
)
{
// 创建一个新的数组来存储处理后的数据
return
items
.
map
((
item
,
index
)
=>
{
const
newId
=
startId
+
index
+
1
;
return
{
key
:
`test-
${
newId
}
`
,
testItem
:
item
.
testItem
,
standardValue
:
item
.
standardValue
,
allowableRange
:
item
.
allowableRange
,
measuredValue
:
null
,
verifier
:
null
,
result
:
'待测试'
,
// 如果原数据中有children,也需要递归处理
...(
item
.
children
?
{
children
:
processTestItems
(
item
.
children
,
startId
+
items
.
length
)
}
:
{})
try
{
loading
.
value
=
true
;
// 直接使用用户选择的项目来创建比对测试数据
const
createData
:
any
=
{
Detections
:
selectedItems
,
ReportKvid
:
detailedData
.
Kvid
};
const
createResponse
=
await
axios
.
post
(
'/Restful/Kivii.Lims.Entities.ReportItem/Detection.json'
,
createData
);
if
(
createResponse
.
data
&&
createResponse
.
data
.
Results
)
{
// 处理返回的数据
testData
.
value
=
createResponse
.
data
.
Results
.
map
((
item
:
any
)
=>
({
...
item
,
Status
:
mapStatusType
(
item
.
StatusType
),
StatusColor
:
getStatusColor
(
item
.
StatusType
)
}));
showDetailSelector
.
value
=
false
;
message
.
success
(
`已添加
${
selectedItems
.
length
}
个测试项目`
);
}
else
{
message
.
error
(
'创建比对测试数据失败'
);
}
}
catch
(
error
)
{
message
.
error
(
'创建比对测试数据失败,请稍后重试'
);
}
finally
{
loading
.
value
=
false
;
}
}
/**
* 清空所有比对测试数据
*
* 删除当前列表中的所有比对测试记录,并重置相关状态
*
* @returns {void} 无返回值
* @usage 绑定到"清空数据"按钮的点击事件
*/
function
clearAllTestData
()
{
dialog
.
warning
({
title
:
'确认删除'
,
content
:
'确定要删除所有比对测试数据吗?此操作不可撤销。'
,
positiveText
:
'确定删除'
,
negativeText
:
'取消'
,
onPositiveClick
:
async
()
=>
{
if
(
testData
.
value
.
length
===
0
)
{
message
.
info
(
'没有数据需要删除'
);
return
;
}
const
kvids
=
testData
.
value
.
map
(
item
=>
item
.
Kvid
).
filter
(
kvid
=>
kvid
);
if
(
kvids
.
length
===
0
)
{
message
.
error
(
'没有有效的数据ID'
);
return
;
}
const
success
=
await
deleteEntities
(
kvids
);
if
(
success
)
{
testData
.
value
=
[];
testPlan
.
value
=
''
;
}
}
});
}
// 页面加载时获取测试方案选项
onMounted
(()
=>
{
fetchTestPlanOptions
();
if
(
props
.
active
)
{
if
(
props
.
item
&&
props
.
item
.
Kvid
&&
props
.
item
.
Kvid
!==
detailedData
.
Kvid
)
{
fetchDataByReportKvid
(
props
.
item
.
Kvid
);
}
}
});
// 检查测试结果
function
checkTestResult
(
row
:
TestDataItem
)
{
// 这里可以添加逻辑来判断测试结果是否在允许范围内
// 现在只是占位,实际应用中应根据实测值和标准值+允许偏差范围判断
if
(
row
.
measuredValue
)
{
const
testValue
=
row
.
TestValue
||
row
.
measuredValue
;
if
(
testValue
)
{
row
.
result
=
'待评估'
;
row
.
Status
=
'待评估'
;
}
else
{
row
.
result
=
'待测试'
;
row
.
Status
=
'待测试'
;
}
}
</
script
>
...
...
@@ -275,22 +749,43 @@ function checkTestResult(row: TestDataItem) {
<span>
测试方案选择
</span>
</div>
<NFormItem
label=
"测试方案"
required
>
<NSelect
v-model:value=
"testPlan"
:options=
"titleOptions"
placeholder=
"请选择测试方案"
style=
"width: 100%"
@
update:value=
"handlePlanChange"
></NSelect>
</NFormItem>
<NFormItem
label=
"选择测试方案"
required
>
<div
class=
"test-plan-selector"
>
<div
class=
"selector-row"
>
<NSelect
v-model:value=
"testPlan"
:options=
"titleOptions"
:placeholder=
"hasExistingData ? '已有数据,请先删除' : '请选择测试方案'"
:disabled=
"hasExistingData"
style=
"flex: 1"
@
update:value=
"handlePlanChange"
@
focus=
"fetchTitleOptions"
></NSelect>
<NFormItem
label=
"测试日期"
required
>
<NDatePicker
v-model:value=
"testDate"
type=
"date"
clearable
style=
"width: 100%"
format=
"yyyy/MM/dd"
></NDatePicker>
</NFormItem>
<!-- 有数据时显示删除按钮 -->
<NButton
v-if=
"hasExistingData"
type=
"error"
size=
"small"
class=
"clear-button"
@
click=
"clearAllTestData"
>
<template
#
icon
>
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 24 24"
width=
"16"
height=
"16"
>
<path
fill=
"currentColor"
d=
"M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z"
/>
</svg>
</
template
>
清空数据
</NButton>
</div>
<NFormItem
label=
"测试样品编号"
class=
"last-form-item"
required
>
<NInput
v-model:value=
"sampleCode"
placeholder=
"请输入测试样品编号"
></NInput>
<!-- 简洁的状态提示 -->
<div
v-if=
"hasExistingData"
class=
"status-tip"
>
<NIcon
size=
"14"
color=
"#f0a020"
>
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 24 24"
>
<path
fill=
"currentColor"
d=
"M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
/>
</svg>
</NIcon>
<span>
已有 {{ testData.length }} 条比对测试数据
</span>
</div>
</div>
</NFormItem>
<NDivider></NDivider>
...
...
@@ -305,50 +800,26 @@ function checkTestResult(row: TestDataItem) {
<span>
比对测试数据
</span>
</div>
<NDataTable
:columns=
"columns"
:data=
"testData"
:bordered=
"false"
:max-height=
"300"
:loading=
"loading"
:row-key=
"row => row.key"
children-key=
"children"
:indent=
"20"
:default-expand-all=
"true"
striped
></NDataTable>
<div
class=
"tree-container"
>
<NTree
:data=
"treeData"
block-line
:render-label=
"renderTreeLabel"
virtual-scroll
style=
"max-height: 300px; min-height: 200px;"
default-expand-all
/>
<div
v-if=
"loading"
class=
"loading-overlay"
>
<NSpin
size=
"medium"
/>
</div>
</div>
<div
class=
"result-summary"
>
<span
class=
"result-label"
>
总体结果:
</span>
<NTag
type=
"warning"
>
未完成
</NTag>
</div>
<NDivider></NDivider>
<div
class=
"section-title"
>
<svg
class=
"section-icon"
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 24 24"
width=
"24"
height=
"24"
>
<path
fill=
"currentColor"
d=
"M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z"
></path>
</svg>
<span>
测试结论
</span>
</div>
<NFormItem
label=
"测试结论"
>
<NRadioGroup
v-model:value=
"testResult"
>
<NSpace>
<NRadio
value=
"pass"
>
通过
</NRadio>
<NRadio
value=
"fail"
>
不通过
</NRadio>
<NRadio
value=
"partial"
>
部分通过
</NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem
label=
"测试意见"
class=
"last-form-item"
>
<NInput
v-model:value=
"testComment"
type=
"textarea"
placeholder=
"请输入测试意见"
:rows=
"4"
></NInput>
</NFormItem>
<!-- 测试方案明细选择弹窗 -->
<NModal
v-model:show=
"showDetailSelector"
...
...
@@ -419,4 +890,176 @@ function checkTestResult(row: TestDataItem) {
:deep
(
.last-form-item
.n-form-item-feedback-wrapper
)
{
display
:
none
;
}
.test-plan-selector
{
width
:
100%
;
}
.selector-row
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
width
:
100%
;
}
.clear-button
{
flex-shrink
:
0
;
border-radius
:
6px
;
height
:
34px
;
padding
:
0
12px
;
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
}
.status-tip
{
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
margin-top
:
6px
;
font-size
:
12px
;
color
:
#f0a020
;
padding
:
4px
0
;
}
.status-tip
span
{
line-height
:
1
;
}
/* 禁用状态下的选择器样式 */
:deep
(
.n-base-selection--disabled
)
{
background-color
:
#fef2f2
;
border-color
:
#fecaca
;
}
:deep
(
.n-base-selection--disabled
.n-base-selection-placeholder
)
{
color
:
#dc2626
!important
;
font-weight
:
500
;
}
/* 树形容器样式 */
.tree-container
{
position
:
relative
;
border
:
1px
solid
#e8e8e8
;
border-radius
:
6px
;
overflow
:
hidden
;
background
:
#fff
;
}
.loading-overlay
{
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
rgba
(
255
,
255
,
255
,
0.8
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
z-index
:
10
;
}
/* 树节点样式 */
:deep
(
.n-tree-node-content
)
{
padding
:
12px
16px
!important
;
}
/* 自定义树节点内容样式 */
.tree-test-node-content
{
width
:
100%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
min-height
:
40px
;
gap
:
16px
;
}
.tree-test-node-left
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
flex
:
1
;
min-width
:
0
;
/* 允许内容收缩 */
}
.tree-test-node-right
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
flex-shrink
:
0
;
/* 防止右侧内容被压缩 */
}
.tree-test-node-title
{
font-weight
:
600
;
color
:
#333
;
font-size
:
14px
;
min-width
:
120px
;
flex-shrink
:
0
;
}
.tree-node-separator
{
color
:
#ccc
;
font-size
:
12px
;
margin
:
0
2px
;
flex-shrink
:
0
;
}
.tree-test-node-detail
{
font-size
:
12px
;
color
:
#666
;
background
:
#f5f5f5
;
padding
:
4px
8px
;
border-radius
:
4px
;
white-space
:
nowrap
;
flex-shrink
:
0
;
}
.tree-test-node-input-group
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
flex-shrink
:
0
;
}
.tree-test-input-label
{
font-size
:
12px
;
color
:
#666
;
white-space
:
nowrap
;
}
.tree-test-node-status
{
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
flex-shrink
:
0
;
}
.tree-node-icon
{
font-size
:
14px
;
display
:
inline-block
;
vertical-align
:
middle
;
}
/* 父级节点样式 */
:deep
(
.n-tree-node
[
aria-level
=
"1"
])
{
background-color
:
#f8f9fa
!important
;
}
:deep
(
.n-tree-node
[
aria-level
=
"1"
]
:hover
)
{
background-color
:
#e9ecef
!important
;
}
/* 子级节点样式 */
:deep
(
.n-tree-node
[
aria-level
=
"2"
])
{
background-color
:
#fff
!important
;
}
:deep
(
.n-tree-node
[
aria-level
=
"2"
]
:hover
)
{
background-color
:
#f5f5f5
!important
;
}
/* 暗色主题适配 */
:deep
(
.n-tree.n-tree--block-line
.n-tree-node
)
{
border-bottom
:
1px
solid
#e8e8e8
;
}
</
style
>
src/views/Lac/codes/fileListView.vue
View file @
54b22571
...
...
@@ -119,7 +119,6 @@
<
script
setup
lang=
"ts"
>
import
{
h
,
ref
,
computed
,
resolveComponent
,
watch
,
withDefaults
,
onBeforeUnmount
}
from
'vue'
;
// import type { DataTableColumns } from 'naive-ui';
interface
FileItem
{
Kvid
:
string
;
...
...
src/views/Lac/codes/fileReview.vue
View file @
54b22571
...
...
@@ -203,11 +203,19 @@ const isCurrentRowReadonly = computed(() => {
* @usage 在组件初始化和激活时调用,用于填充文件类型下拉选择器
*/
async
function
fetchTitleOptions
()
{
// 如果已有数据则不重复加载
if
(
titleOptions
.
value
.
length
>
0
)
{
return
;
}
try
{
loading
.
value
=
true
;
const
response
=
await
axios
.
post
(
'/Restful/Kivii.Standards.Entities.Standard/Query.json?Type=文件审核'
);
const
response
=
await
axios
.
get
(
'/Restful/Kivii.Standards.Entities.Standard/Query.json'
,
{
params
:
{
Type
:
'文件审核'
}
});
if
(
response
.
data
&&
response
.
data
.
Results
)
{
titleOptions
.
value
=
response
.
data
.
Results
.
map
((
item
:
any
,
index
:
number
)
=>
({
label
:
item
.
Title
,
...
...
@@ -354,8 +362,6 @@ watch(
onMounted
(()
=>
{
if
(
props
.
active
)
{
fetchTitleOptions
();
if
(
props
.
item
&&
props
.
item
.
Kvid
&&
props
.
item
.
Kvid
!==
detailedData
.
Kvid
)
{
fetchDataByReportKvid
(
props
.
item
.
Kvid
);
}
...
...
@@ -365,9 +371,7 @@ onMounted(() => {
watch
(
()
=>
props
.
active
,
(
newActive
)
=>
{
if
(
newActive
&&
titleOptions
.
value
.
length
===
0
)
{
fetchTitleOptions
();
if
(
newActive
)
{
if
(
props
.
item
&&
props
.
item
.
Kvid
&&
props
.
item
.
Kvid
!==
detailedData
.
Kvid
)
{
fetchDataByReportKvid
(
props
.
item
.
Kvid
);
}
...
...
@@ -719,6 +723,7 @@ async function handleFileListClosed(files: any[]) {
label-field=
"label"
value-field=
"value"
@
update:value=
"handleTitleChange"
@
focus=
"fetchTitleOptions"
class=
"selector-input"
></NSelect>
...
...
src/views/Lac/codes/mainSupplier.vue
View file @
54b22571
...
...
@@ -347,8 +347,7 @@ function saveApplication(submit = true) {
<!-- 步骤3: 比对测试选择 -->
<ComparisonTest
v-if=
"visitedSteps.has(2) && currentStep === 2"
:test-items=
"testItems"
:save-as-draft=
"false"
:item=
"formModel"
:active=
"currentStep === 2"
@
update:test-items=
"newItems => (testItems = newItems)"
></ComparisonTest>
...
...
@@ -357,7 +356,7 @@ function saveApplication(submit = true) {
<FieldReviewPlanning v-show="currentStep === 3"></FieldReviewPlanning>
步骤5: 现场审核执行
<FieldReviewExecution v-show="currentStep === 4"></FieldReviewExecution>
<FieldReviewExecution v-show="currentStep === 4"></FieldReviewExecution>
-->
</NForm>
<NDivider></NDivider>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment