Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
Z
zrch-risk-39
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
Administrator
zrch-risk-39
Commits
8c864de6
提交
8c864de6
authored
2月 04, 2026
作者:
kxjia
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
完善验证
上级
db81d638
隐藏空白字符变更
内嵌
并排
正在显示
4 个修改的文件
包含
1207 行增加
和
898 行删除
+1207
-898
Tb2.vue
...isk-client-39/src/views/baosong/report/components/Tb2.vue
+318
-202
Tb3.vue
...isk-client-39/src/views/baosong/report/components/Tb3.vue
+318
-176
Tb4.vue
...isk-client-39/src/views/baosong/report/components/Tb4.vue
+211
-260
Tb5.vue
...isk-client-39/src/views/baosong/report/components/Tb5.vue
+360
-260
没有找到文件。
zrch-risk-client-39/src/views/baosong/report/components/Tb2.vue
浏览文件 @
8c864de6
<
template
>
<div
class=
"bank-report-table"
style=
"height: 80vh;"
@
click=
"close
Tooltip
"
>
<div
class=
"bank-report-table"
style=
"height: 80vh;"
@
click=
"close
AllTooltips
"
>
<!-- 加载遮罩层 -->
<div
v-if=
"loading"
class=
"loading-overlay"
>
<div
class=
"loading-spinner"
>
...
...
@@ -16,55 +16,12 @@
</div>
</
template
>
<
template
#
tools
>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"handleOpenHistoryDrawer()"
:disabled=
"loading"
>
近5年数据填报
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"check
Data()"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"saveBatch"
>
保存
</vxe-button>
</
template
>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"handleOpenHistoryDrawer()"
:disabled=
"loading"
>
近5年数据填报
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"validate
Data()"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"saveBatch"
>
保存
</vxe-button>
</
template
>
</vxe-toolbar>
<!-- 校验结果抽屉 -->
<vxe-drawer
v-model=
"drawerVisible"
placement=
"right"
@
show=
"handleValidationDrawerShow"
title=
"校验结果"
width=
"40%"
:footer=
"{ show: true }"
>
<
template
#
default
>
<div
class=
"validation-results"
>
<div
class=
"result-summary"
>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
校验字段数:
</span>
<span
class=
"summary-value"
>
{{
validationResultsList
.
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
通过:
</span>
<span
class=
"summary-value success"
>
{{
validationResultsList
.
filter
(
item
=>
item
.
isValid
).
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
失败:
</span>
<span
class=
"summary-value error"
>
{{
validationResultsList
.
filter
(
item
=>
!
item
.
isValid
).
length
}}
</span>
</div>
</div>
<div
class=
"results-list"
>
<div
v-for=
"(result, index) in validationResultsList"
:key=
"index"
class=
"result-item"
:class=
"
{ success: result.isValid, error: !result.isValid }"
@click="handleValidationResultClick(result)"
>
<div
class=
"result-field"
>
{{
result
.
field
}}
</div>
<div
class=
"result-desc"
>
{{
result
.
description
}}
</div>
<div
class=
"result-value"
>
值:
{{
result
.
fieldValue
||
'空'
}}
</div>
<div
v-if=
"!result.isValid"
class=
"result-error"
>
失败原因:
{{
result
.
description
}}
</div>
</div>
</div>
</div>
</
template
>
</vxe-drawer>
<vxe-table
border
ref=
"tableRef"
...
...
@@ -132,27 +89,57 @@
</vxe-radio-group>
</span>
<
template
v-else
>
<vxe-input
:type=
"item.type"
v-model=
"formData[getFieldKey(row.code, item.field)]"
size=
"mini"
class=
"table-input"
:style=
"
{width:item.width}"
:disabled="!item.hasRight"
:data-field="getFieldKey(row.code, item.field)"
>
</vxe-input>
<!-- 帮助图标 -->
<span
v-if=
"item.hasValidFormula"
class=
"validation-help-icon"
@
mouseenter=
"(event) => showValidationHelp(item, event)"
@
mouseleave=
"closeTooltip"
@
click=
"(event) => showValidationHelp(item, event)"
>
?
</span>
<div
class=
"input-wrapper"
>
<vxe-input
:type=
"item.type"
v-model=
"formData[getFieldKey(row.code, item.field)]"
size=
"mini"
class=
"table-input"
:style=
"
{width:item.width}"
:disabled="!item.hasRight"
:status="getInputStatus(row.code, item.field)"
@blur="handleInputBlur(row.code, item.field, item.matchedFormula?.formula)"
/>
<span
class=
"unit"
>
{{
item
.
unit
}}
</span>
<span
v-if=
"showHelpIcon(row.code, item.field, item)"
class=
"help-icon"
@
click
.
stop=
"toggleTooltip(row.code, item.field)"
title=
"点击查看校验规则"
>
?
</span>
<span
v-if=
"getInputStatus(row.code, item.field) === 'error'"
class=
"error-icon"
@
click
.
stop=
"toggleErrorTooltip(row.code, item.field)"
>
<i
class=
"vxe-icon-error"
>
✗
</i>
</span>
<div
v-if=
"showTooltip && hoveredKey === getFieldKey(row.code, item.field)"
class=
"tooltip"
@
click
.
stop
>
{{
item
.
matchedFormula
?.
des
||
'暂无校验规则'
}}
</div>
<div
v-if=
"showErrorTooltip && hoveredErrorKey === getFieldKey(row.code, item.field)"
class=
"error-tooltip"
@
click
.
stop
>
{{
getErrorMessage
(
row
.
code
,
item
.
field
)
}}
<br>
<span
style=
"color: #ffcccc;"
>
公式:
{{
item
.
matchedFormula
?.
formula
}}
</span>
</div>
</div>
</
template
>
<span
v-if=
"item.unit"
class=
"unit"
>
{{ item.unit }}
</span>
</template>
</template>
</div>
...
...
@@ -163,23 +150,23 @@
<!-- 历史填报检查组件 -->
<HistoryFillCheck
ref=
"historyFillCheckRef"
v-model=
"historyDrawerVisible"
@
closeDrawer=
"closeHistoryDrawer"
/>
<!-- 验证公式帮助提示 -->
<div
v-if=
"validationTooltipVisible"
class=
"validation-tooltip"
:style=
"{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }"
>
<pre
class=
"tooltip-content"
>
{{ validationTooltipContent }}
</pre>
</div>
<!-- 校验抽屉 -->
<ValidationDrawer
ref=
"validationDrawerRef"
v-model=
"drawerVisible"
:tableFormData=
"tableFormData"
@
validationResultClick=
"handleValidationResultClick"
/>
</div>
</template>
<
script
lang=
"ts"
setup
>
import
MultiColumnTable
from
'../tableComponents/MultiColumnTable.vue'
import
HistoryFillCheck
from
'./check/historyFillCheck.vue'
import
ValidationDrawer
from
'./check/ValidationDrawer.vue'
import
{
tableFormData
}
from
'../../data/tb2.data'
import
{
ref
,
reactive
,
nextTick
,
onMounted
}
from
'vue'
import
{
ref
,
reactive
,
nextTick
,
onMounted
,
computed
}
from
'vue'
import
{
VxeUI
}
from
'vxe-table'
import
{
batchSaveOrUpdateBeforeDelete
,
queryRecord
}
from
'../../record/BaosongTaskRecord.api'
import
{
queryAllTplItemForUser
,
findUserRightForTplItem
}
from
'../../alloc/BaosongTaskAlloc.api'
...
...
@@ -207,16 +194,36 @@ const formData = reactive<Record<string, any>>({})
const
formValues
=
ref
<
FormData
[]
>
([])
const
childMultiTableRefs
=
ref
<
Record
<
string
,
any
>>
({})
const
validationDrawerRef
=
ref
<
any
>
(
null
)
const
showTooltip
=
ref
(
false
)
const
hoveredKey
=
ref
(
''
)
const
showErrorTooltip
=
ref
(
false
)
const
hoveredErrorKey
=
ref
(
''
)
const
inputErrors
=
ref
<
Record
<
string
,
any
>>
({})
const
isInitialized
=
ref
(
false
)
const
fieldKeys
=
computed
(()
=>
Object
.
keys
(
formData
))
interface
FormulaItem
{
formula
:
string
des
:
string
[
key
:
string
]:
any
}
interface
InputError
{
message
:
string
formula
:
string
error
?:
string
}
// 权限相关状态
const
userAllocItems
=
ref
<
string
[]
>
([])
const
validFormula
=
ref
<
any
[]
>
([])
const
validationResultsList
=
ref
<
any
[]
>
([])
const
drawerVisible
=
ref
(
false
)
const
historyDrawerVisible
=
ref
(
false
)
const
historyFillCheckRef
=
ref
<
any
>
(
null
)
const
validationTooltipVisible
=
ref
(
false
)
const
validationTooltipPosition
=
ref
({
left
:
0
,
top
:
0
})
const
validationTooltipContent
=
ref
(
''
)
const
queryParam
=
ref
({
taskId
:
-
1
,
...
...
@@ -243,8 +250,9 @@ onMounted(async () => {
queryParam
.
value
.
tplCode
=
String
(
route
.
query
.
tplCode
)
}
// 获取权限和验证公式
try
{
loading
.
value
=
true
userAllocItems
.
value
=
await
findUserRightForTplItem
({
tplid
:
queryParam
.
value
.
tplId
,
taskid
:
queryParam
.
value
.
taskId
...
...
@@ -253,13 +261,25 @@ onMounted(async () => {
validFormula
.
value
=
await
getTblvalidFormula
({
tplid
:
queryParam
.
value
.
tplId
,
})
await
setTplItemMap
()
setFormItemRight
()
await
setData
()
isInitialized
.
value
=
true
setTimeout
(()
=>
{
refreshHelpIcons
()
},
300
)
}
catch
(
error
)
{
console
.
error
(
'获取权限或验证公式失败:'
,
error
)
VxeUI
.
modal
.
message
({
content
:
`初始化页面失败:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)}
`
,
status
:
'error'
});
}
finally
{
loading
.
value
=
false
}
await
setTplItemMap
()
setFormItemRight
()
await
setData
()
})
const
getFieldKey
=
(
pcode
:
string
|
undefined
,
field
:
string
):
string
=>
{
...
...
@@ -455,124 +475,177 @@ const setFormItemRight = () => {
});
}
// 校验数据
const
checkData
=
()
=>
{
const
validateData
=
()
=>
{
drawerVisible
.
value
=
true
validationDrawerRef
.
value
.
setValidateData
(
validFormula
.
value
,
formData
)
}
// 校验结果抽屉显示时触发
const
handleValidationDrawerShow
=
()
=>
{
performValidation
()
}
// 执行校验
const
performValidation
=
()
=>
{
validationResultsList
.
value
=
[]
const
process
:
string
[]
=
[]
process
.
push
(
'开始校验'
)
process
.
push
(
`找到
${
validFormula
.
value
.
length
}
个验证公式`
)
validFormula
.
value
.
forEach
((
formulaItem
:
any
)
=>
{
process
.
push
(
`开始校验公式:
${
formulaItem
.
des
}
`
)
const
result
=
evaluateFormula
(
formulaItem
.
formula
,
formulaItem
.
des
,
process
)
if
(
result
)
{
validationResultsList
.
value
.
push
(
result
)
}
})
process
.
push
(
'校验完成'
)
const
handleInputBlur
=
(
rowCode
:
string
,
field
:
string
,
formula
?:
string
)
=>
{
if
(
!
formula
)
return
const
key
=
getFieldKey
(
rowCode
,
field
)
const
value
=
formData
[
key
]
if
(
!
value
||
value
===
''
)
{
delete
inputErrors
.
value
[
key
]
return
}
validateFieldFormula
(
rowCode
,
field
,
formula
)
}
// 解析和执行验证公式
const
evaluateFormula
=
(
formula
:
string
,
description
:
string
,
process
:
string
[]):
any
=>
{
const
validateFieldFormula
=
(
rowCode
:
string
,
field
:
string
,
formula
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
)
try
{
const
fieldMatch
=
formula
.
match
(
/
\[(\w
+
)\]
/
)
if
(
!
fieldMatch
)
{
process
.
push
(
`跳过: 无法解析公式字段
${
formula
}
`
)
return
null
const
expression
=
buildExpression
(
formula
,
rowCode
)
const
isValid
=
eval
(
expression
)
if
(
!
isValid
)
{
inputErrors
.
value
[
key
]
=
{
message
:
"公式校验失败,请检查填写内容"
,
formula
}
}
else
{
delete
inputErrors
.
value
[
key
]
}
const
fieldName
=
fieldMatch
[
1
]
const
row
=
tableFormData
.
find
(
r
=>
r
.
content
?.
some
((
c
:
any
)
=>
c
.
field
===
fieldName
))
if
(
!
row
)
{
process
.
push
(
`跳过: 未找到字段
${
fieldName
}
`
)
return
null
}
catch
(
error
:
any
)
{
inputErrors
.
value
[
key
]
=
{
message
:
"公式校验失败,请检查填写内容"
,
formula
,
error
:
error
.
message
}
}
}
const
fieldItem
=
row
.
content
.
find
((
c
:
any
)
=>
c
.
field
===
fieldName
)
if
(
!
fieldItem
)
{
process
.
push
(
`跳过: 未找到字段
${
fieldName
}
`
)
return
null
const
buildExpression
=
(
formula
:
string
,
rowCode
:
string
):
string
=>
{
const
fieldNames
=
formula
.
match
(
/
\b[
A-Za-z
][
A-Za-z0-9_
]
*
\b
/g
)
||
[]
const
uniqueFields
=
[...
new
Set
(
fieldNames
.
filter
(
f
=>
!
[
'and'
,
'or'
,
'not'
,
'equal'
,
'less'
,
'greater'
,
'if'
,
'else'
,
'true'
,
'false'
]
.
includes
(
f
.
toLowerCase
())
))]
uniqueFields
.
sort
((
a
,
b
)
=>
b
.
length
-
a
.
length
)
let
expression
=
formula
for
(
const
fieldName
of
uniqueFields
)
{
const
key
=
getFieldKey
(
rowCode
,
fieldName
)
const
value
=
formData
[
key
]
if
(
value
!==
undefined
)
{
expression
=
expression
.
replace
(
new
RegExp
(
`\\b
${
fieldName
}
\\b`
,
'g'
),
`Number(
${
value
}
)`
)
}
}
return
expression
}
const
strKey
=
`
${
row
.
code
}
_
${
fieldName
}
`
const
fieldValue
=
formData
[
strKey
]
// 检查是否有空字段规则
const
emptyFieldRule
=
validFormula
.
value
.
find
((
f
:
any
)
=>
f
.
formula
.
includes
(
fieldName
)
&&
f
.
des
.
includes
(
'空字段'
)
)
const
toggleTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
)
if
(
hoveredKey
.
value
===
key
)
{
showTooltip
.
value
=
false
hoveredKey
.
value
=
''
}
else
{
showTooltip
.
value
=
true
hoveredKey
.
value
=
key
}
}
if
(
emptyFieldRule
&&
(
!
fieldValue
||
fieldValue
===
''
))
{
process
.
push
(
`跳过: 字段
${
fieldName
}
为空,跳过空字段规则`
)
return
null
}
const
toggleErrorTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
)
if
(
hoveredErrorKey
.
value
===
key
)
{
showErrorTooltip
.
value
=
false
hoveredErrorKey
.
value
=
''
}
else
{
showErrorTooltip
.
value
=
true
hoveredErrorKey
.
value
=
key
}
}
const
expression
=
formula
.
replace
(
/
\[(\w
+
)\]
/g
,
(
_
,
field
)
=>
{
const
value
=
formData
[
`
${
row
.
code
}
_
${
field
}
`
]
return
value
!==
undefined
?
`Number(
${
value
}
)`
:
'0'
})
const
closeAllTooltips
=
()
=>
{
showTooltip
.
value
=
false
hoveredKey
.
value
=
''
showErrorTooltip
.
value
=
false
hoveredErrorKey
.
value
=
''
}
process
.
push
(
`执行公式:
${
expression
}
`
)
const
isValid
=
eval
(
expression
)
const
findEarliestFormulaForField
=
(
formulas
:
FormulaItem
[],
field
:
string
):
{
formula
:
FormulaItem
|
null
,
index
:
number
}
=>
{
let
result
=
{
formula
:
null
,
index
:
Infinity
}
formulas
?.
forEach
((
f
:
FormulaItem
)
=>
{
const
text
=
f
.
formula
||
''
const
escapedField
=
field
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
)
const
regex
=
new
RegExp
(
`\\b
${
escapedField
}
\\b(?!\\w)`
,
'g'
)
const
match
=
regex
.
exec
(
text
)
if
(
match
&&
match
.
index
<
result
.
index
)
{
result
=
{
formula
:
f
,
index
:
match
.
index
}
}
})
return
result
}
return
{
field
:
fieldName
,
description
:
description
,
formula
:
formula
,
isValid
:
isValid
,
fieldValue
:
fieldValue
,
rowCode
:
row
.
code
const
refreshHelpIcons
=
()
=>
{
console
.
log
(
'刷新帮助图标...'
)
tableFormData
.
forEach
((
row
:
any
)
=>
{
if
(
row
.
content
&&
Array
.
isArray
(
row
.
content
))
{
row
.
content
.
forEach
((
item
:
any
)
=>
{
if
(
item
.
field
)
{
const
key
=
getFieldKey
(
row
.
code
,
item
.
field
)
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
)
if
(
hasRight
)
{
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
item
.
field
)
item
.
hasValidFormula
=
!!
formulaResult
.
formula
item
.
matchedFormula
=
formulaResult
.
formula
||
null
}
else
{
item
.
hasValidFormula
=
false
item
.
matchedFormula
=
null
}
}
})
}
}
catch
(
error
)
{
process
.
push
(
`公式执行错误:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)}
`
)
return
null
}
})
nextTick
(()
=>
{
console
.
log
(
'帮助图标刷新完成'
)
})
}
// 处理校验结果点击,定位到表格
const
handleValidationResultClick
=
(
result
:
any
)
=>
{
const
row
=
tableFormData
.
find
(
r
=>
r
.
code
===
result
.
rowCode
)
if
(
row
)
{
const
fieldItem
=
row
.
content
.
find
((
c
:
any
)
=>
c
.
field
===
result
.
field
)
if
(
fieldItem
)
{
const
strKey
=
`
${
row
.
code
}
_
${
result
.
field
}
`
const
inputElement
=
document
.
querySelector
(
`[data-field="
${
strKey
}
"]`
)
as
HTMLElement
if
(
inputElement
)
{
inputElement
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'center'
})
inputElement
.
focus
()
inputElement
.
style
.
border
=
'2px solid #ff4d4f'
setTimeout
(()
=>
{
inputElement
.
style
.
border
=
''
},
2000
)
}
}
}
const
message
=
`字段
${
result
.
field
}
的校验结果:
${
result
.
isValid
?
'通过'
:
'失败'
}
`
const
status
=
result
.
isValid
?
'success'
:
'error'
VxeUI
.
modal
.
message
({
content
:
message
,
status
})
}
// 显示验证公式帮助
const
showValidationHelp
=
(
formulaItem
:
any
,
event
:
MouseEvent
)
=>
{
const
rect
=
(
event
.
target
as
HTMLElement
).
getBoundingClientRect
()
validationTooltipPosition
.
value
=
{
left
:
rect
.
left
+
rect
.
width
/
2
,
top
:
rect
.
top
-
10
const
showHelpIcon
=
(
rowCode
:
string
,
field
:
string
,
item
:
any
):
boolean
=>
{
if
(
item
.
hasValidFormula
!==
undefined
)
{
return
item
.
hasValidFormula
===
true
}
validationTooltipContent
.
value
=
`验证公式:
${
formulaItem
.
formula
}
\n说明:
${
formulaItem
.
des
}
`
validationTooltipVisible
.
value
=
true
const
key
=
getFieldKey
(
rowCode
,
field
)
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
)
if
(
!
hasRight
)
return
false
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
field
)
return
!!
formulaResult
.
formula
}
// 关闭验证公式帮助
const
closeTooltip
=
()
=>
{
validationTooltipVisible
.
value
=
false
const
getInputStatus
=
(
rowCode
:
string
,
itemField
:
string
):
'default'
|
'error'
=>
{
const
key
=
getFieldKey
(
rowCode
,
itemField
)
return
inputErrors
.
value
[
key
]
?
'error'
:
''
}
const
getErrorMessage
=
(
rowCode
:
string
,
itemField
:
string
):
string
=>
{
const
key
=
getFieldKey
(
rowCode
,
itemField
)
return
inputErrors
.
value
[
key
]?.
message
||
''
}
// 打开历史填报抽屉
...
...
@@ -881,39 +954,82 @@ const closeHistoryDrawer = () => {
margin-top: 4px;
}
/* 验证公式帮助图标 */
.validation-help-icon {
.input-wrapper {
position: relative;
display: inline-block;
}
.help-icon {
margin-left: 5px;
cursor: pointer;
color: #1890ff;
font-weight: bold;
font-size: 14px;
display: inline-block;
width:
16
px;
height:
16
px;
line-height:
16
px;
width:
20
px;
height:
20
px;
line-height:
20
px;
text-align: center;
background: #1890ff;
color: white;
background: #e6f7ff;
border-radius: 50%;
font-size: 12px;
margin-left: 4px;
transition: all 0.3s;
&:hover {
background: #bae7ff;
}
}
.error-icon {
margin-left: 5px;
cursor: pointer;
vertical-align: middle;
color: #ff4d4f;
font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
/* 验证公式帮助提示 */
.
validation
-tooltip {
position:
fixed
;
background: #333
;
color: white
;
padding: 10px 1
5px;
border-radius: 4
px;
.tooltip,
.
error
-tooltip {
position:
absolute
;
top: 100%
;
left: 0
;
margin-top:
5px;
padding: 10
px;
font-size: 12px;
z-index: 9999
;
transform: translateX(-50%)
;
border-radius: 4px
;
z-index: 1000
;
white-space: pre-wrap;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.
2
);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.
15
);
}
.tooltip-content {
margin: 0;
.tooltip {
background: #333;
color: #fff;
&::before {
content: '';
position: absolute;
bottom: 100%;
left: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #333 transparent;
}
}
.error-tooltip {
background: #8b0000;
color: #fff;
border: 1px solid #ff4d4f;
}
</
style
>
zrch-risk-client-39/src/views/baosong/report/components/Tb3.vue
浏览文件 @
8c864de6
<
template
>
<div
class=
"bank-report-table"
@
click=
"close
Tooltip
"
>
<div
class=
"bank-report-table"
@
click=
"close
AllTooltips
"
>
<!-- 加载遮罩层 -->
<div
v-if=
"loading"
class=
"loading-overlay"
>
<div
class=
"loading-spinner"
>
...
...
@@ -17,7 +17,7 @@
</
template
>
<
template
#
tools
>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"handleOpenHistoryDrawer()"
:disabled=
"loading"
>
近5年数据填报
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"validate
AndShowResults
()"
:disabled=
"loading"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"validate
Data
()"
:disabled=
"loading"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"saveBatch()"
:disabled=
"loading"
>
保存
</vxe-button>
</
template
>
</vxe-toolbar>
...
...
@@ -142,16 +142,46 @@
class=
"table-input"
:style=
"
{width:item.width}"
:status="getInputStatus(row.code, item.field)"
@
input="handleInputChange(row.code, item.field
)"
@
blur="handleInputBlur(row.code, item.field, item.matchedFormula?.formula
)"
>
</vxe-input>
<span
class=
"unit"
>
{{
item
.
unit
}}
</span>
<span
v-if=
"item.hasValidFormula"
class=
"help-icon"
@
click
.
stop=
"toggleTooltip(row.code, item.field)"
>
?
</span>
<div
v-if=
"getInputStatus(row.code, item.field) === 'error'"
class=
"error-tip"
>
{{
getErrorMessage
(
row
.
code
,
item
.
field
)
}}
<span
v-if=
"showHelpIcon(row.code, item.field, item)"
class=
"help-icon"
@
click
.
stop=
"toggleTooltip(row.code, item.field)"
title=
"点击查看校验规则"
>
?
</span>
<span
v-if=
"getInputStatus(row.code, item.field) === 'error'"
class=
"error-icon"
@
click
.
stop=
"toggleErrorTooltip(row.code, item.field)"
>
<i
class=
"vxe-icon-error"
>
✗
</i>
</span>
<div
v-if=
"showTooltip && hoveredKey === row.code +'_'+ item.field"
class=
"tooltip"
@
click
.
stop
>
{{
item
.
matchedFormula
?.
des
||
'暂无校验规则'
}}
</div>
<div
v-if=
"showTooltip && hoveredKey === row.code +'_'+ item.field"
class=
"tooltip"
@
click
.
stop
>
{{
getValidationRule
(
row
.
code
,
item
.
field
)
}}
<div
v-if=
"showErrorTooltip && hoveredErrorKey === row.code +'_'+ item.field"
class=
"error-tooltip"
@
click
.
stop
>
{{
getErrorMessage
(
row
.
code
,
item
.
field
)
}}
<br>
<span
style=
"color: #ffcccc;"
>
公式:
{{
item
.
matchedFormula
?.
formula
}}
</span>
</div>
</div>
</
template
>
...
...
@@ -168,74 +198,16 @@
</vxe-column>
</vxe-table>
<!-- 校验结果抽屉 -->
<vxe-drawer
v-model=
"drawerVisible"
placement=
"right"
@
show=
"onDrawerShow"
title=
"校验结果"
width=
"40%"
:footer=
"{ show: true }"
>
<
template
#
default
>
<div
class=
"validation-results"
>
<div
class=
"result-summary"
>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
校验字段数:
</span>
<span
class=
"summary-value"
>
{{
validationResultsList
.
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
通过:
</span>
<span
class=
"summary-value success"
>
{{
validationResultsList
.
filter
(
item
=>
item
.
isValid
).
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
失败:
</span>
<span
class=
"summary-value error"
>
{{
validationResultsList
.
filter
(
item
=>
!
item
.
isValid
).
length
}}
</span>
</div>
</div>
<!-- 历史填报检查组件 -->
<HistoryFillCheck
ref=
"historyFillCheckRef"
v-model=
"historyDrawerVisible"
@
closeDrawer=
"closeHistoryDrawer"
/>
<div
class=
"result-list"
>
<div
v-for=
"(item, index) in validationResultsList"
:key=
"index"
class=
"result-item"
:class=
"
{ success: item.isValid, error: !item.isValid }"
>
<div
class=
"result-item-header"
>
<span
class=
"result-item-name"
>
{{
item
.
fieldTitle
}}
【
{{
item
.
fieldName
}}
】
</span>
<span
class=
"result-item-status"
:class=
"
{ success: item.isValid, error: !item.isValid }">
{{
item
.
isValid
?
'通过'
:
'失败'
}}
</span>
</div>
<div
class=
"result-item-content"
>
<div
class=
"result-item-formula"
>
<span
class=
"formula-label"
>
验证公式:
</span>
<span
class=
"formula-value"
>
{{
item
.
formula
}}
</span>
</div>
<div
class=
"result-item-result"
>
<span
class=
"result-label"
>
验证结果:
</span>
<span
class=
"result-value"
>
{{
item
.
resultMessage
}}
</span>
</div>
<div
class=
"result-item-values"
v-if=
"item.fieldValues"
>
<span
class=
"values-label"
>
字段值:
</span>
<pre
class=
"values-value"
>
{{
formatJSON
(
item
.
fieldValues
)
}}
</pre>
</div>
<div
class=
"result-item-process"
v-if=
"item.process"
>
<span
class=
"process-label"
>
校验过程:
</span>
<div
class=
"process-content"
>
<div
v-for=
"(step, stepIndex) in item.process"
:key=
"stepIndex"
class=
"process-step"
>
<span
class=
"step-number"
>
{{
stepIndex
+
1
}}
</span>
<span
class=
"step-text"
>
{{
step
}}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</
template
>
</vxe-drawer>
<HistoryFillCheck
ref=
"historyFillCheckRef"
v-model=
"historyDrawerVisible"
@
closeDrawer=
"closeHistoryDrawer"
/>
<!-- 校验抽屉 -->
<ValidationDrawer
ref=
"validationDrawerRef"
v-model=
"drawerVisible"
:tableFormData=
"tableFormData"
@
validationResultClick=
"handleValidationResultClick"
/>
</div>
</template>
...
...
@@ -243,9 +215,10 @@
import
MultiColumnTable
from
'../tableComponents/MultiColumnTable.vue'
import
AttachTable
from
'../tableComponents/AttachTable.vue'
import
HistoryFillCheck
from
'./check/HistoryFillCheck.vue'
import
ValidationDrawer
from
'./check/ValidationDrawer.vue'
import
{
tableFormData
}
from
'../../data/tb3.data'
;
import
{
ref
,
reactive
,
nextTick
,
onMounted
,
toRaw
}
from
'vue'
import
{
ref
,
reactive
,
nextTick
,
onMounted
,
toRaw
,
computed
,
}
from
'vue'
import
{
VxeUI
,
VxeTablePropTypes
,
VxeToolbarPropTypes
}
from
'vxe-table'
import
{
batchSaveOrUpdate
,
queryRecord
,
batchSaveOrUpdateBeforeDelete
}
from
'../../record/BaosongTaskRecord.api'
import
{
findUserRightForTplItem
}
from
'../../alloc/BaosongTaskAlloc.api'
...
...
@@ -255,6 +228,7 @@ import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import
{
useRoute
}
from
'vue-router'
;
const
historyFillCheckRef
=
ref
();
const
validationDrawerRef
=
ref
();
const
historyDrawerVisible
=
ref
(
false
);
const
route
=
useRoute
();
...
...
@@ -269,11 +243,46 @@ const formData = reactive({});
const
formValues
=
ref
<
FormData
[]
>
([])
const
showTooltip
=
ref
(
false
)
const
hoveredKey
=
ref
(
''
)
const
showErrorTooltip
=
ref
(
false
)
const
hoveredErrorKey
=
ref
(
''
)
const
isInitialized
=
ref
(
false
)
const
fieldKeys
=
computed
(()
=>
Object
.
keys
(
formData
));
const
getFieldKey
=
(
rowCode
:
string
,
field
:
string
):
string
=>
{
return
`
${
rowCode
}
_
${
field
}
`
;
};
const
showHelpIcon
=
(
rowCode
:
string
,
field
:
string
,
item
:
any
):
boolean
=>
{
if
(
item
.
hasValidFormula
!==
undefined
)
{
return
item
.
hasValidFormula
===
true
;
}
const
key
=
getFieldKey
(
rowCode
,
field
);
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
!
hasRight
)
return
false
;
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
field
);
return
!!
formulaResult
.
formula
;
};
// 校验结果抽屉相关
const
drawerVisible
=
ref
(
false
)
const
validationResultsList
=
ref
<
any
[]
>
([])
interface
FormulaItem
{
formula
:
string
;
des
:
string
;
[
key
:
string
]:
any
;
}
interface
InputError
{
message
:
string
;
formula
:
string
;
error
?:
string
;
}
interface
ValidationResult
{
fieldTitle
:
string
fieldName
:
string
...
...
@@ -335,7 +344,14 @@ onMounted(async ()=>{
await
setFormItemRight
();
await
setTplItemMap
()
await
setData
();
await
validateAllInputs
();
// 标记初始化完成
isInitialized
.
value
=
true
// 延迟刷新一次,确保问号图标显示
setTimeout
(()
=>
{
refreshHelpIcons
()
},
300
)
}
catch
(
error
)
{
VxeUI
.
modal
.
message
({
content
:
`初始化页面失败:
${
error
.
message
}
`
,
...
...
@@ -605,6 +621,116 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([
{
row
:
9
,
col
:
3
,
rowspan
:
1
,
colspan
:
2
},
])
const
refreshHelpIcons
=
()
=>
{
console
.
log
(
'刷新帮助图标...'
);
tableFormData
.
forEach
((
row
:
any
)
=>
{
if
(
row
.
content
&&
Array
.
isArray
(
row
.
content
))
{
row
.
content
.
forEach
((
item
:
any
)
=>
{
if
(
item
.
field
)
{
const
key
=
getFieldKey
(
row
.
code
,
item
.
field
);
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
hasRight
)
{
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
item
.
field
);
item
.
hasValidFormula
=
!!
formulaResult
.
formula
;
item
.
matchedFormula
=
formulaResult
.
formula
||
null
;
}
else
{
item
.
hasValidFormula
=
false
;
item
.
matchedFormula
=
null
;
}
}
});
}
});
nextTick
(()
=>
{
console
.
log
(
'帮助图标刷新完成'
);
});
};
const
findEarliestFormulaForField
=
(
formulas
:
FormulaItem
[],
field
:
string
):
{
formula
:
FormulaItem
|
null
,
index
:
number
}
=>
{
let
result
=
{
formula
:
null
,
index
:
Infinity
};
formulas
?.
forEach
((
f
:
FormulaItem
)
=>
{
const
text
=
f
.
formula
||
''
;
const
escapedField
=
field
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
regex
=
new
RegExp
(
`\\b
${
escapedField
}
\\b(?!\\w)`
,
'g'
);
const
match
=
regex
.
exec
(
text
);
if
(
match
&&
match
.
index
<
result
.
index
)
{
result
=
{
formula
:
f
,
index
:
match
.
index
};
}
});
return
result
;
};
const
validateData
=
()
=>
{
drawerVisible
.
value
=
true
;
validationDrawerRef
.
value
.
setValidateData
(
validFormula
.
value
,
formData
);
};
const
handleInputBlur
=
(
rowCode
:
string
,
field
:
string
,
formula
?:
string
)
=>
{
if
(
!
formula
)
return
;
const
key
=
getFieldKey
(
rowCode
,
field
);
const
value
=
formData
[
key
];
if
(
!
value
||
value
===
''
)
{
delete
validationResults
.
value
[
key
];
delete
validationMessages
.
value
[
key
];
return
;
}
validateFieldFormula
(
rowCode
,
field
,
formula
);
};
const
validateFieldFormula
=
(
rowCode
:
string
,
field
:
string
,
formula
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
try
{
const
expression
=
buildExpression
(
formula
,
rowCode
);
const
isValid
=
eval
(
expression
);
if
(
!
isValid
)
{
validationResults
.
value
[
key
]
=
'error'
;
validationMessages
.
value
[
key
]
=
"公式校验失败,请检查填写内容"
;
}
else
{
delete
validationResults
.
value
[
key
];
delete
validationMessages
.
value
[
key
];
}
}
catch
(
error
:
any
)
{
validationResults
.
value
[
key
]
=
'error'
;
validationMessages
.
value
[
key
]
=
"公式校验失败,请检查填写内容"
;
}
};
const
buildExpression
=
(
formula
:
string
,
rowCode
:
string
):
string
=>
{
const
fieldNames
=
formula
.
match
(
/
\b[
A-Za-z
][
A-Za-z0-9_
]
*
\b
/g
)
||
[];
const
uniqueFields
=
[...
new
Set
(
fieldNames
.
filter
(
f
=>
!
[
'and'
,
'or'
,
'not'
,
'equal'
,
'less'
,
'greater'
,
'if'
,
'else'
,
'true'
,
'false'
]
.
includes
(
f
.
toLowerCase
())
))];
uniqueFields
.
sort
((
a
,
b
)
=>
b
.
length
-
a
.
length
);
let
expression
=
formula
;
for
(
const
fieldName
of
uniqueFields
)
{
const
key
=
getFieldKey
(
rowCode
,
fieldName
);
const
value
=
formData
[
key
];
if
(
value
!==
undefined
)
{
expression
=
expression
.
replace
(
new
RegExp
(
`\\b
${
fieldName
}
\\b`
,
'g'
),
`Number(
${
value
}
)`
);
}
}
return
expression
;
};
const
handleInputChange
=
(
rowCode
:
string
,
itemField
:
string
)
=>
{
const
key
=
`
${
rowCode
}
_
${
itemField
}
`
validateInput
(
key
)
...
...
@@ -968,15 +1094,39 @@ const validateAllInputs = async () => {
}
}
const
closeHistoryDrawer
=
()
=>
{
historyFillCheckRef
.
value
.
closeDrawer
();
historyDrawerVisible
.
value
=
false
;
}
const
toggleErrorTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
if
(
hoveredErrorKey
.
value
===
key
)
{
showErrorTooltip
.
value
=
false
;
hoveredErrorKey
.
value
=
''
;
}
else
{
showErrorTooltip
.
value
=
true
;
hoveredErrorKey
.
value
=
key
;
}
};
const
closeAllTooltips
=
()
=>
{
showTooltip
.
value
=
false
;
hoveredKey
.
value
=
''
;
showErrorTooltip
.
value
=
false
;
hoveredErrorKey
.
value
=
''
;
};
const
handleValidationResultClick
=
(
result
:
any
)
=>
{
const
message
=
`字段
${
result
.
field
}
的校验结果:
${
result
.
isValid
?
'通过'
:
'失败'
}
`
;
const
status
=
result
.
isValid
?
'success'
:
'error'
;
VxeUI
.
modal
.
message
({
content
:
message
,
status
});
};
const
handleOpenHistoryDrawer
=
()
=>
{
historyFillCheckRef
.
value
.
onDrawerShow
(
queryParam
.
value
.
tplId
);
// 设置抽屉可见
historyFillCheckRef
.
value
?.
onDrawerShow
(
queryParam
.
value
.
tplId
);
historyDrawerVisible
.
value
=
true
;
}
};
const
closeHistoryDrawer
=
()
=>
{
historyDrawerVisible
.
value
=
false
;
};
</
script
>
<
style
lang=
"less"
scoped
>
...
...
@@ -1080,6 +1230,90 @@ const handleOpenHistoryDrawer = () => {
.vxe-checkbox-group {
display: inline-block;
}
.input-wrapper {
position: relative;
display: inline-block;
}
.unit {
margin-left: 5px;
font-size: 12px;
}
.help-icon {
margin-left: 5px;
cursor: pointer;
color: #1890ff;
font-weight: bold;
font-size: 14px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background: #e6f7ff;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background: #bae7ff;
}
}
.error-icon {
margin-left: 5px;
cursor: pointer;
color: #ff4d4f;
font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
.tooltip,
.error-tooltip {
position: absolute;
top: 100%;
left: 0;
margin-top: 5px;
padding: 10px;
font-size: 12px;
border-radius: 4px;
z-index: 1000;
white-space: pre-wrap;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip {
background: #333;
color: #fff;
&::before {
content: '';
position: absolute;
bottom: 100%;
left: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #333 transparent;
}
}
.error-tooltip {
background: #8b0000;
color: #fff;
border: 1px solid #ff4d4f;
}
.bank-report-table {
font-family: "SimSun", "宋体", serif;
font-size: 12px;
...
...
@@ -1160,103 +1394,11 @@ blockquote {
cursor: pointer;
}
.input-wrapper {
position: relative;
display: inline-block;
}
.input-wrapper .unit {
margin-left: 4px;
}
.input-wrapper .error-tip {
position: absolute;
top: 100%;
left: 0;
margin-top: 2px;
padding: 4px 8px;
background: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
font-size: 11px;
color: #ff4d4f;
line-height: 1.4;
max-width: 200px;
word-wrap: break-word;
z-index: 1000;
white-space: normal;
box-shadow: 0 2px 8px rgba(255, 77, 79, 0.15);
}
.input-wrapper .error-tip::before {
content: '';
position: absolute;
top: -5px;
left: 10px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #ffccc7;
}
.vxe-input.status--error .vxe-input--inner {
border-color: #ff4d4f !important;
background-color: #fff2f0 !important;
}
.input-wrapper .help-icon {
display: inline-block;
width: 16px;
height: 16px;
background: #1890ff;
color: white;
border-radius: 50%;
text-align: center;
line-height: 16px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
margin-left: 4px;
user-select: none;
transition: all 0.3s;
}
.input-wrapper .help-icon:hover {
background: #40a9ff;
transform: scale(1.1);
}
.input-wrapper .tooltip {
position: absolute;
top: 100%;
left: 0;
margin-top: 2px;
padding: 6px 10px;
background: #1f1f1f;
color: white;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
max-width: 250px;
word-wrap: break-word;
z-index: 1001;
white-space: normal;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.input-wrapper .tooltip::before {
content: '';
position: absolute;
top: -5px;
left: 12px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #1f1f1f;
}
/* 校验结果抽屉样式 */
.validation-results {
padding: 16px;
...
...
zrch-risk-client-39/src/views/baosong/report/components/Tb4.vue
浏览文件 @
8c864de6
<
template
>
<div
class=
"bank-report-table"
style=
"height: 80vh;"
@
click=
"close
Tooltip
"
>
<div
class=
"bank-report-table"
style=
"height: 80vh;"
@
click=
"close
AllTooltips
"
>
<!-- 加载遮罩层 -->
<div
v-if=
"loading"
class=
"loading-overlay"
>
<div
class=
"loading-spinner"
>
...
...
@@ -17,54 +17,11 @@
</
template
>
<
template
#
tools
>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"handleOpenHistoryDrawer()"
:disabled=
"loading"
>
近5年数据填报
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"
check
Data()"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"
validate
Data()"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"saveBatch()"
>
保存
</vxe-button>
</
template
>
</vxe-toolbar>
<!-- 校验结果抽屉 -->
<vxe-drawer
v-model=
"drawerVisible"
placement=
"right"
@
show=
"handleValidationDrawerShow"
title=
"校验结果"
width=
"40%"
:footer=
"{ show: true }"
>
<
template
#
default
>
<div
class=
"validation-results"
>
<div
class=
"result-summary"
>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
校验字段数:
</span>
<span
class=
"summary-value"
>
{{
validationResultsList
.
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
通过:
</span>
<span
class=
"summary-value success"
>
{{
validationResultsList
.
filter
(
item
=>
item
.
isValid
).
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
失败:
</span>
<span
class=
"summary-value error"
>
{{
validationResultsList
.
filter
(
item
=>
!
item
.
isValid
).
length
}}
</span>
</div>
</div>
<div
class=
"results-list"
>
<div
v-for=
"(result, index) in validationResultsList"
:key=
"index"
class=
"result-item"
:class=
"
{ success: result.isValid, error: !result.isValid }"
@click="handleValidationResultClick(result)"
>
<div
class=
"result-field"
>
{{
result
.
field
}}
</div>
<div
class=
"result-desc"
>
{{
result
.
description
}}
</div>
<div
class=
"result-value"
>
值:
{{
result
.
fieldValue
||
'空'
}}
</div>
<div
v-if=
"!result.isValid"
class=
"result-error"
>
失败原因:
{{
result
.
description
}}
</div>
</div>
</div>
</div>
</
template
>
</vxe-drawer>
<MyVxeTable
:title=
"tableFormData.project"
:data=
"tableFormData.data"
...
...
@@ -79,22 +36,22 @@
<!-- 历史填报检查组件 -->
<HistoryFillCheck
ref=
"historyFillCheckRef"
v-model=
"historyDrawerVisible"
@
closeDrawer=
"closeHistoryDrawer"
/>
<!-- 验证公式帮助提示 -->
<div
v-if=
"validationTooltipVisible"
class=
"validation-tooltip"
:style=
"{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }"
>
<pre
class=
"tooltip-content"
>
{{ validationTooltipContent }}
</pre>
</div>
<!-- 校验抽屉 -->
<ValidationDrawer
ref=
"validationDrawerRef"
v-model=
"drawerVisible"
:tableFormData=
"tableFormData"
@
validationResultClick=
"handleValidationResultClick"
/>
</div>
</template>
<
script
lang=
"ts"
setup
>
import
MyVxeTable
from
'../tableComponents/MyVxeTable.vue'
import
HistoryFillCheck
from
'./check/historyFillCheck.vue'
import
ValidationDrawer
from
'./check/ValidationDrawer.vue'
import
{
tableFormData
}
from
'../../data/tb4.data'
;
import
{
ref
,
reactive
,
onMoun
ted
}
from
'vue'
import
{
ref
,
reactive
,
nextTick
,
onMounted
,
toRaw
,
compu
ted
}
from
'vue'
import
{
VxeUI
}
from
'vxe-table'
import
{
batchSaveOrUpdateBeforeDelete
,
queryRecord
}
from
'../../record/BaosongTaskRecord.api'
import
{
findUserRightForTplItem
}
from
'../../alloc/BaosongTaskAlloc.api'
...
...
@@ -104,6 +61,7 @@ import { useRoute } from 'vue-router';
const
route
=
useRoute
();
const
refMyVxeTable
=
ref
();
const
validationDrawerRef
=
ref
();
const
tplItemMap
=
ref
({});
const
historyFillCheckRef
=
ref
<
any
>
(
null
);
const
queryParam
=
ref
({
...
...
@@ -117,12 +75,13 @@ const queryParam = ref({
// 权限相关状态
const
userAllocItems
=
ref
<
string
[]
>
([])
const
validFormula
=
ref
<
any
[]
>
([])
const
validationResultsList
=
ref
<
any
[]
>
([])
const
drawerVisible
=
ref
(
false
)
const
historyDrawerVisible
=
ref
(
false
)
const
validationTooltipVisible
=
ref
(
false
)
const
validationTooltipPosition
=
ref
({
left
:
0
,
top
:
0
})
const
validationTooltipContent
=
ref
(
''
)
const
showTooltip
=
ref
(
false
)
const
hoveredKey
=
ref
(
''
)
const
showErrorTooltip
=
ref
(
false
)
const
hoveredErrorKey
=
ref
(
''
)
const
isInitialized
=
ref
(
false
)
interface
FormData
{
id
:
number
|
null
...
...
@@ -135,6 +94,18 @@ interface FormData {
rind
:
number
}
interface
FormulaItem
{
formula
:
string
;
des
:
string
;
[
key
:
string
]:
any
;
}
interface
InputError
{
message
:
string
;
formula
:
string
;
error
?:
string
;
}
onMounted
(
async
()
=>
{
if
(
route
.
query
.
taskId
)
{
queryParam
.
value
.
taskId
=
Number
(
route
.
query
.
taskId
);
...
...
@@ -167,12 +138,42 @@ onMounted(async ()=>{
}
await
setTplItemMap
()
await
setData
();
await
setFormItemRight
()
await
setData
()
// 标记初始化完成
isInitialized
.
value
=
true
// 延迟刷新一次,确保问号图标显示
setTimeout
(()
=>
{
refreshHelpIcons
()
},
300
)
})
const
loading
=
ref
(
false
)
const
formData
=
reactive
({});
const
formValues
=
ref
<
FormData
[]
>
([])
const
inputErrors
=
ref
<
Record
<
string
,
InputError
>>
({})
const
fieldKeys
=
computed
(()
=>
Object
.
keys
(
formData
));
const
getFieldKey
=
(
rowCode
:
string
,
field
:
string
):
string
=>
{
return
`
${
rowCode
}
_
${
field
}
`
;
};
const
showHelpIcon
=
(
rowCode
:
string
,
field
:
string
,
item
:
any
):
boolean
=>
{
if
(
item
.
hasValidFormula
!==
undefined
)
{
return
item
.
hasValidFormula
===
true
;
}
const
key
=
getFieldKey
(
rowCode
,
field
);
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
!
hasRight
)
return
false
;
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
field
);
return
!!
formulaResult
.
formula
;
};
const
saveBatch
=
async
()
=>
{
try
{
...
...
@@ -294,116 +295,175 @@ async function setTplItemMap() {
}
}
// 校验数据
const
checkData
=
()
=>
{
drawerVisible
.
value
=
true
}
// 校验结果抽屉显示时触发
const
handleValidationDrawerShow
=
()
=>
{
performValidation
()
}
// 执行校验
const
performValidation
=
()
=>
{
validationResultsList
.
value
=
[]
const
process
:
string
[]
=
[]
process
.
push
(
'开始校验'
)
process
.
push
(
`找到
${
validFormula
.
value
.
length
}
个验证公式`
)
validFormula
.
value
.
forEach
((
formulaItem
:
any
)
=>
{
process
.
push
(
`开始校验公式:
${
formulaItem
.
des
}
`
)
const
result
=
evaluateFormula
(
formulaItem
.
formula
,
formulaItem
.
des
,
process
)
if
(
result
)
{
validationResultsList
.
value
.
push
(
result
)
const
refreshHelpIcons
=
()
=>
{
console
.
log
(
'刷新帮助图标...'
);
tableFormData
.
columns
.
forEach
((
column
:
any
)
=>
{
if
(
column
.
field
)
{
const
key
=
getFieldKey
(
tableFormData
.
code
,
column
.
field
);
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
hasRight
)
{
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
column
.
field
);
column
.
hasValidFormula
=
!!
formulaResult
.
formula
;
column
.
matchedFormula
=
formulaResult
.
formula
||
null
;
}
else
{
column
.
hasValidFormula
=
false
;
column
.
matchedFormula
=
null
;
}
}
})
process
.
push
(
'校验完成'
)
}
// 解析和执行验证公式
const
evaluateFormula
=
(
formula
:
string
,
description
:
string
,
process
:
string
[]):
any
=>
{
try
{
const
fieldMatch
=
formula
.
match
(
/
\[(\w
+
)\]
/
)
if
(
!
fieldMatch
)
{
process
.
push
(
`跳过: 无法解析公式字段
${
formula
}
`
)
return
null
});
nextTick
(()
=>
{
console
.
log
(
'帮助图标刷新完成'
);
});
};
const
setFormItemRight
=
()
=>
{
tableFormData
.
columns
.
forEach
((
column
:
any
)
=>
{
if
(
column
.
field
)
{
const
key
=
getFieldKey
(
tableFormData
.
code
,
column
.
field
);
column
.
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
column
.
hasRight
)
{
const
{
formula
}
=
findEarliestFormulaForField
(
validFormula
.
value
,
column
.
field
);
column
.
hasValidFormula
=
!!
formula
;
column
.
matchedFormula
=
formula
;
}
}
const
fieldName
=
fieldMatch
[
1
]
const
strKey
=
`
${
tableFormData
.
code
}
_
${
fieldName
}
`
const
fieldValue
=
formData
[
strKey
]
// 检查是否有空字段规则
const
emptyFieldRule
=
validFormula
.
value
.
find
((
f
:
any
)
=>
f
.
formula
.
includes
(
fieldName
)
&&
f
.
des
.
includes
(
'空字段'
)
)
if
(
emptyFieldRule
&&
(
!
fieldValue
||
fieldValue
===
''
))
{
process
.
push
(
`跳过: 字段
${
fieldName
}
为空,跳过空字段规则`
)
return
null
});
};
const
findEarliestFormulaForField
=
(
formulas
:
FormulaItem
[],
field
:
string
):
{
formula
:
FormulaItem
|
null
,
index
:
number
}
=>
{
let
result
=
{
formula
:
null
,
index
:
Infinity
};
formulas
?.
forEach
((
f
:
FormulaItem
)
=>
{
const
text
=
f
.
formula
||
''
;
const
escapedField
=
field
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
regex
=
new
RegExp
(
`\\b
${
escapedField
}
\\b(?!\\w)`
,
'g'
);
const
match
=
regex
.
exec
(
text
);
if
(
match
&&
match
.
index
<
result
.
index
)
{
result
=
{
formula
:
f
,
index
:
match
.
index
};
}
});
return
result
;
};
const
expression
=
formula
.
replace
(
/
\[(\w
+
)\]
/g
,
(
_
,
field
)
=>
{
const
value
=
formData
[
`
${
tableFormData
.
code
}
_
${
field
}
`
]
return
value
!==
undefined
?
`Number(
${
value
}
)`
:
'0'
})
const
validateData
=
(
)
=>
{
drawerVisible
.
value
=
true
;
validationDrawerRef
.
value
.
setValidateData
(
validFormula
.
value
,
formData
);
};
process
.
push
(
`执行公式:
${
expression
}
`
)
const
isValid
=
eval
(
expression
)
const
handleInputBlur
=
(
rowCode
:
string
,
field
:
string
,
formula
?:
string
)
=>
{
if
(
!
formula
)
return
;
const
key
=
getFieldKey
(
rowCode
,
field
);
const
value
=
formData
[
key
];
if
(
!
value
||
value
===
''
)
{
delete
inputErrors
.
value
[
key
];
return
;
}
validateFieldFormula
(
rowCode
,
field
,
formula
);
};
return
{
field
:
fieldName
,
description
:
description
,
formula
:
formula
,
isValid
:
isValid
,
fieldValue
:
fieldValue
,
rowCode
:
tableFormData
.
code
const
validateFieldFormula
=
(
rowCode
:
string
,
field
:
string
,
formula
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
try
{
const
expression
=
buildExpression
(
formula
,
rowCode
);
const
isValid
=
eval
(
expression
);
if
(
!
isValid
)
{
inputErrors
.
value
[
key
]
=
{
message
:
"公式校验失败,请检查填写内容"
,
formula
};
}
else
{
delete
inputErrors
.
value
[
key
];
}
}
catch
(
error
)
{
process
.
push
(
`公式执行错误:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)}
`
)
return
null
}
catch
(
error
:
any
)
{
inputErrors
.
value
[
key
]
=
{
message
:
"公式校验失败,请检查填写内容"
,
formula
,
error
:
error
.
message
};
}
}
// 处理校验结果点击,定位到表格
const
handleValidationResultClick
=
(
result
:
any
)
=>
{
const
strKey
=
`
${
result
.
rowCode
}
_
${
result
.
field
}
`
const
inputElement
=
document
.
querySelector
(
`[data-field="
${
strKey
}
"]`
)
as
HTMLElement
if
(
inputElement
)
{
inputElement
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'center'
})
inputElement
.
focus
()
inputElement
.
style
.
border
=
'2px solid #ff4d4f'
setTimeout
(()
=>
{
inputElement
.
style
.
border
=
''
},
2000
)
};
const
buildExpression
=
(
formula
:
string
,
rowCode
:
string
):
string
=>
{
const
fieldNames
=
formula
.
match
(
/
\b[
A-Za-z
][
A-Za-z0-9_
]
*
\b
/g
)
||
[];
const
uniqueFields
=
[...
new
Set
(
fieldNames
.
filter
(
f
=>
!
[
'and'
,
'or'
,
'not'
,
'equal'
,
'less'
,
'greater'
,
'if'
,
'else'
,
'true'
,
'false'
]
.
includes
(
f
.
toLowerCase
())
))];
uniqueFields
.
sort
((
a
,
b
)
=>
b
.
length
-
a
.
length
);
let
expression
=
formula
;
for
(
const
fieldName
of
uniqueFields
)
{
const
key
=
getFieldKey
(
rowCode
,
fieldName
);
const
value
=
formData
[
key
];
if
(
value
!==
undefined
)
{
expression
=
expression
.
replace
(
new
RegExp
(
`\\b
${
fieldName
}
\\b`
,
'g'
),
`Number(
${
value
}
)`
);
}
}
}
// 显示验证公式帮助
const
showValidationHelp
=
(
formulaItem
:
any
,
event
:
MouseEvent
)
=>
{
const
rect
=
(
event
.
target
as
HTMLElement
).
getBoundingClientRect
()
validationTooltipPosition
.
value
=
{
left
:
rect
.
left
+
rect
.
width
/
2
,
top
:
rect
.
top
-
10
return
expression
;
};
const
toggleTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
if
(
hoveredKey
.
value
===
key
)
{
showTooltip
.
value
=
false
;
hoveredKey
.
value
=
''
;
}
else
{
showTooltip
.
value
=
true
;
hoveredKey
.
value
=
key
;
}
validationTooltipContent
.
value
=
`验证公式:
${
formulaItem
.
formula
}
\n说明:
${
formulaItem
.
des
}
`
validationTooltipVisible
.
value
=
true
}
};
const
toggleErrorTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
if
(
hoveredErrorKey
.
value
===
key
)
{
showErrorTooltip
.
value
=
false
;
hoveredErrorKey
.
value
=
''
;
}
else
{
showErrorTooltip
.
value
=
true
;
hoveredErrorKey
.
value
=
key
;
}
};
// 关闭验证公式帮助
const
closeTooltip
=
()
=>
{
validationTooltipVisible
.
value
=
false
}
const
closeAllTooltips
=
()
=>
{
showTooltip
.
value
=
false
;
hoveredKey
.
value
=
''
;
showErrorTooltip
.
value
=
false
;
hoveredErrorKey
.
value
=
''
;
};
const
handleValidationResultClick
=
(
result
:
any
)
=>
{
const
message
=
`字段
${
result
.
field
}
的校验结果:
${
result
.
isValid
?
'通过'
:
'失败'
}
`
;
const
status
=
result
.
isValid
?
'success'
:
'error'
;
VxeUI
.
modal
.
message
({
content
:
message
,
status
});
};
// 打开历史填报抽屉
const
handleOpenHistoryDrawer
=
()
=>
{
historyDrawerVisible
.
value
=
true
historyFillCheckRef
.
value
?.
onDrawerShow
(
queryParam
.
value
.
tplId
);
historyDrawerVisible
.
value
=
true
;
}
// 关闭历史填报抽屉
const
closeHistoryDrawer
=
()
=>
{
historyDrawerVisible
.
value
=
false
historyDrawerVisible
.
value
=
false
;
}
</
script
>
...
...
@@ -486,112 +546,4 @@ const closeHistoryDrawer = () => {
font-size: 14px;
color: #666;
}
/* 校验结果 */
.validation-results {
padding: 20px;
}
.result-summary {
display: flex;
gap: 40px;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
.summary-item {
display: flex;
align-items: center;
gap: 10px;
}
.summary-label {
font-weight: 500;
color: #666;
}
.summary-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
max-height: 500px;
overflow-y: auto;
}
.result-item {
padding: 12px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.result-item:hover {
transform: translateX(5px);
}
.result-item.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
}
.result-item.error {
background: #fff2f0;
border: 1px solid #ffccc7;
}
.result-field {
font-weight: bold;
margin-bottom: 4px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.result-value {
font-size: 12px;
color: #888;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 4px;
}
/* 验证公式帮助提示 */
.validation-tooltip {
position: fixed;
background: #333;
color: white;
padding: 10px 15px;
border-radius: 4px;
font-size: 12px;
z-index: 9999;
transform: translateX(-50%);
white-space: pre-wrap;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.tooltip-content {
margin: 0;
}
</
style
>
\ No newline at end of file
zrch-risk-client-39/src/views/baosong/report/components/Tb5.vue
浏览文件 @
8c864de6
<
template
>
<div
class=
"bank-report-table"
style=
"height: 80vh"
@
click=
"close
Tooltip
"
>
<div
class=
"bank-report-table"
style=
"height: 80vh"
@
click=
"close
AllTooltips
"
>
<!-- 加载遮罩层 -->
<div
v-if=
"loading"
class=
"loading-overlay"
>
<div
class=
"loading-spinner"
>
...
...
@@ -17,46 +17,11 @@
</
template
>
<
template
#
tools
>
<vxe-button
status=
"primary"
icon=
"vxe-icon-edit"
@
click=
"handleOpenHistoryDrawer()"
:disabled=
"loading"
>
近5年数据填报
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"
check
Data()"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"
validate
Data()"
>
校验
</vxe-button>
<vxe-button
status=
"primary"
icon=
"vxe-icon-save"
@
click=
"saveBatch()"
>
保存
</vxe-button>
</
template
>
</vxe-toolbar>
<!-- 校验结果抽屉 -->
<vxe-drawer
v-model=
"drawerVisible"
placement=
"right"
@
show=
"handleValidationDrawerShow"
title=
"校验结果"
width=
"40%"
:footer=
"{ show: true }"
>
<
template
#
default
>
<div
class=
"validation-results"
>
<div
class=
"result-summary"
>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
校验字段数:
</span>
<span
class=
"summary-value"
>
{{
validationResultsList
.
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
通过:
</span>
<span
class=
"summary-value success"
>
{{
validationResultsList
.
filter
((
item
)
=>
item
.
isValid
).
length
}}
</span>
</div>
<div
class=
"summary-item"
>
<span
class=
"summary-label"
>
失败:
</span>
<span
class=
"summary-value error"
>
{{
validationResultsList
.
filter
((
item
)
=>
!
item
.
isValid
).
length
}}
</span>
</div>
</div>
<div
class=
"results-list"
>
<div
v-for=
"(result, index) in validationResultsList"
:key=
"index"
class=
"result-item"
:class=
"
{ success: result.isValid, error: !result.isValid }"
@click="handleValidationResultClick(result)"
>
<div
class=
"result-field"
>
{{
result
.
field
}}
</div>
<div
class=
"result-desc"
>
{{
result
.
description
}}
</div>
<div
class=
"result-value"
>
值:
{{
result
.
fieldValue
||
'空'
}}
</div>
<div
v-if=
"!result.isValid"
class=
"result-error"
>
失败原因:
{{
result
.
description
}}
</div>
</div>
</div>
</div>
</
template
>
</vxe-drawer>
<vxe-table
border
ref=
"tableRef"
...
...
@@ -189,8 +154,59 @@
</span>
</div>
<
template
v-else
>
<vxe-input
:type=
"item.type"
v-model=
"formData[row.code + '_' + item.field]"
size=
"mini"
class=
"table-input"
>
</vxe-input>
<span
class=
"unit"
>
{{
item
.
placeholder
}}
</span>
<div
class=
"input-wrapper"
>
<vxe-input
:type=
"item.type"
v-model=
"formData[row.code + '_' + item.field]"
size=
"mini"
class=
"table-input"
@
blur=
"handleInputBlur(row.code, item.field, item.matchedFormula?.formula)"
/>
<span
class=
"unit"
>
{{
item
.
placeholder
}}
</span>
<span
v-if=
"showHelpIcon(row.code, item.field, item)"
class=
"help-icon"
@
click
.
stop=
"toggleTooltip(row.code, item.field)"
title=
"点击查看校验规则"
>
?
</span>
<span
v-if=
"inputErrors[getFieldKey(row.code, item.field)]"
class=
"error-icon"
@
click
.
stop=
"toggleErrorTooltip(row.code, item.field)"
>
<i
class=
"vxe-icon-error"
>
✗
</i>
</span>
<div
v-if=
"showErrorTooltip && hoveredErrorKey === getFieldKey(row.code, item.field)"
class=
"error-tooltip"
@
click
.
stop
>
{{
inputErrors
[
getFieldKey
(
row
.
code
,
item
.
field
)].
message
}}
<br>
<span
style=
"color: #ffcccc;"
>
公式:
{{
inputErrors
[
getFieldKey
(
row
.
code
,
item
.
field
)].
formula
}}
</span>
<template
v-if=
"inputErrors[getFieldKey(row.code, item.field)].error"
>
<br>
<span
style=
"color: #ff9999; font-size: 11px;"
>
错误:
{{
inputErrors
[
getFieldKey
(
row
.
code
,
item
.
field
)].
error
}}
</span>
</
template
>
</div>
<div
v-if=
"showTooltip && hoveredKey === getFieldKey(row.code, item.field)"
class=
"tooltip"
@
click
.
stop
>
{{ item.matchedFormula?.des || '暂无校验规则' }}
</div>
</div>
</template>
</template>
</template>
...
...
@@ -202,14 +218,13 @@
<!-- 历史填报检查组件 -->
<HistoryFillCheck
ref=
"historyFillCheckRef"
v-model=
"historyDrawerVisible"
@
close-drawer=
"closeHistoryDrawer"
/>
<!-- 验证公式帮助提示 -->
<div
v-if=
"validationTooltipVisible"
class=
"validation-tooltip"
:style=
"{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }"
>
<pre
class=
"tooltip-content"
>
{{ validationTooltipContent }}
</pre>
</div>
<!-- 校验抽屉 -->
<ValidationDrawer
ref=
"validationDrawerRef"
v-model=
"drawerVisible"
:tableFormData=
"tableFormData"
@
validationResultClick=
"handleValidationResultClick"
/>
</div>
</template>
...
...
@@ -217,9 +232,10 @@
import
MultiColumnTable
from
'../tableComponents/MultiColumnTable.vue'
;
import
AttachTable
from
'../tableComponents/AttachTable.vue'
;
import
HistoryFillCheck
from
'./check/historyFillCheck.vue'
;
import
ValidationDrawer
from
'./check/ValidationDrawer.vue'
;
import
{
tableFormData
}
from
'../../data/tb5.data'
;
import
{
ref
,
reactive
,
nextTick
,
onMounted
,
toRaw
}
from
'vue'
;
import
{
ref
,
reactive
,
nextTick
,
onMounted
,
toRaw
,
computed
}
from
'vue'
;
import
{
VxeUI
,
VxeToolbarInstance
,
VxeToolbarPropTypes
}
from
'vxe-table'
;
import
{
batchSaveOrUpdate
,
queryRecord
,
batchSaveOrUpdateBeforeDelete
}
from
'../../record/BaosongTaskRecord.api'
;
import
{
queryAllTplItemForUser
,
findUserRightForTplItem
}
from
'../../alloc/BaosongTaskAlloc.api'
;
...
...
@@ -229,8 +245,9 @@
const
route
=
useRoute
();
const
tableRef
=
ref
();
const
tplItemMap
=
ref
({});
const
historyFillCheckRef
=
ref
<
any
>
(
null
);
const
validationDrawerRef
=
ref
();
const
tplItemMap
=
ref
({});
const
queryParam
=
ref
({
taskId
:
-
1
,
taskName
:
''
,
...
...
@@ -242,12 +259,12 @@
// 权限相关状态
const
userAllocItems
=
ref
<
string
[]
>
([]);
const
validFormula
=
ref
<
any
[]
>
([]);
const
validationResultsList
=
ref
<
any
[]
>
([]);
const
drawerVisible
=
ref
(
false
);
const
historyDrawerVisible
=
ref
(
false
);
const
validationTooltipVisible
=
ref
(
false
);
const
validationTooltipPosition
=
ref
({
left
:
0
,
top
:
0
});
const
validationTooltipContent
=
ref
(
''
);
const
showTooltip
=
ref
(
false
);
const
hoveredKey
=
ref
(
''
);
const
showErrorTooltip
=
ref
(
false
);
const
hoveredErrorKey
=
ref
(
''
);
interface
FormData
{
id
:
number
|
null
;
...
...
@@ -260,6 +277,18 @@
rind
:
number
;
}
interface
FormulaItem
{
formula
:
string
;
des
:
string
;
[
key
:
string
]:
any
;
}
interface
InputError
{
message
:
string
;
formula
:
string
;
error
?:
string
;
}
onMounted
(
async
()
=>
{
if
(
route
.
query
.
taskId
)
{
queryParam
.
value
.
taskId
=
Number
(
route
.
query
.
taskId
);
...
...
@@ -292,16 +321,48 @@
}
await
setTplItemMap
();
await
setFormItemRight
();
await
setData
();
// 标记初始化完成
isInitialized
.
value
=
true
;
// 延迟刷新一次,确保问号图标显示
setTimeout
(()
=>
{
refreshHelpIcons
();
},
300
);
});
const
loading
=
ref
(
false
);
const
isInitialized
=
ref
(
false
);
const
formData
=
reactive
({});
const
formValues
=
ref
<
FormData
[]
>
([]);
const
inputErrors
=
ref
<
Record
<
string
,
InputError
>>
({});
const
fieldKeys
=
computed
(()
=>
Object
.
keys
(
formData
));
const
getFieldKey
=
(
rowCode
:
string
,
field
:
string
):
string
=>
{
return
`
${
rowCode
}
_
${
field
}
`
;
};
const
showHelpIcon
=
(
rowCode
:
string
,
field
:
string
,
item
:
any
):
boolean
=>
{
if
(
item
.
hasValidFormula
!==
undefined
)
{
return
item
.
hasValidFormula
===
true
;
}
const
key
=
getFieldKey
(
rowCode
,
field
);
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
!
hasRight
)
return
false
;
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
field
);
return
!!
formulaResult
.
formula
;
};
const
saveBatch
=
async
()
=>
{
try
{
if
(
!
await
validateForm
())
return
;
formValues
.
value
=
[];
for
(
const
strKey
in
formData
)
{
...
...
@@ -318,17 +379,28 @@
if
(
formValues
.
value
.
length
>
0
)
{
await
batchSaveOrUpdateBeforeDelete
(
formValues
.
value
);
VxeUI
.
modal
.
message
({
content
:
'保存成功'
,
status
:
'success'
});
//await setData()
}
else
{
VxeUI
.
modal
.
message
({
content
:
'没有需要保存的数据'
,
status
:
'warning'
});
}
}
catch
(
error
)
{
}
catch
(
error
:
any
)
{
VxeUI
.
modal
.
message
({
content
:
`保存失败:
${
error
.
message
}
`
,
status
:
'error'
});
}
finally
{
loading
.
value
=
false
;
}
};
const
validateForm
=
async
():
Promise
<
boolean
>
=>
{
const
$table
=
tableRef
.
value
;
if
(
!
$table
)
return
true
;
const
checkResult
=
$table
.
validate
();
if
(
!
checkResult
)
{
VxeUI
.
modal
.
message
({
content
:
'表单验证失败,请检查填写内容'
,
status
:
'error'
});
return
false
;
}
return
true
;
};
const
childMultiTableRefs
=
ref
({});
const
setMultiColumnTableRef
=
(
el
:
any
,
index
:
string
)
=>
{
if
(
el
)
{
...
...
@@ -520,132 +592,184 @@
}
// 校验数据
const
check
Data
=
()
=>
{
const
validate
Data
=
()
=>
{
drawerVisible
.
value
=
true
;
validationDrawerRef
.
value
.
setValidateData
(
validFormula
.
value
,
formData
);
};
// 校验结果抽屉显示时触发
const
handleValidationDrawerShow
=
()
=>
{
performValidation
();
};
// 执行校验
const
performValidation
=
()
=>
{
validationResultsList
.
value
=
[];
const
process
:
string
[]
=
[];
process
.
push
(
'开始校验'
);
process
.
push
(
`找到
${
validFormula
.
value
.
length
}
个验证公式`
);
validFormula
.
value
.
forEach
((
formulaItem
:
any
)
=>
{
process
.
push
(
`开始校验公式:
${
formulaItem
.
des
}
`
);
const
result
=
evaluateFormula
(
formulaItem
.
formula
,
formulaItem
.
des
,
process
);
if
(
result
)
{
validationResultsList
.
value
.
push
(
result
);
}
});
process
.
push
(
'校验完成'
);
const
handleInputBlur
=
(
rowCode
:
string
,
field
:
string
,
formula
?:
string
)
=>
{
if
(
!
formula
)
return
;
const
key
=
getFieldKey
(
rowCode
,
field
);
const
value
=
formData
[
key
];
if
(
!
value
||
value
===
''
)
{
delete
inputErrors
.
value
[
key
];
return
;
}
validateFieldFormula
(
rowCode
,
field
,
formula
);
};
// 解析和执行验证公式
const
evaluateFormula
=
(
formula
:
string
,
description
:
string
,
process
:
string
[]):
any
=>
{
const
validateFieldFormula
=
(
rowCode
:
string
,
field
:
string
,
formula
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
try
{
const
fieldMatch
=
formula
.
match
(
/
\[(\w
+
)\]
/
);
if
(
!
fieldMatch
)
{
process
.
push
(
`跳过: 无法解析公式字段
${
formula
}
`
);
return
null
;
}
const
fieldName
=
fieldMatch
[
1
];
const
row
=
tableFormData
.
find
((
r
:
any
)
=>
r
.
content
?.
some
((
c
:
any
)
=>
c
.
field
===
fieldName
));
if
(
!
row
)
{
process
.
push
(
`跳过: 未找到字段
${
fieldName
}
`
);
return
null
;
}
const
fieldItem
=
row
.
content
.
find
((
c
:
any
)
=>
c
.
field
===
fieldName
);
if
(
!
fieldItem
)
{
process
.
push
(
`跳过: 未找到字段
${
fieldName
}
`
);
return
null
;
const
expression
=
buildExpression
(
formula
,
rowCode
);
const
isValid
=
eval
(
expression
);
if
(
!
isValid
)
{
inputErrors
.
value
[
key
]
=
{
message
:
"公式校验失败,请检查填写内容"
,
formula
};
}
else
{
delete
inputErrors
.
value
[
key
];
}
}
catch
(
error
:
any
)
{
inputErrors
.
value
[
key
]
=
{
message
:
"公式校验失败,请检查填写内容"
,
formula
,
error
:
error
.
message
};
}
};
const
strKey
=
`
${
row
.
code
}
_
${
fieldName
}
`
;
const
fieldValue
=
formData
[
strKey
];
// 检查是否有空字段规则
const
emptyFieldRule
=
validFormula
.
value
.
find
((
f
:
any
)
=>
f
.
formula
.
includes
(
fieldName
)
&&
f
.
des
.
includes
(
'空字段'
));
if
(
emptyFieldRule
&&
(
!
fieldValue
||
fieldValue
===
''
))
{
process
.
push
(
`跳过: 字段
${
fieldName
}
为空,跳过空字段规则`
);
return
null
;
const
buildExpression
=
(
formula
:
string
,
rowCode
:
string
):
string
=>
{
const
fieldNames
=
formula
.
match
(
/
\b[
A-Za-z
][
A-Za-z0-9_
]
*
\b
/g
)
||
[];
const
uniqueFields
=
[...
new
Set
(
fieldNames
.
filter
(
f
=>
!
[
'and'
,
'or'
,
'not'
,
'equal'
,
'less'
,
'greater'
,
'if'
,
'else'
,
'true'
,
'false'
]
.
includes
(
f
.
toLowerCase
())
))];
uniqueFields
.
sort
((
a
,
b
)
=>
b
.
length
-
a
.
length
);
let
expression
=
formula
;
for
(
const
fieldName
of
uniqueFields
)
{
const
key
=
getFieldKey
(
rowCode
,
fieldName
);
const
value
=
formData
[
key
];
if
(
value
!==
undefined
)
{
expression
=
expression
.
replace
(
new
RegExp
(
`\\b
${
fieldName
}
\\b`
,
'g'
),
`Number(
${
value
}
)`
);
}
}
return
expression
;
};
const
expression
=
formula
.
replace
(
/
\[(\w
+
)\]
/g
,
(
_
,
field
)
=>
{
const
value
=
formData
[
`
${
row
.
code
}
_
${
field
}
`
];
return
value
!==
undefined
?
`Number(
${
value
}
)`
:
'0'
;
});
process
.
push
(
`执行公式:
${
expression
}
`
);
const
isValid
=
eval
(
expression
);
const
toggleTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
if
(
hoveredKey
.
value
===
key
)
{
showTooltip
.
value
=
false
;
hoveredKey
.
value
=
''
;
}
else
{
showTooltip
.
value
=
true
;
hoveredKey
.
value
=
key
;
}
};
return
{
field
:
fieldName
,
description
:
description
,
formula
:
formula
,
isValid
:
isValid
,
fieldValue
:
fieldValue
,
rowCode
:
row
.
code
,
};
}
catch
(
error
)
{
process
.
push
(
`公式执行错误:
${
error
instanceof
Error
?
error
.
message
:
String
(
error
)}
`
);
return
null
;
const
toggleErrorTooltip
=
(
rowCode
:
string
,
field
:
string
)
=>
{
const
key
=
getFieldKey
(
rowCode
,
field
);
if
(
hoveredErrorKey
.
value
===
key
)
{
showErrorTooltip
.
value
=
false
;
hoveredErrorKey
.
value
=
''
;
}
else
{
showErrorTooltip
.
value
=
true
;
hoveredErrorKey
.
value
=
key
;
}
};
// 处理校验结果点击,定位到表格
const
handleValidationResultClick
=
(
result
:
any
)
=>
{
const
row
=
tableFormData
.
find
((
r
:
any
)
=>
r
.
code
===
result
.
rowCode
);
if
(
row
)
{
const
fieldItem
=
row
.
content
.
find
((
c
:
any
)
=>
c
.
field
===
result
.
field
);
if
(
fieldItem
)
{
const
strKey
=
`
${
row
.
code
}
_
${
result
.
field
}
`
;
const
inputElement
=
document
.
querySelector
(
`[data-field="
${
strKey
}
"]`
)
as
HTMLElement
;
if
(
inputElement
)
{
inputElement
.
scrollIntoView
({
behavior
:
'smooth'
,
block
:
'center'
});
inputElement
.
focus
();
inputElement
.
style
.
border
=
'2px solid #ff4d4f'
;
setTimeout
(()
=>
{
inputElement
.
style
.
border
=
''
;
},
2000
);
}
const
closeAllTooltips
=
()
=>
{
showTooltip
.
value
=
false
;
hoveredKey
.
value
=
''
;
showErrorTooltip
.
value
=
false
;
hoveredErrorKey
.
value
=
''
;
};
const
refreshHelpIcons
=
()
=>
{
console
.
log
(
'刷新帮助图标...'
);
tableFormData
.
forEach
((
row
:
any
)
=>
{
if
(
row
.
content
&&
Array
.
isArray
(
row
.
content
))
{
row
.
content
.
forEach
((
item
:
any
)
=>
{
if
(
item
.
field
)
{
const
key
=
getFieldKey
(
row
.
code
,
item
.
field
);
const
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
hasRight
)
{
const
formulaResult
=
findEarliestFormulaForField
(
validFormula
.
value
,
item
.
field
);
item
.
hasValidFormula
=
!!
formulaResult
.
formula
;
item
.
matchedFormula
=
formulaResult
.
formula
||
null
;
}
else
{
item
.
hasValidFormula
=
false
;
item
.
matchedFormula
=
null
;
}
}
});
}
}
});
// 强制更新视图
nextTick
(()
=>
{
console
.
log
(
'帮助图标刷新完成'
);
});
};
// 显示验证公式帮助
const
showValidationHelp
=
(
formulaItem
:
any
,
event
:
MouseEvent
)
=>
{
const
rect
=
(
event
.
target
as
HTMLElement
).
getBoundingClientRect
();
validationTooltipPosition
.
value
=
{
left
:
rect
.
left
+
rect
.
width
/
2
,
top
:
rect
.
top
-
10
,
};
validationTooltipContent
.
value
=
`验证公式:
${
formulaItem
.
formula
}
\n说明:
${
formulaItem
.
des
}
`
;
validationTooltipVisible
.
value
=
true
;
const
setFormItemRight
=
()
=>
{
tableFormData
.
forEach
((
row
:
any
)
=>
{
if
(
row
.
content
&&
Array
.
isArray
(
row
.
content
))
{
row
.
content
.
forEach
((
item
:
any
)
=>
{
if
(
item
.
field
)
{
const
key
=
getFieldKey
(
row
.
code
,
item
.
field
);
item
.
hasRight
=
userAllocItems
.
value
.
includes
(
key
);
if
(
item
.
hasRight
)
{
const
{
formula
}
=
findEarliestFormulaForField
(
validFormula
.
value
,
item
.
field
);
item
.
hasValidFormula
=
!!
formula
;
item
.
matchedFormula
=
formula
;
}
}
});
}
});
};
// 关闭验证公式帮助
const
closeTooltip
=
()
=>
{
validationTooltipVisible
.
value
=
false
;
const
findEarliestFormulaForField
=
(
formulas
:
FormulaItem
[],
field
:
string
):
{
formula
:
FormulaItem
|
null
,
index
:
number
}
=>
{
let
result
=
{
formula
:
null
,
index
:
Infinity
};
formulas
?.
forEach
((
f
:
FormulaItem
)
=>
{
const
text
=
f
.
formula
||
''
;
const
escapedField
=
field
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
regex
=
new
RegExp
(
`\\b
${
escapedField
}
\\b(?!\\w)`
,
'g'
);
const
match
=
regex
.
exec
(
text
);
if
(
match
&&
match
.
index
<
result
.
index
)
{
result
=
{
formula
:
f
,
index
:
match
.
index
};
}
});
return
result
;
};
// 打开历史填报抽屉
const
handleOpenHistoryDrawer
=
()
=>
{
historyFillCheckRef
.
value
?.
onDrawerShow
(
queryParam
.
value
.
tplId
);
historyDrawerVisible
.
value
=
true
;
};
// 关闭历史填报抽屉
const
closeHistoryDrawer
=
()
=>
{
historyDrawerVisible
.
value
=
false
;
};
const
handleValidationResultClick
=
(
result
:
any
)
=>
{
const
message
=
`字段
${
result
.
field
}
的校验结果:
${
result
.
isValid
?
'通过'
:
'失败'
}
`
;
const
status
=
result
.
isValid
?
'success'
:
'error'
;
VxeUI
.
modal
.
message
({
content
:
message
,
status
});
};
</
script
>
<
style
lang=
"less"
scoped
>
...
...
@@ -682,6 +806,89 @@
margin: 0 2px;
}
.input-wrapper {
position: relative;
display: inline-block;
}
.unit {
margin-left: 5px;
font-size: 12px;
}
.help-icon {
margin-left: 5px;
cursor: pointer;
color: #1890ff;
font-weight: bold;
font-size: 14px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background: #e6f7ff;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background: #bae7ff;
}
}
.error-icon {
margin-left: 5px;
cursor: pointer;
color: #ff4d4f;
font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
.tooltip,
.error-tooltip {
position: absolute;
top: 100%;
left: 0;
margin-top: 5px;
padding: 10px;
font-size: 12px;
border-radius: 4px;
z-index: 1000;
white-space: pre-wrap;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip {
background: #333;
color: #fff;
&::before {
content: '';
position: absolute;
bottom: 100%;
left: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #333 transparent;
}
}
.error-tooltip {
background: #8b0000;
color: #fff;
border: 1px solid #ff4d4f;
}
.radio-group {
display: inline-block;
margin-right: 10px;
...
...
@@ -739,111 +946,4 @@
font-size: 14px;
color: #666;
}
/* 校验结果 */
.validation-results {
padding: 20px;
}
.result-summary {
display: flex;
gap: 40px;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
.summary-item {
display: flex;
align-items: center;
gap: 10px;
}
.summary-label {
font-weight: 500;
color: #666;
}
.summary-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
max-height: 500px;
overflow-y: auto;
}
.result-item {
padding: 12px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.result-item:hover {
transform: translateX(5px);
}
.result-item.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
}
.result-item.error {
background: #fff2f0;
border: 1px solid #ffccc7;
}
.result-field {
font-weight: bold;
margin-bottom: 4px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.result-value {
font-size: 12px;
color: #888;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 4px;
}
/* 验证公式帮助提示 */
.validation-tooltip {
position: fixed;
background: #333;
color: white;
padding: 10px 15px;
border-radius: 4px;
font-size: 12px;
z-index: 9999;
transform: translateX(-50%);
white-space: pre-wrap;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.tooltip-content {
margin: 0;
}
</
style
>
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论