提交 8c864de6 authored 作者: kxjia's avatar kxjia

完善验证

上级 db81d638
<template> <template>
<div class="bank-report-table" style="height: 80vh;" @click="closeTooltip"> <div class="bank-report-table" style="height: 80vh;" @click="closeAllTooltips">
<!-- 加载遮罩层 --> <!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay"> <div v-if="loading" class="loading-overlay">
<div class="loading-spinner"> <div class="loading-spinner">
...@@ -16,55 +16,12 @@ ...@@ -16,55 +16,12 @@
</div> </div>
</template> </template>
<template #tools> <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="handleOpenHistoryDrawer()" :disabled="loading">近5年数据填报</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="checkData()">校验</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="validateData()">校验</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch">保存</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch">保存</vxe-button>
</template> </template>
</vxe-toolbar> </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 <vxe-table
border border
ref="tableRef" ref="tableRef"
...@@ -132,27 +89,57 @@ ...@@ -132,27 +89,57 @@
</vxe-radio-group> </vxe-radio-group>
</span> </span>
<template v-else> <template v-else>
<vxe-input <div class="input-wrapper">
:type="item.type" <vxe-input
v-model="formData[getFieldKey(row.code, item.field)]" :type="item.type"
size="mini" v-model="formData[getFieldKey(row.code, item.field)]"
class="table-input" size="mini"
:style="{width:item.width}" class="table-input"
:disabled="!item.hasRight" :style="{width:item.width}"
:data-field="getFieldKey(row.code, item.field)" :disabled="!item.hasRight"
></vxe-input> :status="getInputStatus(row.code, item.field)"
<!-- 帮助图标 --> @blur="handleInputBlur(row.code, item.field, item.matchedFormula?.formula)"
<span />
v-if="item.hasValidFormula" <span class="unit">{{ item.unit }}</span>
class="validation-help-icon"
@mouseenter="(event) => showValidationHelp(item, event)" <span
@mouseleave="closeTooltip" v-if="showHelpIcon(row.code, item.field, item)"
@click="(event) => showValidationHelp(item, event)" class="help-icon"
> @click.stop="toggleTooltip(row.code, item.field)"
title="点击查看校验规则"
</span> >
?
</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> </template>
<span v-if="item.unit" class="unit">{{ item.unit }}</span>
</template> </template>
</template> </template>
</div> </div>
...@@ -163,23 +150,23 @@ ...@@ -163,23 +150,23 @@
<!-- 历史填报检查组件 --> <!-- 历史填报检查组件 -->
<HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @closeDrawer="closeHistoryDrawer"/> <HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @closeDrawer="closeHistoryDrawer"/>
<!-- 验证公式帮助提示 --> <!-- 校验抽屉 -->
<div <ValidationDrawer
v-if="validationTooltipVisible" ref="validationDrawerRef"
class="validation-tooltip" v-model="drawerVisible"
:style="{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }" :tableFormData="tableFormData"
> @validationResultClick="handleValidationResultClick"
<pre class="tooltip-content">{{ validationTooltipContent }}</pre> />
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MultiColumnTable from '../tableComponents/MultiColumnTable.vue' import MultiColumnTable from '../tableComponents/MultiColumnTable.vue'
import HistoryFillCheck from './check/historyFillCheck.vue' import HistoryFillCheck from './check/historyFillCheck.vue'
import ValidationDrawer from './check/ValidationDrawer.vue'
import { tableFormData } from '../../data/tb2.data' 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 { VxeUI } from 'vxe-table'
import { batchSaveOrUpdateBeforeDelete, queryRecord } from '../../record/BaosongTaskRecord.api' import { batchSaveOrUpdateBeforeDelete, queryRecord } from '../../record/BaosongTaskRecord.api'
import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
...@@ -207,16 +194,36 @@ const formData = reactive<Record<string, any>>({}) ...@@ -207,16 +194,36 @@ const formData = reactive<Record<string, any>>({})
const formValues = ref<FormData[]>([]) const formValues = ref<FormData[]>([])
const childMultiTableRefs = ref<Record<string, any>>({}) 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 userAllocItems = ref<string[]>([])
const validFormula = ref<any[]>([]) const validFormula = ref<any[]>([])
const validationResultsList = ref<any[]>([])
const drawerVisible = ref(false) const drawerVisible = ref(false)
const historyDrawerVisible = ref(false) const historyDrawerVisible = ref(false)
const historyFillCheckRef = ref<any>(null) const historyFillCheckRef = ref<any>(null)
const validationTooltipVisible = ref(false)
const validationTooltipPosition = ref({ left: 0, top: 0 })
const validationTooltipContent = ref('')
const queryParam = ref({ const queryParam = ref({
taskId: -1, taskId: -1,
...@@ -243,8 +250,9 @@ onMounted(async () => { ...@@ -243,8 +250,9 @@ onMounted(async () => {
queryParam.value.tplCode = String(route.query.tplCode) queryParam.value.tplCode = String(route.query.tplCode)
} }
// 获取权限和验证公式
try { try {
loading.value = true
userAllocItems.value = await findUserRightForTplItem({ userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId, tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId taskid: queryParam.value.taskId
...@@ -253,13 +261,25 @@ onMounted(async () => { ...@@ -253,13 +261,25 @@ onMounted(async () => {
validFormula.value = await getTblvalidFormula({ validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId, tplid: queryParam.value.tplId,
}) })
await setTplItemMap()
setFormItemRight()
await setData()
isInitialized.value = true
setTimeout(() => {
refreshHelpIcons()
}, 300)
} catch (error) { } catch (error) {
console.error('获取权限或验证公式失败:', 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 => { const getFieldKey = (pcode: string | undefined, field: string): string => {
...@@ -455,124 +475,177 @@ const setFormItemRight = () => { ...@@ -455,124 +475,177 @@ const setFormItemRight = () => {
}); });
} }
// 校验数据 const validateData = () => {
const checkData = () => {
drawerVisible.value = true drawerVisible.value = true
validationDrawerRef.value.setValidateData(validFormula.value, formData)
} }
// 校验结果抽屉显示时触发 const handleInputBlur = (rowCode: string, field: string, formula?: string) => {
const handleValidationDrawerShow = () => { if (!formula) return
performValidation()
} const key = getFieldKey(rowCode, field)
const value = formData[key]
// 执行校验
const performValidation = () => { if (!value || value === '') {
validationResultsList.value = [] delete inputErrors.value[key]
const process: string[] = [] return
process.push('开始校验') }
process.push(`找到 ${validFormula.value.length} 个验证公式`)
validateFieldFormula(rowCode, field, formula)
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 validateFieldFormula = (rowCode: string, field: string, formula: string) => {
const evaluateFormula = (formula: string, description: string, process: string[]): any => { const key = getFieldKey(rowCode, field)
try { try {
const fieldMatch = formula.match(/\[(\w+)\]/) const expression = buildExpression(formula, rowCode)
if (!fieldMatch) { const isValid = eval(expression)
process.push(`跳过: 无法解析公式字段 ${formula}`) if (!isValid) {
return null inputErrors.value[key] = {
message: "公式校验失败,请检查填写内容",
formula
}
} else {
delete inputErrors.value[key]
} }
} catch (error: any) {
const fieldName = fieldMatch[1] inputErrors.value[key] = {
const row = tableFormData.find(r => r.content?.some((c: any) => c.field === fieldName)) message: "公式校验失败,请检查填写内容",
if (!row) { formula,
process.push(`跳过: 未找到字段 ${fieldName}`) error: error.message
return null
} }
}
}
const fieldItem = row.content.find((c: any) => c.field === fieldName) const buildExpression = (formula: string, rowCode: string): string => {
if (!fieldItem) { const fieldNames = formula.match(/\b[A-Za-z][A-Za-z0-9_]*\b/g) || []
process.push(`跳过: 未找到字段 ${fieldName}`) const uniqueFields = [...new Set(fieldNames.filter(f =>
return null !['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 toggleTooltip = (rowCode: string, field: string) => {
const fieldValue = formData[strKey] const key = getFieldKey(rowCode, field)
if (hoveredKey.value === key) {
// 检查是否有空字段规则 showTooltip.value = false
const emptyFieldRule = validFormula.value.find((f: any) => hoveredKey.value = ''
f.formula.includes(fieldName) && f.des.includes('空字段') } else {
) showTooltip.value = true
hoveredKey.value = key
}
}
if (emptyFieldRule && (!fieldValue || fieldValue === '')) { const toggleErrorTooltip = (rowCode: string, field: string) => {
process.push(`跳过: 字段 ${fieldName} 为空,跳过空字段规则`) const key = getFieldKey(rowCode, field)
return null if (hoveredErrorKey.value === key) {
} showErrorTooltip.value = false
hoveredErrorKey.value = ''
} else {
showErrorTooltip.value = true
hoveredErrorKey.value = key
}
}
const expression = formula.replace(/\[(\w+)\]/g, (_, field) => { const closeAllTooltips = () => {
const value = formData[`${row.code}_${field}`] showTooltip.value = false
return value !== undefined ? `Number(${value})` : '0' hoveredKey.value = ''
}) showErrorTooltip.value = false
hoveredErrorKey.value = ''
}
process.push(`执行公式: ${expression}`) const findEarliestFormulaForField = (
const isValid = eval(expression) 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 { const refreshHelpIcons = () => {
field: fieldName, console.log('刷新帮助图标...')
description: description, tableFormData.forEach((row: any) => {
formula: formula, if (row.content && Array.isArray(row.content)) {
isValid: isValid, row.content.forEach((item: any) => {
fieldValue: fieldValue, if (item.field) {
rowCode: row.code 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 handleValidationResultClick = (result: any) => {
const row = tableFormData.find(r => r.code === result.rowCode) const message = `字段 ${result.field} 的校验结果: ${result.isValid ? '通过' : '失败'}`
if (row) { const status = result.isValid ? 'success' : 'error'
const fieldItem = row.content.find((c: any) => c.field === result.field)
if (fieldItem) { VxeUI.modal.message({ content: message, status })
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 showHelpIcon = (rowCode: string, field: string, item: any): boolean => {
const showValidationHelp = (formulaItem: any, event: MouseEvent) => { if (item.hasValidFormula !== undefined) {
const rect = (event.target as HTMLElement).getBoundingClientRect() return item.hasValidFormula === true
validationTooltipPosition.value = {
left: rect.left + rect.width / 2,
top: rect.top - 10
} }
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 getInputStatus = (rowCode: string, itemField: string): 'default' | 'error' => {
const closeTooltip = () => { const key = getFieldKey(rowCode, itemField)
validationTooltipVisible.value = false 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 = () => { ...@@ -881,39 +954,82 @@ const closeHistoryDrawer = () => {
margin-top: 4px; margin-top: 4px;
} }
/* 验证公式帮助图标 */ .input-wrapper {
.validation-help-icon { position: relative;
display: inline-block;
}
.help-icon {
margin-left: 5px;
cursor: pointer;
color: #1890ff;
font-weight: bold;
font-size: 14px;
display: inline-block; display: inline-block;
width: 16px; width: 20px;
height: 16px; height: 20px;
line-height: 16px; line-height: 20px;
text-align: center; text-align: center;
background: #1890ff; background: #e6f7ff;
color: white;
border-radius: 50%; border-radius: 50%;
font-size: 12px; transition: all 0.3s;
margin-left: 4px;
&:hover {
background: #bae7ff;
}
}
.error-icon {
margin-left: 5px;
cursor: pointer; 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);
}
} }
/* 验证公式帮助提示 */ .tooltip,
.validation-tooltip { .error-tooltip {
position: fixed; position: absolute;
background: #333; top: 100%;
color: white; left: 0;
padding: 10px 15px; margin-top: 5px;
border-radius: 4px; padding: 10px;
font-size: 12px; font-size: 12px;
z-index: 9999; border-radius: 4px;
transform: translateX(-50%); z-index: 1000;
white-space: pre-wrap; white-space: pre-wrap;
max-width: 300px; 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 { .tooltip {
margin: 0; 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> </style>
<template> <template>
<div class="bank-report-table" @click="closeTooltip"> <div class="bank-report-table" @click="closeAllTooltips">
<!-- 加载遮罩层 --> <!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay"> <div v-if="loading" class="loading-overlay">
<div class="loading-spinner"> <div class="loading-spinner">
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
</template> </template>
<template #tools> <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="handleOpenHistoryDrawer()" :disabled="loading">近5年数据填报</vxe-button>
<vxe-button status="primary" icon="vxe-icon-edit" @click="validateAndShowResults()" :disabled="loading">校验</vxe-button> <vxe-button status="primary" icon="vxe-icon-edit" @click="validateData()" :disabled="loading">校验</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()" :disabled="loading">保存</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()" :disabled="loading">保存</vxe-button>
</template> </template>
</vxe-toolbar> </vxe-toolbar>
...@@ -142,16 +142,46 @@ ...@@ -142,16 +142,46 @@
class="table-input" class="table-input"
:style="{width:item.width}" :style="{width:item.width}"
:status="getInputStatus(row.code, item.field)" :status="getInputStatus(row.code, item.field)"
@input="handleInputChange(row.code, item.field)" @blur="handleInputBlur(row.code, item.field, item.matchedFormula?.formula)"
> >
</vxe-input> </vxe-input>
<span class="unit"> {{ item.unit }}</span> <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"> <span
{{ getErrorMessage(row.code, item.field) }} 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>
<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>
</div> </div>
</template> </template>
...@@ -168,74 +198,16 @@ ...@@ -168,74 +198,16 @@
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 校验结果抽屉 --> <!-- 历史填报检查组件 -->
<vxe-drawer <HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @closeDrawer="closeHistoryDrawer"/>
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>
<div class="result-list"> <!-- 校验抽屉 -->
<div <ValidationDrawer
v-for="(item, index) in validationResultsList" ref="validationDrawerRef"
:key="index" v-model="drawerVisible"
class="result-item" :tableFormData="tableFormData"
:class="{ success: item.isValid, error: !item.isValid }" @validationResultClick="handleValidationResultClick"
> />
<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"/>
</div> </div>
</template> </template>
...@@ -243,9 +215,10 @@ ...@@ -243,9 +215,10 @@
import MultiColumnTable from '../tableComponents/MultiColumnTable.vue' import MultiColumnTable from '../tableComponents/MultiColumnTable.vue'
import AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import HistoryFillCheck from './check/HistoryFillCheck.vue' import HistoryFillCheck from './check/HistoryFillCheck.vue'
import ValidationDrawer from './check/ValidationDrawer.vue'
import { tableFormData } from '../../data/tb3.data'; 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 { VxeUI,VxeTablePropTypes,VxeToolbarPropTypes } from 'vxe-table'
import { batchSaveOrUpdate, queryRecord,batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api' import { batchSaveOrUpdate, queryRecord,batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api'
import { findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api' import { findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
...@@ -255,6 +228,7 @@ import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api' ...@@ -255,6 +228,7 @@ import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const historyFillCheckRef = ref(); const historyFillCheckRef = ref();
const validationDrawerRef = ref();
const historyDrawerVisible = ref(false); const historyDrawerVisible = ref(false);
const route = useRoute(); const route = useRoute();
...@@ -269,11 +243,46 @@ const formData = reactive({}); ...@@ -269,11 +243,46 @@ const formData = reactive({});
const formValues = ref<FormData[]>([]) const formValues = ref<FormData[]>([])
const showTooltip = ref(false) const showTooltip = ref(false)
const hoveredKey = ref('') 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 drawerVisible = ref(false)
const validationResultsList = ref<any[]>([]) const validationResultsList = ref<any[]>([])
interface FormulaItem {
formula: string;
des: string;
[key: string]: any;
}
interface InputError {
message: string;
formula: string;
error?: string;
}
interface ValidationResult { interface ValidationResult {
fieldTitle: string fieldTitle: string
fieldName: string fieldName: string
...@@ -335,7 +344,14 @@ onMounted(async ()=>{ ...@@ -335,7 +344,14 @@ onMounted(async ()=>{
await setFormItemRight(); await setFormItemRight();
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
await validateAllInputs();
// 标记初始化完成
isInitialized.value = true
// 延迟刷新一次,确保问号图标显示
setTimeout(() => {
refreshHelpIcons()
}, 300)
} catch (error) { } catch (error) {
VxeUI.modal.message({ VxeUI.modal.message({
content: `初始化页面失败: ${error.message}`, content: `初始化页面失败: ${error.message}`,
...@@ -605,6 +621,116 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([ ...@@ -605,6 +621,116 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([
{ row: 9, col: 3, rowspan: 1, colspan: 2 }, { 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 handleInputChange = (rowCode: string, itemField: string) => {
const key = `${rowCode}_${itemField}` const key = `${rowCode}_${itemField}`
validateInput(key) validateInput(key)
...@@ -968,15 +1094,39 @@ const validateAllInputs = async () => { ...@@ -968,15 +1094,39 @@ const validateAllInputs = async () => {
} }
} }
const closeHistoryDrawer = () => { const toggleErrorTooltip = (rowCode: string, field: string) => {
historyFillCheckRef.value.closeDrawer(); const key = getFieldKey(rowCode, field);
historyDrawerVisible.value = false; 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 = () => { const handleOpenHistoryDrawer = () => {
historyFillCheckRef.value.onDrawerShow(queryParam.value.tplId); historyFillCheckRef.value?.onDrawerShow(queryParam.value.tplId);
// 设置抽屉可见
historyDrawerVisible.value = true; historyDrawerVisible.value = true;
} };
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false;
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
...@@ -1080,6 +1230,90 @@ const handleOpenHistoryDrawer = () => { ...@@ -1080,6 +1230,90 @@ const handleOpenHistoryDrawer = () => {
.vxe-checkbox-group { .vxe-checkbox-group {
display: inline-block; 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 { .bank-report-table {
font-family: "SimSun", "宋体", serif; font-family: "SimSun", "宋体", serif;
font-size: 12px; font-size: 12px;
...@@ -1160,103 +1394,11 @@ blockquote { ...@@ -1160,103 +1394,11 @@ blockquote {
cursor: pointer; 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 { .vxe-input.status--error .vxe-input--inner {
border-color: #ff4d4f !important; border-color: #ff4d4f !important;
background-color: #fff2f0 !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 { .validation-results {
padding: 16px; padding: 16px;
......
<template> <template>
<div class="bank-report-table" style="height: 80vh;" @click="closeTooltip"> <div class="bank-report-table" style="height: 80vh;" @click="closeAllTooltips">
<!-- 加载遮罩层 --> <!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay"> <div v-if="loading" class="loading-overlay">
<div class="loading-spinner"> <div class="loading-spinner">
...@@ -17,54 +17,11 @@ ...@@ -17,54 +17,11 @@
</template> </template>
<template #tools> <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="handleOpenHistoryDrawer()" :disabled="loading">近5年数据填报</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="checkData()">校验</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="validateData()">校验</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</vxe-button>
</template> </template>
</vxe-toolbar> </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 <MyVxeTable
:title="tableFormData.project" :title="tableFormData.project"
:data="tableFormData.data" :data="tableFormData.data"
...@@ -79,22 +36,22 @@ ...@@ -79,22 +36,22 @@
<!-- 历史填报检查组件 --> <!-- 历史填报检查组件 -->
<HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @closeDrawer="closeHistoryDrawer"/> <HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @closeDrawer="closeHistoryDrawer"/>
<!-- 验证公式帮助提示 --> <!-- 校验抽屉 -->
<div <ValidationDrawer
v-if="validationTooltipVisible" ref="validationDrawerRef"
class="validation-tooltip" v-model="drawerVisible"
:style="{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }" :tableFormData="tableFormData"
> @validationResultClick="handleValidationResultClick"
<pre class="tooltip-content">{{ validationTooltipContent }}</pre> />
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MyVxeTable from '../tableComponents/MyVxeTable.vue' import MyVxeTable from '../tableComponents/MyVxeTable.vue'
import HistoryFillCheck from './check/historyFillCheck.vue' import HistoryFillCheck from './check/historyFillCheck.vue'
import ValidationDrawer from './check/ValidationDrawer.vue'
import { tableFormData } from '../../data/tb4.data'; import { tableFormData } from '../../data/tb4.data';
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, nextTick, onMounted, toRaw, computed } from 'vue'
import { VxeUI } from 'vxe-table' import { VxeUI } from 'vxe-table'
import { batchSaveOrUpdateBeforeDelete, queryRecord } from '../../record/BaosongTaskRecord.api' import { batchSaveOrUpdateBeforeDelete, queryRecord } from '../../record/BaosongTaskRecord.api'
import { findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api' import { findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
...@@ -104,6 +61,7 @@ import { useRoute } from 'vue-router'; ...@@ -104,6 +61,7 @@ import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const refMyVxeTable = ref(); const refMyVxeTable = ref();
const validationDrawerRef = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null); const historyFillCheckRef = ref<any>(null);
const queryParam = ref({ const queryParam = ref({
...@@ -117,12 +75,13 @@ const queryParam = ref({ ...@@ -117,12 +75,13 @@ const queryParam = ref({
// 权限相关状态 // 权限相关状态
const userAllocItems = ref<string[]>([]) const userAllocItems = ref<string[]>([])
const validFormula = ref<any[]>([]) const validFormula = ref<any[]>([])
const validationResultsList = ref<any[]>([])
const drawerVisible = ref(false) const drawerVisible = ref(false)
const historyDrawerVisible = ref(false) const historyDrawerVisible = ref(false)
const validationTooltipVisible = ref(false) const showTooltip = ref(false)
const validationTooltipPosition = ref({ left: 0, top: 0 }) const hoveredKey = ref('')
const validationTooltipContent = ref('') const showErrorTooltip = ref(false)
const hoveredErrorKey = ref('')
const isInitialized = ref(false)
interface FormData { interface FormData {
id: number | null id: number | null
...@@ -135,6 +94,18 @@ interface FormData { ...@@ -135,6 +94,18 @@ interface FormData {
rind: number rind: number
} }
interface FormulaItem {
formula: string;
des: string;
[key: string]: any;
}
interface InputError {
message: string;
formula: string;
error?: string;
}
onMounted(async ()=>{ onMounted(async ()=>{
if (route.query.taskId) { if (route.query.taskId) {
queryParam.value.taskId = Number(route.query.taskId); queryParam.value.taskId = Number(route.query.taskId);
...@@ -167,12 +138,42 @@ onMounted(async ()=>{ ...@@ -167,12 +138,42 @@ onMounted(async ()=>{
} }
await setTplItemMap() await setTplItemMap()
await setData(); await setFormItemRight()
await setData()
// 标记初始化完成
isInitialized.value = true
// 延迟刷新一次,确保问号图标显示
setTimeout(() => {
refreshHelpIcons()
}, 300)
}) })
const loading = ref(false) const loading = ref(false)
const formData = reactive({}); const formData = reactive({});
const formValues = ref<FormData[]>([]) 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 () => { const saveBatch = async () => {
try { try {
...@@ -294,116 +295,175 @@ async function setTplItemMap() { ...@@ -294,116 +295,175 @@ async function setTplItemMap() {
} }
} }
// 校验数据 const refreshHelpIcons = () => {
const checkData = () => { console.log('刷新帮助图标...');
drawerVisible.value = true tableFormData.columns.forEach((column: any) => {
} if (column.field) {
const key = getFieldKey(tableFormData.code, column.field);
// 校验结果抽屉显示时触发 const hasRight = userAllocItems.value.includes(key);
const handleValidationDrawerShow = () => {
performValidation() if (hasRight) {
} const formulaResult = findEarliestFormulaForField(validFormula.value, column.field);
column.hasValidFormula = !!formulaResult.formula;
// 执行校验 column.matchedFormula = formulaResult.formula || null;
const performValidation = () => { } else {
validationResultsList.value = [] column.hasValidFormula = false;
const process: string[] = [] column.matchedFormula = null;
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('校验完成')
} nextTick(() => {
console.log('帮助图标刷新完成');
// 解析和执行验证公式 });
const evaluateFormula = (formula: string, description: string, process: string[]): any => { };
try {
const fieldMatch = formula.match(/\[(\w+)\]/) const setFormItemRight = () => {
if (!fieldMatch) { tableFormData.columns.forEach((column: any) => {
process.push(`跳过: 无法解析公式字段 ${formula}`) if (column.field) {
return null 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 findEarliestFormulaForField = (
formulas: FormulaItem[],
// 检查是否有空字段规则 field: string
const emptyFieldRule = validFormula.value.find((f: any) => ): { formula: FormulaItem | null, index: number } => {
f.formula.includes(fieldName) && f.des.includes('空字段') let result = { formula: null, index: Infinity };
)
formulas?.forEach((f: FormulaItem) => {
if (emptyFieldRule && (!fieldValue || fieldValue === '')) { const text = f.formula || '';
process.push(`跳过: 字段 ${fieldName} 为空,跳过空字段规则`) const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return null 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 validateData = () => {
const value = formData[`${tableFormData.code}_${field}`] drawerVisible.value = true;
return value !== undefined ? `Number(${value})` : '0' validationDrawerRef.value.setValidateData(validFormula.value, formData);
}) };
process.push(`执行公式: ${expression}`) const handleInputBlur = (rowCode: string, field: string, formula?: string) => {
const isValid = eval(expression) 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 { const validateFieldFormula = (rowCode: string, field: string, formula: string) => {
field: fieldName, const key = getFieldKey(rowCode, field);
description: description,
formula: formula, try {
isValid: isValid, const expression = buildExpression(formula, rowCode);
fieldValue: fieldValue, const isValid = eval(expression);
rowCode: tableFormData.code if (!isValid) {
inputErrors.value[key] = {
message: "公式校验失败,请检查填写内容",
formula
};
} else {
delete inputErrors.value[key];
} }
} catch (error) { } catch (error: any) {
process.push(`公式执行错误: ${error instanceof Error ? error.message : String(error)}`) inputErrors.value[key] = {
return null message: "公式校验失败,请检查填写内容",
formula,
error: error.message
};
} }
} };
// 处理校验结果点击,定位到表格 const buildExpression = (formula: string, rowCode: string): string => {
const handleValidationResultClick = (result: any) => { const fieldNames = formula.match(/\b[A-Za-z][A-Za-z0-9_]*\b/g) || [];
const strKey = `${result.rowCode}_${result.field}` const uniqueFields = [...new Set(fieldNames.filter(f =>
const inputElement = document.querySelector(`[data-field="${strKey}"]`) as HTMLElement !['and', 'or', 'not', 'equal', 'less', 'greater', 'if', 'else', 'true', 'false']
if (inputElement) { .includes(f.toLowerCase())
inputElement.scrollIntoView({ behavior: 'smooth', block: 'center' }) ))];
inputElement.focus()
inputElement.style.border = '2px solid #ff4d4f' uniqueFields.sort((a, b) => b.length - a.length);
setTimeout(() => {
inputElement.style.border = '' let expression = formula;
}, 2000) 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 showValidationHelp = (formulaItem: any, event: MouseEvent) => {
const rect = (event.target as HTMLElement).getBoundingClientRect() const toggleTooltip = (rowCode: string, field: string) => {
validationTooltipPosition.value = { const key = getFieldKey(rowCode, field);
left: rect.left + rect.width / 2, if (hoveredKey.value === key) {
top: rect.top - 10 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 closeAllTooltips = () => {
const closeTooltip = () => { showTooltip.value = false;
validationTooltipVisible.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 = () => { const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true historyFillCheckRef.value?.onDrawerShow(queryParam.value.tplId);
historyDrawerVisible.value = true;
} }
// 关闭历史填报抽屉 // 关闭历史填报抽屉
const closeHistoryDrawer = () => { const closeHistoryDrawer = () => {
historyDrawerVisible.value = false historyDrawerVisible.value = false;
} }
</script> </script>
...@@ -486,112 +546,4 @@ const closeHistoryDrawer = () => { ...@@ -486,112 +546,4 @@ const closeHistoryDrawer = () => {
font-size: 14px; font-size: 14px;
color: #666; 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> </style>
\ No newline at end of file
<template> <template>
<div class="bank-report-table" style="height: 80vh" @click="closeTooltip"> <div class="bank-report-table" style="height: 80vh" @click="closeAllTooltips">
<!-- 加载遮罩层 --> <!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay"> <div v-if="loading" class="loading-overlay">
<div class="loading-spinner"> <div class="loading-spinner">
...@@ -17,46 +17,11 @@ ...@@ -17,46 +17,11 @@
</template> </template>
<template #tools> <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="handleOpenHistoryDrawer()" :disabled="loading">近5年数据填报</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="checkData()">校验</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="validateData()">校验</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</vxe-button> <vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</vxe-button>
</template> </template>
</vxe-toolbar> </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 <vxe-table
border border
ref="tableRef" ref="tableRef"
...@@ -189,8 +154,59 @@ ...@@ -189,8 +154,59 @@
</span> </span>
</div> </div>
<template v-else> <template v-else>
<vxe-input :type="item.type" v-model="formData[row.code + '_' + item.field]" size="mini" class="table-input"> </vxe-input> <div class="input-wrapper">
<span class="unit"> {{ item.placeholder }}</span> <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> </template>
</template> </template>
...@@ -202,14 +218,13 @@ ...@@ -202,14 +218,13 @@
<!-- 历史填报检查组件 --> <!-- 历史填报检查组件 -->
<HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @close-drawer="closeHistoryDrawer" /> <HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @close-drawer="closeHistoryDrawer" />
<!-- 验证公式帮助提示 --> <!-- 校验抽屉 -->
<div <ValidationDrawer
v-if="validationTooltipVisible" ref="validationDrawerRef"
class="validation-tooltip" v-model="drawerVisible"
:style="{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }" :tableFormData="tableFormData"
> @validationResultClick="handleValidationResultClick"
<pre class="tooltip-content">{{ validationTooltipContent }}</pre> />
</div>
</div> </div>
</template> </template>
...@@ -217,9 +232,10 @@ ...@@ -217,9 +232,10 @@
import MultiColumnTable from '../tableComponents/MultiColumnTable.vue'; import MultiColumnTable from '../tableComponents/MultiColumnTable.vue';
import AttachTable from '../tableComponents/AttachTable.vue'; import AttachTable from '../tableComponents/AttachTable.vue';
import HistoryFillCheck from './check/historyFillCheck.vue'; import HistoryFillCheck from './check/historyFillCheck.vue';
import ValidationDrawer from './check/ValidationDrawer.vue';
import { tableFormData } from '../../data/tb5.data'; 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 { VxeUI, VxeToolbarInstance, VxeToolbarPropTypes } from 'vxe-table';
import { batchSaveOrUpdate, queryRecord, batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api'; import { batchSaveOrUpdate, queryRecord, batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api';
import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'; import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api';
...@@ -229,8 +245,9 @@ ...@@ -229,8 +245,9 @@
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null); const historyFillCheckRef = ref<any>(null);
const validationDrawerRef = ref();
const tplItemMap = ref({});
const queryParam = ref({ const queryParam = ref({
taskId: -1, taskId: -1,
taskName: '', taskName: '',
...@@ -242,12 +259,12 @@ ...@@ -242,12 +259,12 @@
// 权限相关状态 // 权限相关状态
const userAllocItems = ref<string[]>([]); const userAllocItems = ref<string[]>([]);
const validFormula = ref<any[]>([]); const validFormula = ref<any[]>([]);
const validationResultsList = ref<any[]>([]);
const drawerVisible = ref(false); const drawerVisible = ref(false);
const historyDrawerVisible = ref(false); const historyDrawerVisible = ref(false);
const validationTooltipVisible = ref(false); const showTooltip = ref(false);
const validationTooltipPosition = ref({ left: 0, top: 0 }); const hoveredKey = ref('');
const validationTooltipContent = ref(''); const showErrorTooltip = ref(false);
const hoveredErrorKey = ref('');
interface FormData { interface FormData {
id: number | null; id: number | null;
...@@ -260,6 +277,18 @@ ...@@ -260,6 +277,18 @@
rind: number; rind: number;
} }
interface FormulaItem {
formula: string;
des: string;
[key: string]: any;
}
interface InputError {
message: string;
formula: string;
error?: string;
}
onMounted(async () => { onMounted(async () => {
if (route.query.taskId) { if (route.query.taskId) {
queryParam.value.taskId = Number(route.query.taskId); queryParam.value.taskId = Number(route.query.taskId);
...@@ -292,16 +321,48 @@ ...@@ -292,16 +321,48 @@
} }
await setTplItemMap(); await setTplItemMap();
await setFormItemRight();
await setData(); await setData();
// 标记初始化完成
isInitialized.value = true;
// 延迟刷新一次,确保问号图标显示
setTimeout(() => {
refreshHelpIcons();
}, 300);
}); });
const loading = ref(false); const loading = ref(false);
const isInitialized = ref(false);
const formData = reactive({}); const formData = reactive({});
const formValues = ref<FormData[]>([]); 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 () => { const saveBatch = async () => {
try { try {
if (!await validateForm()) return;
formValues.value = []; formValues.value = [];
for (const strKey in formData) { for (const strKey in formData) {
...@@ -318,17 +379,28 @@ ...@@ -318,17 +379,28 @@
if (formValues.value.length > 0) { if (formValues.value.length > 0) {
await batchSaveOrUpdateBeforeDelete(formValues.value); await batchSaveOrUpdateBeforeDelete(formValues.value);
VxeUI.modal.message({ content: '保存成功', status: 'success' }); VxeUI.modal.message({ content: '保存成功', status: 'success' });
//await setData()
} else { } else {
VxeUI.modal.message({ content: '没有需要保存的数据', status: 'warning' }); VxeUI.modal.message({ content: '没有需要保存的数据', status: 'warning' });
} }
} catch (error) { } catch (error: any) {
VxeUI.modal.message({ content: `保存失败: ${error.message}`, status: 'error' }); VxeUI.modal.message({ content: `保存失败: ${error.message}`, status: 'error' });
} finally { } finally {
loading.value = false; 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 childMultiTableRefs = ref({});
const setMultiColumnTableRef = (el: any, index: string) => { const setMultiColumnTableRef = (el: any, index: string) => {
if (el) { if (el) {
...@@ -520,132 +592,184 @@ ...@@ -520,132 +592,184 @@
} }
// 校验数据 // 校验数据
const checkData = () => { const validateData = () => {
drawerVisible.value = true; drawerVisible.value = true;
validationDrawerRef.value.setValidateData(validFormula.value,formData);
}; };
// 校验结果抽屉显示时触发 const handleInputBlur = (rowCode: string, field: string, formula?: string) => {
const handleValidationDrawerShow = () => { if (!formula) return;
performValidation();
}; const key = getFieldKey(rowCode, field);
const value = formData[key];
// 执行校验
const performValidation = () => { if (!value || value === '') {
validationResultsList.value = []; delete inputErrors.value[key];
const process: string[] = []; return;
process.push('开始校验'); }
process.push(`找到 ${validFormula.value.length} 个验证公式`);
validateFieldFormula(rowCode, field, formula);
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 validateFieldFormula = (rowCode: string, field: string, formula: string) => {
const evaluateFormula = (formula: string, description: string, process: string[]): any => { const key = getFieldKey(rowCode, field);
try { try {
const fieldMatch = formula.match(/\[(\w+)\]/); const expression = buildExpression(formula, rowCode);
if (!fieldMatch) { const isValid = eval(expression);
process.push(`跳过: 无法解析公式字段 ${formula}`); if (!isValid) {
return null; inputErrors.value[key] = {
} message: "公式校验失败,请检查填写内容",
formula
const fieldName = fieldMatch[1]; };
const row = tableFormData.find((r: any) => r.content?.some((c: any) => c.field === fieldName)); } else {
if (!row) { delete inputErrors.value[key];
process.push(`跳过: 未找到字段 ${fieldName}`);
return null;
}
const fieldItem = row.content.find((c: any) => c.field === fieldName);
if (!fieldItem) {
process.push(`跳过: 未找到字段 ${fieldName}`);
return null;
} }
} catch (error: any) {
inputErrors.value[key] = {
message: "公式校验失败,请检查填写内容",
formula,
error: error.message
};
}
};
const strKey = `${row.code}_${fieldName}`; const buildExpression = (formula: string, rowCode: string): string => {
const fieldValue = formData[strKey]; 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']
const emptyFieldRule = validFormula.value.find((f: any) => f.formula.includes(fieldName) && f.des.includes('空字段')); .includes(f.toLowerCase())
))];
if (emptyFieldRule && (!fieldValue || fieldValue === '')) {
process.push(`跳过: 字段 ${fieldName} 为空,跳过空字段规则`); uniqueFields.sort((a, b) => b.length - a.length);
return null;
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 toggleTooltip = (rowCode: string, field: string) => {
const value = formData[`${row.code}_${field}`]; const key = getFieldKey(rowCode, field);
return value !== undefined ? `Number(${value})` : '0'; if (hoveredKey.value === key) {
}); showTooltip.value = false;
hoveredKey.value = '';
process.push(`执行公式: ${expression}`); } else {
const isValid = eval(expression); showTooltip.value = true;
hoveredKey.value = key;
}
};
return { const toggleErrorTooltip = (rowCode: string, field: string) => {
field: fieldName, const key = getFieldKey(rowCode, field);
description: description, if (hoveredErrorKey.value === key) {
formula: formula, showErrorTooltip.value = false;
isValid: isValid, hoveredErrorKey.value = '';
fieldValue: fieldValue, } else {
rowCode: row.code, showErrorTooltip.value = true;
}; hoveredErrorKey.value = key;
} catch (error) {
process.push(`公式执行错误: ${error instanceof Error ? error.message : String(error)}`);
return null;
} }
}; };
// 处理校验结果点击,定位到表格 const closeAllTooltips = () => {
const handleValidationResultClick = (result: any) => { showTooltip.value = false;
const row = tableFormData.find((r: any) => r.code === result.rowCode); hoveredKey.value = '';
if (row) { showErrorTooltip.value = false;
const fieldItem = row.content.find((c: any) => c.field === result.field); hoveredErrorKey.value = '';
if (fieldItem) { };
const strKey = `${row.code}_${result.field}`;
const inputElement = document.querySelector(`[data-field="${strKey}"]`) as HTMLElement; const refreshHelpIcons = () => {
if (inputElement) { console.log('刷新帮助图标...');
inputElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); tableFormData.forEach((row: any) => {
inputElement.focus(); if (row.content && Array.isArray(row.content)) {
inputElement.style.border = '2px solid #ff4d4f'; row.content.forEach((item: any) => {
setTimeout(() => { if (item.field) {
inputElement.style.border = ''; const key = getFieldKey(row.code, item.field);
}, 2000); 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 setFormItemRight = () => {
const showValidationHelp = (formulaItem: any, event: MouseEvent) => { tableFormData.forEach((row: any) => {
const rect = (event.target as HTMLElement).getBoundingClientRect(); if (row.content && Array.isArray(row.content)) {
validationTooltipPosition.value = { row.content.forEach((item: any) => {
left: rect.left + rect.width / 2, if (item.field) {
top: rect.top - 10, const key = getFieldKey(row.code, item.field);
}; item.hasRight = userAllocItems.value.includes(key);
validationTooltipContent.value = `验证公式: ${formulaItem.formula}\n说明: ${formulaItem.des}`;
validationTooltipVisible.value = true; if (item.hasRight) {
const { formula } = findEarliestFormulaForField(validFormula.value, item.field);
item.hasValidFormula = !!formula;
item.matchedFormula = formula;
}
}
});
}
});
}; };
// 关闭验证公式帮助 const findEarliestFormulaForField = (
const closeTooltip = () => { formulas: FormulaItem[],
validationTooltipVisible.value = false; 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 = () => { const handleOpenHistoryDrawer = () => {
historyFillCheckRef.value?.onDrawerShow(queryParam.value.tplId);
historyDrawerVisible.value = true; historyDrawerVisible.value = true;
}; };
// 关闭历史填报抽屉
const closeHistoryDrawer = () => { const closeHistoryDrawer = () => {
historyDrawerVisible.value = false; 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> </script>
<style lang="less" scoped> <style lang="less" scoped>
...@@ -682,6 +806,89 @@ ...@@ -682,6 +806,89 @@
margin: 0 2px; 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 { .radio-group {
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
...@@ -739,111 +946,4 @@ ...@@ -739,111 +946,4 @@
font-size: 14px; font-size: 14px;
color: #666; 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> </style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论