提交 22db19e3 authored 作者: kxjia's avatar kxjia

修改校验

上级 1e9a3c5d
<template> <template>
<div class="bank-report-table" style="height:80vh"> <div class="bank-report-table" style="height:80vh" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #buttons>
<div style="margin:10px"> <div style="margin:10px">
...@@ -8,8 +16,9 @@ ...@@ -8,8 +16,9 @@
</div> </div>
</template> </template>
<template #tools> <template #tools>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</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="checkData()">校验</vxe-button>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</vxe-button>
</template> </template>
</vxe-toolbar> </vxe-toolbar>
<vxe-table <vxe-table
...@@ -50,6 +59,7 @@ ...@@ -50,6 +59,7 @@
:calcSum="row.calcSum" :calcSum="row.calcSum"
:showFooter="row.showFooter" :showFooter="row.showFooter"
:ref="(el) => setAttachTableRef(el, row.code)" :ref="(el) => setAttachTableRef(el, row.code)"
:disabled="!row.hasRight"
style="margin:0px;padding:0px" style="margin:0px;padding:0px"
/> />
</template> </template>
...@@ -62,6 +72,7 @@ ...@@ -62,6 +72,7 @@
:footerData="row.footerData" :footerData="row.footerData"
:showFooter="row.showFooter" :showFooter="row.showFooter"
:ref="(el) => setMyVxeTableRef(el, row.code)" :ref="(el) => setMyVxeTableRef(el, row.code)"
:disabled="!row.hasRight"
style="margin:0px;padding:0px" style="margin:0px;padding:0px"
/> />
</template> </template>
...@@ -78,7 +89,7 @@ ...@@ -78,7 +89,7 @@
<span v-else-if="item.type === 'radio-group'" class="radio-group"> <span v-else-if="item.type === 'radio-group'" class="radio-group">
<vxe-radio-group v-model="formData[row.code +'_'+ item.field]" <vxe-radio-group v-model="formData[row.code +'_'+ item.field]"
:disabled="item.relatedFiled&&formData[row.code +'_'+ item.relatedFiled]!='是'" :disabled="!item.hasRight || (item.relatedFiled&&formData[row.code +'_'+ item.relatedFiled]!='是')"
> >
<vxe-radio <vxe-radio
v-for="(opt, optIndex) in item.options" v-for="(opt, optIndex) in item.options"
...@@ -90,17 +101,17 @@ ...@@ -90,17 +101,17 @@
</span> </span>
<div v-else-if="item.type === 'checkbox-group'"> <div v-else-if="item.type === 'checkbox-group'">
<span class="checkbox-group" > <span class="checkbox-group" >
<vxe-checkbox-group v-model="formData[row.code+'_'+item.field]"> <vxe-checkbox-group v-model="formData[row.code+'_'+item.field]" :disabled="!item.hasRight">
<vxe-checkbox v-for="(opt, optIndex) in item.options" :key="optIndex" :label="opt"> <vxe-checkbox v-for="(opt, optIndex) in item.options" :key="optIndex" :label="opt">
{{ opt }} {{ opt }}
</vxe-checkbox> </vxe-checkbox>
<template v-if="item.otherField"> <template v-if="item.otherField">
<div style="width:100%"> <div style="width:100%">
<vxe-checkbox label="其他"> <vxe-checkbox label="其他" :disabled="!item.hasRight">
其他: 其他:
</vxe-checkbox> </vxe-checkbox>
<vxe-input v-model="formData[row.code + '_'+ item.otherField]" <vxe-input v-model="formData[row.code + '_'+ item.otherField]"
:disabled="!(formData[row.code+'_'+item.field]?.indexOf('其他')>-1)" :disabled="!(formData[row.code+'_'+item.field]?.indexOf('其他')>-1) || !item.hasRight"
style="width: 50%" style="width: 50%"
> >
</vxe-input> </vxe-input>
...@@ -112,10 +123,10 @@ ...@@ -112,10 +123,10 @@
<div v-else-if="item.type === 'checkboxAndInput'"> <div v-else-if="item.type === 'checkboxAndInput'">
<span style="margin-left:20px"> <span style="margin-left:20px">
<vxe-checkbox-group v-model="formData[row.code+'_'+item.field]"> <vxe-checkbox-group v-model="formData[row.code+'_'+item.field]" :disabled="!item.hasRight">
<vxe-checkbox v-for="(opt, optIndex) in item.options" :key="optIndex" :label="opt" style="margin-left: 0px;display: block;"> <vxe-checkbox v-for="(opt, optIndex) in item.options" :key="optIndex" :label="opt" :disabled="!item.hasRight" style="margin-left: 0px;display: block;">
{{ opt }} {{ opt }}
<vxe-input type="number" v-model="formData[row.code + '_'+ item.optionItemField[optIndex]]" style="width: 100px;margin: 0px;"> <vxe-input type="number" v-model="formData[row.code + '_'+ item.optionItemField[optIndex]]" :disabled="!item.hasRight" style="width: 100px;margin: 0px;">
</vxe-input> </vxe-input>
<span class="unit"> {{ item.optionItemUint }}</span> <span class="unit"> {{ item.optionItemUint }}</span>
</vxe-checkbox> </vxe-checkbox>
...@@ -123,18 +134,27 @@ ...@@ -123,18 +134,27 @@
</span> </span>
</div> </div>
<template v-else> <template v-else>
<vxe-input <div class="input-wrapper">
:disabled="item.relatedFiled&&formData[row.code +'_'+ item.relatedFiled]!='是'" <vxe-input
:type="item.type" :disabled="!item.hasRight || (item.relatedFiled&&formData[row.code +'_'+ item.relatedFiled]!='是')"
v-model="formData[row.code +'_'+ item.field]" :type="item.type"
size="mini" v-model="formData[row.code +'_'+ item.field]"
class="table-input" size="mini"
:style="item.style" class="table-input"
> :style="item.style"
</vxe-input> :status="getInputStatus(row.code, item.field)"
<span class="unit"> {{ item.unit }}</span> @input="handleInputChange(row.code, item.field)"
>
</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) }}
</div>
<div v-if="showTooltip && hoveredKey === row.code +'_'+ item.field" class="tooltip" @click.stop>
{{ getValidationRule(row.code, item.field) }}
</div>
</div>
</template> </template>
</template> </template>
</template> </template>
...@@ -149,6 +169,74 @@ ...@@ -149,6 +169,74 @@
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 校验结果抽屉 -->
<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="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"/>
</div> </div>
</template> </template>
...@@ -156,21 +244,47 @@ ...@@ -156,21 +244,47 @@
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 MyVxeTable1 from '../tableComponents/MyVxeTable.vue' import MyVxeTable1 from '../tableComponents/MyVxeTable.vue'
import HistoryFillCheck from './check/HistoryFillCheck.vue'
import { tableFormData } from '../../data/tb1.data'; import { tableFormData } from '../../data/tb1.data';
import { ref, reactive,nextTick,onMounted,toRaw } from 'vue' import { ref, reactive,nextTick,onMounted,toRaw } 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 } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const refCheckTbData = ref(); const historyFillCheckRef = ref();
const historyDrawerVisible = ref(false);
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const userAllocItems = ref([])
const validFormula = ref([])
const validationResults = ref<Record<string, string>>({})
const validationMessages = ref<Record<string, string>>({})
const formData = reactive({});
const formValues = ref<FormData[]>([])
const showTooltip = ref(false)
const hoveredKey = ref('')
// 校验结果抽屉相关
const drawerVisible = ref(false)
const validationResultsList = ref<any[]>([])
interface ValidationResult {
fieldTitle: string
fieldName: string
formula: string
isValid: boolean
resultMessage: string
process?: string[]
}
const queryParam = ref({ const queryParam = ref({
taskId:-1, taskId:-1,
taskName:'', taskName:'',
...@@ -190,6 +304,9 @@ interface FormData { ...@@ -190,6 +304,9 @@ interface FormData {
rind:number rind:number
} }
// 初始化loading状态
const loading = ref(true)
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);
...@@ -198,31 +315,71 @@ onMounted(async ()=>{ ...@@ -198,31 +315,71 @@ onMounted(async ()=>{
queryParam.value.taskName = String(route.query.taskName); queryParam.value.taskName = String(route.query.taskName);
} }
if (route.query.tplId) { if (route.query.tplId) {
queryParam.value.tplId = Number(route.query.tplId); queryParam.value.tplId = Number(route.query.tplId);
} }
if (route.query.tplName) { if (route.query.tplName) {
queryParam.value.tplName = String(route.query.tplName); queryParam.value.tplName = String(route.query.tplName);
} }
if (route.query.tplCode) { if (route.query.tplCode) {
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
await setTplItemMap() try {
await setData(); userAllocItems.value = await findUserRightForTplItem({
}) tplid:queryParam.value.tplId,
taskid:queryParam.value.taskId
});
const loading = ref(false)
const formData = reactive({});
const formValues = ref<FormData[]>([]) validFormula.value = await getTblvalidFormula({
tplid:queryParam.value.tplId,
});
await setFormItemRight();
await setTplItemMap()
await setData();
} catch (error) {
VxeUI.modal.message({
content: `初始化页面失败: ${error.message}`,
status: 'error'
});
} finally {
loading.value = false
}
})
const setFormItemRight = async () => {
tableFormData.forEach(row => {
if (row.content && Array.isArray(row.content)) {
row.content.forEach(item => {
if (item.field) {
let tmpKey = `${row.code}_${item.field}`;
item["hasRight"] = userAllocItems.value.indexOf(tmpKey)>-1
item["hasValidFormula"] = validFormula.value?.length > 0 &&
validFormula.value.some((f: any) => {
const formulaText = (f.formula || '').toString()
return formulaText.includes(item.field)
})
}
});
}
});
}
const saveBatch = async () => { const saveBatch = async () => {
try { try {
const saveLoading = ref(true)
formValues.value = [] formValues.value = []
await getFillDatas()
if(formValues.value.length > 0) { for (const strKey in formData) {
const valData = formData[strKey]
if (valData) {
let tmpAry = strKey.split("_")
await setFormValues(tmpAry[0],tmpAry[1],valData,1)
}
}
await getAttachTableFormData();
await getAllMultiTableFormData()
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' })
} else { } else {
...@@ -235,6 +392,235 @@ const saveBatch = async () => { ...@@ -235,6 +392,235 @@ const saveBatch = async () => {
} }
} }
const handleOpenHistoryDrawer = () => {
historyFillCheckRef.value.onDrawerShow(queryParam.value.tplId);
historyDrawerVisible.value = true;
}
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false;
}
const checkData = async () => {
validationResultsList.value = []
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)
}
})
process.push('校验完成')
}
const evaluateFormula = (formula: string, formulaDes: string, process: string[]): ValidationResult | null => {
const fieldRegex = /COL\d+/g
const fields = [...new Set(formula.match(fieldRegex) || [])]
process.push(`公式包含字段: ${fields.join(', ')}`)
const fieldValues: Record<string, any> = {}
let hasEmptyField = false
for (const field of fields) {
const value = getFieldValue(field)
fieldValues[field] = value
if (value === '' || value === null || value === undefined) {
hasEmptyField = true
process.push(`字段 ${field} 值为空,跳过此公式校验`)
}
}
if (hasEmptyField) {
return {
fieldTitle: formulaDes,
fieldName: fields.join(', '),
formula,
isValid: true,
resultMessage: '跳过校验 (有字段值为空)',
process: [...process]
}
}
process.push('所有字段值都已获取,开始计算')
process.push(`字段值: ${JSON.stringify(fieldValues)}`)
try {
const sanitizedFormula = sanitizeAndEvaluate(formula, fieldValues, process)
process.push(`计算公式: ${sanitizedFormula}`)
const result = evaluateString(sanitizedFormula)
process.push(`计算结果: ${result}`)
if (result) {
return {
fieldTitle: formulaDes,
fieldName: fields.join(', '),
formula,
isValid: true,
resultMessage: '校验通过',
fieldValues,
process: [...process]
}
} else {
return {
fieldTitle: formulaDes,
fieldName: fields.join(', '),
formula,
isValid: false,
resultMessage: '校验失败 - 验证公式返回 false',
fieldValues,
process: [...process]
}
}
} catch (error) {
process.push(`计算出错: ${error instanceof Error ? error.message : '未知错误'}`)
return {
fieldTitle: formulaDes,
fieldName: fields.join(', '),
formula,
isValid: false,
resultMessage: `校验出错: ${error instanceof Error ? error.message : '未知错误'}`,
fieldValues,
process: [...process]
}
}
}
const getFieldValue = (field: string): any => {
for (const row of tableFormData) {
if (row.content && Array.isArray(row.content)) {
for (const item of row.content) {
if (item.field === field) {
return formData[`${row.code}_${field}`]
}
}
}
}
return null
}
const sanitizeAndEvaluate = (formula: string, fieldValues: Record<string, any>, process: string[]): string => {
let sanitizedFormula = formula
const wordBoundaryRegex = /COL\d+/g
sanitizedFormula = sanitizedFormula.replace(wordBoundaryRegex, (match) => {
const value = fieldValues[match]
process.push(`替换 ${match} = ${value}`)
return value !== '' && value !== null && value !== undefined ? String(value) : 'null'
})
sanitizedFormula = sanitizedFormula.replace(/==/g, '===')
sanitizedFormula = sanitizedFormula.replace(/!==/g, '!==')
return sanitizedFormula
}
const evaluateString = (str: string): any => {
try {
const func = new Function('return ' + str)
return func()
} catch (error) {
throw new Error(`公式计算错误: ${str}`)
}
}
const getInputStatus = (rowCode: string, field: string): 'success' | 'error' | '' => {
const key = `${rowCode}_${field}`
return validationResults.value[key] === 'error' ? 'error' : ''
}
const getErrorMessage = (rowCode: string, field: string): string => {
const key = `${rowCode}_${field}`
return validationMessages.value[key] || ''
}
const handleInputChange = (rowCode: string, field: string) => {
validateField(rowCode, field)
}
const validateField = (rowCode: string, field: string) => {
const key = `${rowCode}_${field}`
validationResults.value[key] = ''
validationMessages.value[key] = ''
const relevantFormulas = validFormula.value.filter((f: any) =>
f.formula.includes(field)
)
if (relevantFormulas.length > 0) {
const formula = relevantFormulas[0]
const fieldValues: Record<string, any> = {}
const fieldRegex = /COL\d+/g
const fields = [...new Set(formula.formula.match(fieldRegex) || [])]
let hasEmptyField = false
for (const fld of fields) {
const value = getFieldValue(fld)
fieldValues[fld] = value
if (value === '' || value === null || value === undefined) {
hasEmptyField = true
}
}
if (!hasEmptyField) {
try {
const sanitizedFormula = sanitizeAndEvaluate(formula.formula, fieldValues, [])
const result = evaluateString(sanitizedFormula)
if (!result) {
validationResults.value[key] = 'error'
validationMessages.value[key] = `校验失败: ${formula.des}`
}
} catch (error) {
validationResults.value[key] = 'error'
validationMessages.value[key] = `校验错误: ${error instanceof Error ? error.message : '未知错误'}`
}
}
}
}
const toggleTooltip = (rowCode: string, field: string) => {
const key = `${rowCode}_${field}`
if (hoveredKey.value === key) {
showTooltip.value = false
hoveredKey.value = ''
} else {
showTooltip.value = true
hoveredKey.value = key
}
}
const closeTooltip = () => {
showTooltip.value = false
hoveredKey.value = ''
}
const getValidationRule = (rowCode: string, field: string): string => {
const relevantFormulas = validFormula.value.filter((f: any) =>
f.formula.includes(field)
)
if (relevantFormulas.length > 0) {
return `${relevantFormulas[0].des}\n公式: ${relevantFormulas[0].formula}`
}
return '暂无校验规则'
}
const formatJSON = (obj: any): string => {
return JSON.stringify(obj, null, 2)
}
const getFillDatas = async() => { const getFillDatas = async() => {
try { try {
...@@ -353,16 +739,6 @@ const setFormValues = async (pcode,code,valData,rind) => { ...@@ -353,16 +739,6 @@ const setFormValues = async (pcode,code,valData,rind) => {
formValues.value.push(tempForm) formValues.value.push(tempForm)
} }
async function checkData() {
let tplName = queryParam.value.tplName;
await getFillDatas();
if(formValues.value.length > 0) {
refCheckTbData.value.setIniData(tplName,formValues.value)
}
}
async function setData() { async function setData() {
try { try {
loading.value = true; loading.value = true;
...@@ -518,7 +894,6 @@ async function setData() { ...@@ -518,7 +894,6 @@ async function setData() {
} }
} }
async function setTplItemMap() { async function setTplItemMap() {
try { try {
...@@ -615,4 +990,243 @@ async function setTplItemMap() { ...@@ -615,4 +990,243 @@ async function setTplItemMap() {
border-right: none; border-right: none;
background: transparent; background: transparent;
} }
</style>
\ No newline at end of file .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%;
}
.error-tip {
position: absolute;
bottom: -20px;
left: 0;
color: #f5222d;
font-size: 12px;
}
.tooltip {
position: absolute;
top: 100%;
left: 0;
margin-top: 5px;
padding: 10px;
background: #333;
color: #fff;
font-size: 12px;
border-radius: 4px;
z-index: 1000;
white-space: pre-wrap;
max-width: 300px;
}
.tooltip::before {
content: '';
position: absolute;
bottom: 100%;
left: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #333 transparent;
}
.validation-results {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.result-summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.summary-item {
display: flex;
align-items: center;
gap: 5px;
}
.summary-value {
font-weight: bold;
font-size: 16px;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #f5222d;
}
.result-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.result-item {
padding: 10px;
border: 1px solid #e8e8e8;
border-radius: 4px;
}
.result-item.success {
border-left: 4px solid #52c41a;
}
.result-item.error {
border-left: 4px solid #f5222d;
}
.result-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.result-item-name {
font-weight: bold;
font-size: 14px;
}
.result-item-status {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.result-item-status.success {
background: #f6ffed;
color: #52c41a;
}
.result-item-status.error {
background: #fff2f0;
color: #f5222d;
}
.result-item-content {
font-size: 13px;
}
.result-item-formula,
.result-item-result,
.result-item-values,
.result-item-process {
margin-bottom: 8px;
}
.result-item-values {
margin-bottom: 15px;
}
.values-label,
.formula-label,
.result-label,
.process-label {
font-weight: bold;
margin-right: 5px;
}
.values-value {
white-space: pre-wrap;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
margin-top: 5px;
}
.process-content {
margin-top: 5px;
background: #fafafa;
padding: 8px;
border-radius: 4px;
}
.process-step {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 4px;
font-size: 12px;
}
.step-number {
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background: #e8e8e8;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 14px;
color: #666;
}
</style>
<template> <template>
<div class="bank-report-table" style="height:80vh"> <div class="bank-report-table" style="height:80vh" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #button>
<div class="toolbar-info"> <div class="toolbar-info">
<span class="info-label">填报任务:{{ queryParam?.taskName }}</span> <span class="info-label">填报任务:{{ queryParam?.taskName }}</span>
<span class="info-label">表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span> <span class="info-label">表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span>
</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-save" @click="checkData()">校验</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"
...@@ -197,21 +251,34 @@ ...@@ -197,21 +251,34 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
</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 AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import HistoryFillCheck from './check/historyFillCheck.vue'
import { tableFormData } from '../../data/tb10.data' import { tableFormData } from '../../data/tb10.data'
import { ref, reactive, nextTick, onMounted } from 'vue' import { ref, reactive, nextTick, onMounted } from 'vue'
import { VxeUI,VxeTablePropTypes } from 'vxe-table' import { VxeUI,VxeTablePropTypes } from 'vxe-table'
import { batchSaveOrUpdateBeforeDelete, queryRecord } from '../../record/BaosongTaskRecord.api' import { batchSaveOrUpdateBeforeDelete, queryRecord } from '../../record/BaosongTaskRecord.api'
import { queryAllTplItemForUser } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
interface FormData { interface FormData {
id: number | null id: number | null
taskid: number taskid: number
...@@ -226,6 +293,7 @@ interface FormData { ...@@ -226,6 +293,7 @@ interface FormData {
const route = useRoute() const route = useRoute()
const tableRef = ref() const tableRef = ref()
const tplItemMap = ref<Record<string, any>>({}) const tplItemMap = ref<Record<string, any>>({})
const historyFillCheckRef = ref<any>(null)
const loading = ref(false) const loading = ref(false)
const formData = reactive<Record<string, any>>({}) const formData = reactive<Record<string, any>>({})
const formValues = ref<FormData[]>([]) const formValues = ref<FormData[]>([])
...@@ -240,6 +308,16 @@ const queryParam = ref({ ...@@ -240,6 +308,16 @@ const queryParam = ref({
tplCode: '' tplCode: ''
}) })
// 权限相关状态
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('')
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)
...@@ -257,6 +335,20 @@ onMounted(async () => { ...@@ -257,6 +335,20 @@ onMounted(async () => {
queryParam.value.tplCode = String(route.query.tplCode) queryParam.value.tplCode = String(route.query.tplCode)
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData() await setData()
}) })
...@@ -505,6 +597,136 @@ async function setTplItemMap() { ...@@ -505,6 +597,136 @@ 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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
const mergeCells = ref<VxeTablePropTypes.MergeCells>([ const mergeCells = ref<VxeTablePropTypes.MergeCells>([
{ row: 0, col: 1, rowspan: 1, colspan: 2 }, { row: 0, col: 1, rowspan: 1, colspan: 2 },
{ row: 1, col: 1, rowspan: 1, colspan: 2 }, { row: 1, col: 1, rowspan: 1, colspan: 2 },
...@@ -550,6 +772,150 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([ ...@@ -550,6 +772,150 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([
} }
} }
/* 加载遮罩层样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* 校验结果样式 */
.validation-results {
padding: 20px;
height: 600px;
overflow-y: auto;
}
.result-summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.summary-item {
display: flex;
flex-direction: column;
}
.summary-label {
font-size: 12px;
color: #666;
}
.summary-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
margin-top: 10px;
}
.result-item {
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f5f5f5;
}
.result-item.success {
background-color: #f6ffed;
border-left: 4px solid #52c41a;
}
.result-item.error {
background-color: #fff2f0;
border-left: 4px solid #ff4d4f;
}
.result-field {
font-weight: bold;
margin-bottom: 5px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.result-value {
font-size: 12px;
color: #999;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 5px;
}
/* 验证公式帮助提示样式 */
.validation-tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip-content {
margin: 0;
white-space: pre-wrap;
}
.content-cell { .content-cell {
line-height: 1.8; line-height: 1.8;
padding: 5px; padding: 5px;
......
<template> <template>
<div class="bank-report-table" style="height: 80vh;"> <div class="bank-report-table" style="height: 80vh;" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #buttons>
<div class="toolbar-info"> <div class="toolbar-info">
...@@ -8,9 +16,55 @@ ...@@ -8,9 +16,55 @@
</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-save" @click="checkData()">校验</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"
...@@ -84,7 +138,19 @@ ...@@ -84,7 +138,19 @@
size="mini" size="mini"
class="table-input" class="table-input"
:style="{width:item.width}" :style="{width:item.width}"
:disabled="!item.hasRight"
:data-field="getFieldKey(row.code, item.field)"
></vxe-input> ></vxe-input>
<!-- 帮助图标 -->
<span
v-if="item.hasValidFormula"
class="validation-help-icon"
@mouseenter="(event) => showValidationHelp(item, event)"
@mouseleave="closeTooltip"
@click="(event) => showValidationHelp(item, event)"
>
</span>
</template> </template>
<span v-if="item.unit" class="unit">{{ item.unit }}</span> <span v-if="item.unit" class="unit">{{ item.unit }}</span>
</template> </template>
...@@ -93,18 +159,32 @@ ...@@ -93,18 +159,32 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
</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 { tableFormData } from '../../data/tb2.data' import { tableFormData } from '../../data/tb2.data'
import { ref, reactive, nextTick, onMounted } from 'vue' import { ref, reactive, nextTick, onMounted } 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 } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
...@@ -127,6 +207,17 @@ const formData = reactive<Record<string, any>>({}) ...@@ -127,6 +207,17 @@ 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 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({ const queryParam = ref({
taskId: -1, taskId: -1,
taskName: '', taskName: '',
...@@ -152,7 +243,22 @@ onMounted(async () => { ...@@ -152,7 +243,22 @@ onMounted(async () => {
queryParam.value.tplCode = String(route.query.tplCode) queryParam.value.tplCode = String(route.query.tplCode)
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
setFormItemRight()
await setData() await setData()
}) })
...@@ -330,6 +436,155 @@ async function setTplItemMap() { ...@@ -330,6 +436,155 @@ async function setTplItemMap() {
} }
} }
// 设置表单项权限标识
const setFormItemRight = () => {
tableFormData.forEach(row => {
if (row.content && Array.isArray(row.content)) {
row.content.forEach(item => {
if (item.field) {
let tmpKey = `${row.code}_${item.field}`;
item["hasRight"] = userAllocItems.value.indexOf(tmpKey) > -1
item["hasValidFormula"] = validFormula.value?.length > 0 &&
validFormula.value.some((f: any) => {
const formulaText = (f.formula || '').toString()
return formulaText.includes(item.field)
})
}
});
}
});
}
// 校验数据
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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
try {
const fieldMatch = formula.match(/\[(\w+)\]/)
if (!fieldMatch) {
process.push(`跳过: 无法解析公式字段 ${formula}`)
return null
}
const fieldName = fieldMatch[1]
const row = tableFormData.find(r => 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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
function shouldShowExtraFields(opt, rowCode, itemField) { function shouldShowExtraFields(opt, rowCode, itemField) {
return formData[getFieldKey(rowCode, itemField)] === opt.label && return formData[getFieldKey(rowCode, itemField)] === opt.label &&
...@@ -500,4 +755,165 @@ async function setTplItemMap() { ...@@ -500,4 +755,165 @@ async function setTplItemMap() {
gap: 8px; gap: 8px;
} }
/* 加载遮罩层 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
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-help-icon {
display: inline-block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
background: #1890ff;
color: white;
border-radius: 50%;
font-size: 12px;
margin-left: 4px;
cursor: pointer;
vertical-align: middle;
}
/* 验证公式帮助提示 */
.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>
<template> <template>
<div class="bank-report-table" style="height: 80vh;"> <div class="bank-report-table" style="height: 80vh;" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #buttons>
<div style="margin:10px"> <div style="margin:10px">
...@@ -8,9 +16,55 @@ ...@@ -8,9 +16,55 @@
</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-save" @click="checkData()">校验</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"
...@@ -20,22 +74,37 @@ ...@@ -20,22 +74,37 @@
style="margin:0px;padding:0px;height:100%" style="margin:0px;padding:0px;height:100%"
ref="refMyVxeTable" ref="refMyVxeTable"
/> />
<!-- 历史填报检查组件 -->
<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>
</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 { tableFormData } from '../../data/tb4.data'; import { tableFormData } from '../../data/tb4.data';
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } 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 { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const refMyVxeTable = ref(); const refMyVxeTable = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null);
const queryParam = ref({ const queryParam = ref({
taskId:-1, taskId:-1,
taskName:'', taskName:'',
...@@ -44,6 +113,16 @@ const queryParam = ref({ ...@@ -44,6 +113,16 @@ const queryParam = ref({
tplCode:'' tplCode:''
}) })
// 权限相关状态
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('')
interface FormData { interface FormData {
id: number | null id: number | null
taskid: number taskid: number
...@@ -72,6 +151,20 @@ onMounted(async ()=>{ ...@@ -72,6 +151,20 @@ onMounted(async ()=>{
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
}) })
...@@ -199,6 +292,118 @@ async function setTplItemMap() { ...@@ -199,6 +292,118 @@ async function setTplItemMap() {
VxeUI.modal.message({ content: `加载数据失败: ${error.message}`, status: 'error' }) VxeUI.modal.message({ content: `加载数据失败: ${error.message}`, status: 'error' })
} }
} }
// 校验数据
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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
try {
const fieldMatch = formula.match(/\[(\w+)\]/)
if (!fieldMatch) {
process.push(`跳过: 无法解析公式字段 ${formula}`)
return null
}
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 expression = formula.replace(/\[(\w+)\]/g, (_, field) => {
const value = formData[`${tableFormData.code}_${field}`]
return value !== undefined ? `Number(${value})` : '0'
})
process.push(`执行公式: ${expression}`)
const isValid = eval(expression)
return {
field: fieldName,
description: description,
formula: formula,
isValid: isValid,
fieldValue: fieldValue,
rowCode: tableFormData.code
}
} catch (error) {
process.push(`公式执行错误: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
// 处理校验结果点击,定位到表格
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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
...@@ -242,4 +447,150 @@ async function setTplItemMap() { ...@@ -242,4 +447,150 @@ async function setTplItemMap() {
background: transparent; background: transparent;
line-height: 50px; line-height: 50px;
} }
/* 加载遮罩层 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
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> </style>
\ No newline at end of file
<template> <template>
<div class="bank-report-table" style="height: 80vh;"> <div class="bank-report-table" style="height: 80vh;" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #buttons>
<div style="margin:10px"> <div style="margin:10px">
...@@ -9,9 +16,54 @@ ...@@ -9,9 +16,54 @@
</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-save" @click="checkData()">校验</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"
...@@ -167,25 +219,39 @@ ...@@ -167,25 +219,39 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
</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 AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import HistoryFillCheck from './check/historyFillCheck.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 } 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 } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null);
const queryParam = ref({ const queryParam = ref({
taskId:-1, taskId:-1,
taskName:'', taskName:'',
...@@ -194,6 +260,16 @@ const queryParam = ref({ ...@@ -194,6 +260,16 @@ const queryParam = ref({
tplCode:'' tplCode:''
}) })
// 权限相关状态
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('')
interface FormData { interface FormData {
id: number | null id: number | null
taskid: number taskid: number
...@@ -222,6 +298,20 @@ onMounted(async ()=>{ ...@@ -222,6 +298,20 @@ onMounted(async ()=>{
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
}) })
...@@ -474,6 +564,136 @@ async function setTplItemMap() { ...@@ -474,6 +564,136 @@ 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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
...@@ -527,97 +747,149 @@ async function setTplItemMap() { ...@@ -527,97 +747,149 @@ async function setTplItemMap() {
white-space: nowrap; white-space: nowrap;
} }
.vxe-input--inner { /* 加载遮罩层 */
height: 24px; .loading-overlay {
line-height: 24px; position: absolute;
padding: 0 5px; top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
} }
.vxe-radio--label, .loading-spinner {
.vxe-checkbox--label { text-align: center;
font-size: 12px;
} }
.vxe-radio--icon, .spinner {
.vxe-checkbox--icon { width: 40px;
font-size: 12px; height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
} }
.vxe-radio-group, @keyframes spin {
.vxe-checkbox-group { 0% { transform: rotate(0deg); }
display: inline-block; 100% { transform: rotate(360deg); }
} }
.bank-report-table { .loading-text {
font-family: "SimSun", "宋体", serif; font-size: 14px;
font-size: 12px; color: #666;
color: #000;
margin: 5px auto;
width: 90%;
} }
.custom-table { /* 校验结果 */
width: 100%; .validation-results {
border: 1px solid #000; padding: 20px;
} }
.custom-table .vxe-header--column, .result-summary {
.custom-table .vxe-body--column { display: flex;
border-right: 1px solid #000; gap: 40px;
border-bottom: 1px solid #000; margin-bottom: 20px;
padding: 5px; padding: 15px;
background: #f5f5f5;
border-radius: 4px;
} }
.content-cell { .summary-item {
line-height: 1.8; display: flex;
align-items: center;
gap: 10px;
} }
.table-input { .summary-label {
width: 80px; font-weight: 500;
margin: 0 2px; color: #666;
} }
.summary-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
/* 附件样式 */ .summary-value.success {
.attachments { color: #52c41a;
margin-top: 20px;
width: 100%;
} }
.attachments h4 { .summary-value.error {
font-size: 14px; 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; font-weight: bold;
margin-bottom: 10px; margin-bottom: 4px;
} }
.attachment-table { .result-desc {
width: 100%; font-size: 12px;
border: 1px solid #000; color: #666;
margin-bottom: 4px;
} }
.attachment-table .vxe-header--column, .result-value {
.attachment-table .vxe-body--column { font-size: 12px;
border-right: 1px solid #000; color: #888;
border-bottom: 1px solid #000;
padding: 5px;
} }
.vxe-input { .result-error {
border-bottom: 1px solid #333; font-size: 12px;
text-align: center; color: #ff4d4f;
margin: 5px 3px; margin-top: 4px;
padding: 0; }
border-top: none;
border-left: none; /* 验证公式帮助提示 */
border-right: none; .validation-tooltip {
background: transparent; position: fixed;
line-height: 50px; background: #333;
} color: white;
padding: 10px 15px;
blockquote { border-radius: 4px;
padding: 0px 5px; font-size: 12px;
color: black; z-index: 9999;
cursor: pointer; 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>
<template> <template>
<div class="bank-report-table" style="height:80vh"> <div class="bank-report-table" style="height: 80vh;">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #button>
<div style="margin:10px"> <div style="margin:10px">
<span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span> <span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span>
<span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span> <span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span>
</div> </div>
</template> </template>
<template #tools> <template #tools>
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</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="checkData()">校验</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"
...@@ -157,6 +210,20 @@ ...@@ -157,6 +210,20 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
<!-- 原有检查组件 -->
<CheckTbData ref="refCheckTbData"></CheckTbData> <CheckTbData ref="refCheckTbData"></CheckTbData>
</div> </div>
</template> </template>
...@@ -166,16 +233,18 @@ import MultiColumnTable from '../tableComponents/MultiColumnTable.vue' ...@@ -166,16 +233,18 @@ import MultiColumnTable from '../tableComponents/MultiColumnTable.vue'
import AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import MyVxeTable1 from '../tableComponents/MyVxeTable.vue' import MyVxeTable1 from '../tableComponents/MyVxeTable.vue'
import CheckTbData from './CheckTbData.vue' import CheckTbData from './CheckTbData.vue'
import HistoryFillCheck from './check/historyFillCheck.vue'
import { tableFormData } from '../../data/tb6.data'; import { tableFormData } from '../../data/tb6.data';
import { ref, reactive,nextTick,onMounted,toRaw } from 'vue' import { ref, reactive,nextTick,onMounted,toRaw } 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 } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const refCheckTbData = ref(); const refCheckTbData = ref();
const historyFillCheckRef = ref<any>(null);
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
...@@ -188,6 +257,16 @@ const queryParam = ref({ ...@@ -188,6 +257,16 @@ const queryParam = ref({
tplCode:'' tplCode:''
}) })
// 权限相关状态
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('')
interface FormData { interface FormData {
id: number | null id: number | null
taskid: number taskid: number
...@@ -216,6 +295,20 @@ onMounted(async ()=>{ ...@@ -216,6 +295,20 @@ onMounted(async ()=>{
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
}) })
...@@ -363,7 +456,138 @@ const setFormValues = async (pcode,code,valData,rind) => { ...@@ -363,7 +456,138 @@ const setFormValues = async (pcode,code,valData,rind) => {
} }
async function checkData() { // 校验数据
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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
// 原有检查方法
async function checkDataOrig() {
let tplName = queryParam.value.tplName; let tplName = queryParam.value.tplName;
await getFillDatas(); await getFillDatas();
if(formValues.value.length > 0) { if(formValues.value.length > 0) {
...@@ -524,6 +748,150 @@ async function setTplItemMap() { ...@@ -524,6 +748,150 @@ async function setTplItemMap() {
width: 60%; width: 60%;
} }
/* 加载遮罩层样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* 校验结果样式 */
.validation-results {
padding: 20px;
height: 600px;
overflow-y: auto;
}
.result-summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.summary-item {
display: flex;
flex-direction: column;
}
.summary-label {
font-size: 12px;
color: #666;
}
.summary-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
margin-top: 10px;
}
.result-item {
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f5f5f5;
}
.result-item.success {
background-color: #f6ffed;
border-left: 4px solid #52c41a;
}
.result-item.error {
background-color: #fff2f0;
border-left: 4px solid #ff4d4f;
}
.result-field {
font-weight: bold;
margin-bottom: 5px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.result-value {
font-size: 12px;
color: #999;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 5px;
}
/* 验证公式帮助提示样式 */
.validation-tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip-content {
margin: 0;
white-space: pre-wrap;
}
.custom-table { .custom-table {
width: 100%; width: 100%;
border: 1px solid #000; border: 1px solid #000;
......
...@@ -2,16 +2,62 @@ ...@@ -2,16 +2,62 @@
<div class="bank-report-table" style="height: 80vh;"> <div class="bank-report-table" style="height: 80vh;">
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #button>
<div style="margin:10px"> <div style="margin:10px">
<span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span> <span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span>
<span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span> <span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span>
</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-save" @click="checkData()">校验</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"
...@@ -166,23 +212,38 @@ ...@@ -166,23 +212,38 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import HistoryFillCheck from './check/historyFillCheck.vue'
import { tableFormData } from '../../data/tb7.data'; import { tableFormData } from '../../data/tb7.data';
import { ref, reactive,nextTick,onMounted,toRaw } from 'vue' import { ref, reactive,nextTick,onMounted,toRaw } from 'vue'
import { VxeUI,VxeToolbarInstance,VxeTablePropTypes } from 'vxe-table' import { VxeUI,VxeToolbarInstance,VxeTablePropTypes } from 'vxe-table'
import { batchSaveOrUpdate, queryRecord,batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api' import { batchSaveOrUpdate, queryRecord,batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api'
import { queryAllTplItemForUser } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null);
const queryParam = ref({ const queryParam = ref({
taskId:-1, taskId:-1,
taskName:'', taskName:'',
...@@ -191,6 +252,16 @@ const queryParam = ref({ ...@@ -191,6 +252,16 @@ const queryParam = ref({
tplCode:'' tplCode:''
}) })
// 权限相关状态
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('')
interface FormData { interface FormData {
id: number | null id: number | null
taskid: number taskid: number
...@@ -219,6 +290,20 @@ onMounted(async ()=>{ ...@@ -219,6 +290,20 @@ onMounted(async ()=>{
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
}) })
...@@ -371,6 +456,136 @@ async function setTplItemMap() { ...@@ -371,6 +456,136 @@ 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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
const mergeCells = ref<VxeTablePropTypes.MergeCells>([ const mergeCells = ref<VxeTablePropTypes.MergeCells>([
{ row: 7, col: 2, rowspan: 1, colspan: 2 }, { row: 7, col: 2, rowspan: 1, colspan: 2 },
]) ])
...@@ -389,10 +604,155 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([ ...@@ -389,10 +604,155 @@ const mergeCells = ref<VxeTablePropTypes.MergeCells>([
/* 表格样式 */ /* 表格样式 */
.custom-table, .attachment-table { .custom-table, .attachment-table {
width: 100%; width: 100%;
border: 1px solid #000; border: 1px solid #000;
border-collapse: collapse; /* 更整齐的边框 */ border-collapse: collapse; /* 统一边框处理 */
} }
/* 加载遮罩层样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* 校验结果样式 */
.validation-results {
padding: 20px;
height: 600px;
overflow-y: auto;
}
.result-summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.summary-item {
display: flex;
flex-direction: column;
}
.summary-label {
font-size: 12px;
color: #666;
}
.summary-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
margin-top: 10px;
}
.result-item {
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f5f5f5;
}
.result-item.success {
background-color: #f6ffed;
border-left: 4px solid #52c41a;
}
.result-item.error {
background-color: #fff2f0;
border-left: 4px solid #ff4d4f;
}
.result-field {
font-weight: bold;
margin-bottom: 5px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.result-value {
font-size: 12px;
color: #999;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 5px;
}
/* 验证公式帮助提示样式 */
.validation-tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip-content {
margin: 0;
white-space: pre-wrap;
}
/* 表格单元格样式 */
.custom-table .vxe-header--column, .custom-table .vxe-header--column,
.custom-table .vxe-body--column, .custom-table .vxe-body--column,
.attachment-table .vxe-header--column, .attachment-table .vxe-header--column,
......
<template> <template>
<div class="bank-report-table"> <div class="bank-report-table" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #button>
<div style="margin:10px"> <div style="margin:10px">
<span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span> <span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span>
<span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span> <span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span>
</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-save" @click="checkData()">校验</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
height="auto" height="auto"
...@@ -197,22 +250,37 @@ ...@@ -197,22 +250,37 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import HistoryFillCheck from './check/historyFillCheck.vue'
import { tableFormData } from '../../data/tb8.data'; import { tableFormData } from '../../data/tb8.data';
import { ref, reactive,nextTick,onMounted,toRaw } from 'vue' import { ref, reactive,nextTick,onMounted,toRaw } 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 } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null);
const queryParam = ref({ const queryParam = ref({
taskId:-1, taskId:-1,
taskName:'', taskName:'',
...@@ -221,6 +289,16 @@ const queryParam = ref({ ...@@ -221,6 +289,16 @@ const queryParam = ref({
tplCode:'' tplCode:''
}) })
// 权限相关状态
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('')
interface FormData { interface FormData {
id: number | null id: number | null
...@@ -250,6 +328,20 @@ onMounted(async ()=>{ ...@@ -250,6 +328,20 @@ onMounted(async ()=>{
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
}) })
...@@ -383,6 +475,136 @@ async function setTplItemMap() { ...@@ -383,6 +475,136 @@ 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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
/* 基础字体和排版 */ /* 基础字体和排版 */
...@@ -398,10 +620,155 @@ async function setTplItemMap() { ...@@ -398,10 +620,155 @@ async function setTplItemMap() {
/* 表格样式 */ /* 表格样式 */
.custom-table, .attachment-table { .custom-table, .attachment-table {
width: 100%; width: 100%;
border: 1px solid #000; border: 1px solid #000;
border-collapse: collapse; /* 更整齐的边框 */ border-collapse: collapse; /* 统一边框处理 */
} }
/* 加载遮罩层样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* 校验结果样式 */
.validation-results {
padding: 20px;
height: 600px;
overflow-y: auto;
}
.result-summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.summary-item {
display: flex;
flex-direction: column;
}
.summary-label {
font-size: 12px;
color: #666;
}
.summary-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
margin-top: 10px;
}
.result-item {
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f5f5f5;
}
.result-item.success {
background-color: #f6ffed;
border-left: 4px solid #52c41a;
}
.result-item.error {
background-color: #fff2f0;
border-left: 4px solid #ff4d4f;
}
.result-field {
font-weight: bold;
margin-bottom: 5px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.result-value {
font-size: 12px;
color: #999;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 5px;
}
/* 验证公式帮助提示样式 */
.validation-tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip-content {
margin: 0;
white-space: pre-wrap;
}
/* 表格单元格样式 */
.custom-table .vxe-header--column, .custom-table .vxe-header--column,
.custom-table .vxe-body--column, .custom-table .vxe-body--column,
.attachment-table .vxe-header--column, .attachment-table .vxe-header--column,
......
<template> <template>
<div class="bank-report-table" style="height: 80vh;"> <div class="bank-report-table" style="height: 80vh;" @click="closeTooltip">
<!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #button>
<div style="margin:10px"> <div style="margin:10px">
<span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span> <span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span>
<span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span> <span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span>
</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-save" @click="checkData()">校验</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"
...@@ -194,25 +247,39 @@ ...@@ -194,25 +247,39 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 历史填报检查组件 -->
<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>
</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 AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import HistoryFillCheck from './check/historyFillCheck.vue'
import { tableFormData } from '../../data/tb9.data'; import { tableFormData } from '../../data/tb9.data';
import { ref, reactive,nextTick,onMounted,toRaw } from 'vue' import { ref, reactive,nextTick,onMounted,toRaw } 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 } from '../../alloc/BaosongTaskAlloc.api' import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'
import { allTplItems } from '../../tpl/BaosongTplItem.api' import { allTplItems } from '../../tpl/BaosongTplItem.api'
import { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
const tplItemMap = ref({}); const tplItemMap = ref({});
const historyFillCheckRef = ref<any>(null);
const queryParam = ref({ const queryParam = ref({
taskId:-1, taskId:-1,
taskName:'', taskName:'',
...@@ -221,6 +288,16 @@ const queryParam = ref({ ...@@ -221,6 +288,16 @@ const queryParam = ref({
tplCode:'' tplCode:''
}) })
// 权限相关状态
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('')
interface FormData { interface FormData {
id: number | null id: number | null
taskid: number taskid: number
...@@ -249,6 +326,20 @@ onMounted(async ()=>{ ...@@ -249,6 +326,20 @@ onMounted(async ()=>{
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
// 获取权限和验证公式
try {
userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
validFormula.value = await getTblvalidFormula({
tplid: queryParam.value.tplId,
})
} catch (error) {
console.error('获取权限或验证公式失败:', error)
}
await setTplItemMap() await setTplItemMap()
await setData(); await setData();
}) })
...@@ -507,6 +598,136 @@ async function setTplItemMap() { ...@@ -507,6 +598,136 @@ 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)
}
})
process.push('校验完成')
}
// 解析和执行验证公式
const evaluateFormula = (formula: string, description: string, process: string[]): any => {
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 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 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)
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 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 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 closeTooltip = () => {
validationTooltipVisible.value = false
}
// 打开历史填报抽屉
const handleOpenHistoryDrawer = () => {
historyDrawerVisible.value = true
}
// 关闭历史填报抽屉
const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
/* 基础字体和排版 */ /* 基础字体和排版 */
...@@ -521,10 +742,155 @@ async function setTplItemMap() { ...@@ -521,10 +742,155 @@ async function setTplItemMap() {
/* 表格样式 */ /* 表格样式 */
.custom-table, .attachment-table { .custom-table, .attachment-table {
width: 100%; width: 100%;
border: 1px solid #000; border: 1px solid #000;
border-collapse: collapse; /* 更整齐的边框 */ border-collapse: collapse; /* 统一边框处理 */
} }
/* 加载遮罩层样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 10px;
color: #666;
font-size: 14px;
}
/* 校验结果样式 */
.validation-results {
padding: 20px;
height: 600px;
overflow-y: auto;
}
.result-summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.summary-item {
display: flex;
flex-direction: column;
}
.summary-label {
font-size: 12px;
color: #666;
}
.summary-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.summary-value.success {
color: #52c41a;
}
.summary-value.error {
color: #ff4d4f;
}
.results-list {
margin-top: 10px;
}
.result-item {
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.result-item:hover {
background-color: #f5f5f5;
}
.result-item.success {
background-color: #f6ffed;
border-left: 4px solid #52c41a;
}
.result-item.error {
background-color: #fff2f0;
border-left: 4px solid #ff4d4f;
}
.result-field {
font-weight: bold;
margin-bottom: 5px;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.result-value {
font-size: 12px;
color: #999;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 5px;
}
/* 验证公式帮助提示样式 */
.validation-tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 10px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip-content {
margin: 0;
white-space: pre-wrap;
}
/* 表格单元格样式 */
.custom-table .vxe-header--column, .custom-table .vxe-header--column,
.custom-table .vxe-body--column, .custom-table .vxe-body--column,
.attachment-table .vxe-header--column, .attachment-table .vxe-header--column,
......
...@@ -51,11 +51,11 @@ const gridOptions = reactive<VxeGridProps<RowVO>>({ ...@@ -51,11 +51,11 @@ const gridOptions = reactive<VxeGridProps<RowVO>>({
{ field: 'itemName', title: '字段名称',width: "15%"}, { field: 'itemName', title: '字段名称',width: "15%"},
{ field: 'values', title: '最近5年填报数据',width: "75%", { field: 'values', title: '最近5年填报数据',width: "75%",
children: [ children: [
{ field: 'value1', title: '第一次',width: "15%"}, { field: 'value0', title: '第一次',width: "15%"},
{ field: 'value2', title: '第二次',width: "15%"}, { field: 'value1', title: '第二次',width: "15%"},
{ field: 'value3', title: '第三次',width: "15%"}, { field: 'value2', title: '第三次',width: "15%"},
{ field: 'value4', title: '第四次',width: "15%"}, { field: 'value3', title: '第四次',width: "15%"},
{ field: 'value5', title: '第五次',width: "15%"}, { field: 'value4', title: '第五次',width: "15%"},
], ],
}, },
{ field: 'opt', title: '操作' ,width: "10%"}, { field: 'opt', title: '操作' ,width: "10%"},
...@@ -67,19 +67,24 @@ async function onDrawerShow(tplid: number) { ...@@ -67,19 +67,24 @@ async function onDrawerShow(tplid: number) {
const retData = await findFillHistoryForCheck({ const retData = await findFillHistoryForCheck({
tplid: tplid tplid: tplid
}); });
// 调试:查看数据结构
console.log('原始数据:', retData);
// 处理 values 数据 // 处理 values 数据
const processedData = retData.map(item => { const processedData = retData.map((item, itemIndex) => {
const processedItem = {...item}; const processedItem = {...item};
// 将 values 数组中的 value 属性提取出来 // 将 values 数组中的 value 属性提取出来
if (Array.isArray(processedItem.values)) { if (Array.isArray(processedItem.values)) {
processedItem.values.forEach((valueItem, index) => { processedItem.values.forEach((valueItem, index) => {
// 将每个 valueItem 的 value 属性设置到单独的字段中 // 将每个 valueItem 的 value 属性设置到单独的字段中
processedItem[`value${index}`] = valueItem.value; // 注意:index从0开始,但字段名应该从1开始
processedItem[`value${index + 1}`] = valueItem.value;
}); });
} }
return processedItem; return processedItem;
}); });
console.log('处理后的数据:', processedData);
gridOptions.data = processedData; gridOptions.data = processedData;
return false; return false;
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论