提交 9cb3fade authored 作者: liuluyu's avatar liuluyu

Merge branch 'master' of http://47.97.51.208/root/zrch-risk-39

...@@ -79,6 +79,7 @@ ...@@ -79,6 +79,7 @@
"swagger-ui-dist": "^5.29.3", "swagger-ui-dist": "^5.29.3",
"tinymce": "6.6.2", "tinymce": "6.6.2",
"vditor": "^3.11.2", "vditor": "^3.11.2",
"vkbeautify": "^0.99.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-cropper": "^0.6.5", "vue-cropper": "^0.6.5",
"vue-cropperjs": "^5.0.0", "vue-cropperjs": "^5.0.0",
......
...@@ -176,6 +176,9 @@ importers: ...@@ -176,6 +176,9 @@ importers:
vditor: vditor:
specifier: ^3.11.2 specifier: ^3.11.2
version: 3.11.2 version: 3.11.2
vkbeautify:
specifier: ^0.99.3
version: 0.99.3
vue: vue:
specifier: ^3.5.22 specifier: ^3.5.22
version: 3.5.27(typescript@5.9.3) version: 3.5.27(typescript@5.9.3)
...@@ -7619,6 +7622,9 @@ packages: ...@@ -7619,6 +7622,9 @@ packages:
yaml: yaml:
optional: true optional: true
vkbeautify@0.99.3:
resolution: {integrity: sha512-2ozZEFfmVvQcHWoHLNuiKlUfDKlhh4KGsy54U0UrlLMR1SO+XKAIDqBxtBwHgNrekurlJwE8A9K6L49T78ZQ9Q==}
vue-component-type-helpers@2.2.12: vue-component-type-helpers@2.2.12:
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
...@@ -15963,6 +15969,8 @@ snapshots: ...@@ -15963,6 +15969,8 @@ snapshots:
terser: 5.46.0 terser: 5.46.0
tsx: 4.21.0 tsx: 4.21.0
vkbeautify@0.99.3: {}
vue-component-type-helpers@2.2.12: {} vue-component-type-helpers@2.2.12: {}
vue-cropper@0.6.5: {} vue-cropper@0.6.5: {}
......
<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>
...@@ -148,7 +168,75 @@ ...@@ -148,7 +168,75 @@
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<CheckTbData ref="refCheckTbData"></CheckTbData>
<!-- 校验结果抽屉 -->
<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 CheckTbData from './CheckTbData.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">
...@@ -8,9 +16,47 @@ ...@@ -8,9 +16,47 @@
</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"
...@@ -152,44 +198,69 @@ ...@@ -152,44 +198,69 @@
</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:'',
tplId: -1, tplId:-1,
tplName: '', tplName:'',
tplCode: '', tplCode:''
}); })
interface FormData { // 权限相关状态
id: number | null; const userAllocItems = ref<string[]>([])
taskid: number; const validFormula = ref<any[]>([])
tplid: number; const validationResultsList = ref<any[]>([])
itemid?: number; const drawerVisible = ref(false)
itempid?: number; const historyDrawerVisible = ref(false)
content: string; const validationTooltipVisible = ref(false)
tplcode: string; const validationTooltipPosition = ref({ left: 0, top: 0 })
rind: number; const validationTooltipContent = ref('')
}
interface FormData {
onMounted(async () => { id: number | null;
taskid: number;
tplid: number;
itemid?: number;
itempid?: number;
content: string;
tplcode: string;
rind: number;
}
onMounted(async ()=>{
if (route.query.taskId) { if (route.query.taskId) {
queryParam.value.taskId = Number(route.query.taskId); queryParam.value.taskId = Number(route.query.taskId);
} }
...@@ -206,233 +277,379 @@ ...@@ -206,233 +277,379 @@
queryParam.value.tplCode = String(route.query.tplCode); queryParam.value.tplCode = String(route.query.tplCode);
} }
await setTplItemMap(); // 获取权限和验证公式
await setData(); 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 setData();
});
const loading = ref(false); const loading = ref(false);
const formData = reactive({}); const formData = reactive({});
const formValues = ref<FormData[]>([]); const formValues = ref<FormData[]>([]);
const saveBatch = async () => { const saveBatch = async () => {
try { try {
formValues.value = []; formValues.value = [];
for (const strKey in formData) { for (const strKey in formData) {
const valData = formData[strKey]; const valData = formData[strKey];
if (valData) { if (valData) {
let tmpAry = strKey.split('_'); let tmpAry = strKey.split('_');
await setFormValues(tmpAry[0], tmpAry[1], valData, 1); await setFormValues(tmpAry[0], tmpAry[1], valData, 1);
}
} }
}
await getAttachTableFormData(); await getAttachTableFormData();
await getAllMultiTableFormData(); await getAllMultiTableFormData();
if (formValues.value.length > 0) { if (formValues.value.length > 0) {
await batchSaveOrUpdateBeforeDelete(formValues.value); await batchSaveOrUpdateBeforeDelete(formValues.value);
VxeUI.modal.message({ content: '保存成功', status: 'success' }); VxeUI.modal.message({ content: '保存成功', status: 'success' });
//await setData() //await setData()
} else { } else {
VxeUI.modal.message({ content: '没有需要保存的数据', status: 'warning' }); VxeUI.modal.message({ content: '没有需要保存的数据', status: 'warning' });
}
} catch (error) {
VxeUI.modal.message({ content: `保存失败: ${error.message}`, status: 'error' });
} finally {
loading.value = false;
} }
} catch (error) { };
VxeUI.modal.message({ content: `保存失败: ${error.message}`, status: 'error' });
} finally {
loading.value = false;
}
};
const childMultiTableRefs = ref({}); const childMultiTableRefs = ref({});
const setMultiColumnTableRef = (el: any, index: string) => { const setMultiColumnTableRef = (el: any, index: string) => {
if (el) { if (el) {
childMultiTableRefs.value[index] = el; childMultiTableRefs.value[index] = el;
} }
}; };
const getAllMultiTableFormData = async () => { const getAllMultiTableFormData = async () => {
for (const pcode in childMultiTableRefs.value) { for (const pcode in childMultiTableRefs.value) {
const child = childMultiTableRefs.value[pcode]; const child = childMultiTableRefs.value[pcode];
if (child) { if (child) {
const datas = child.getFormData(); const datas = child.getFormData();
let rind = 0; let rind = 0;
for (const obj of datas) { for (const obj of datas) {
rind++; rind++;
Object.keys(obj).forEach((code) => { Object.keys(obj).forEach((code) => {
setFormValues(pcode, code, obj[code], rind); setFormValues(pcode, code, obj[code], rind);
}); });
}
} }
} }
} };
};
const childAttachTableRefs = ref({}); const childAttachTableRefs = ref({});
const setAttachTableRef = (el: any, index: string) => { const setAttachTableRef = (el: any, index: string) => {
if (el) { if (el) {
childAttachTableRefs.value[index] = el; // 保存子组件实例 childAttachTableRefs.value[index] = el; // 保存子组件实例
} }
}; };
const getAttachTableFormData = async () => { const getAttachTableFormData = async () => {
for (const pcode in childAttachTableRefs.value) { for (const pcode in childAttachTableRefs.value) {
const child = childAttachTableRefs.value[pcode]; const child = childAttachTableRefs.value[pcode];
if (child) { if (child) {
const datas = child.getFormData(); const datas = child.getFormData();
for (const code in datas) { for (const code in datas) {
await setFormValues(pcode, code, datas[code], 1); await setFormValues(pcode, code, datas[code], 1);
}
} }
} }
}
};
const setFormValues = async (pcode, code, valData, rind) => {
if (!valData) return;
let vals = Array.isArray(valData) ? valData.join(',') : valData;
let strKey = pcode + '_' + code;
const item = tplItemMap.value[strKey];
const { pid, itemid } = item ?? {};
let tempForm: FormData = {
id: null,
rind: rind,
taskid: queryParam.value.taskId,
tplid: queryParam.value.tplId,
tplcode: code,
itemid: itemid,
itempid: pid,
content: vals,
}; };
formValues.value.push(tempForm);
};
async function setData() { const setFormValues = async (pcode, code, valData, rind) => {
try { if (!valData) return;
loading.value = true; let vals = Array.isArray(valData) ? valData.join(',') : valData;
const taskId = queryParam.value.taskId; let strKey = pcode + '_' + code;
const tplid = queryParam.value.tplId; const item = tplItemMap.value[strKey];
const { pid, itemid } = item ?? {};
let tempForm: FormData = {
id: null,
rind: rind,
taskid: queryParam.value.taskId,
tplid: queryParam.value.tplId,
tplcode: code,
itemid: itemid,
itempid: pid,
content: vals,
};
formValues.value.push(tempForm);
};
Object.keys(formData).forEach((key) => delete formData[key]); async function setData() {
try {
loading.value = true;
const taskId = queryParam.value.taskId;
const tplid = queryParam.value.tplId;
await setTplItemMap(); Object.keys(formData).forEach((key) => delete formData[key]);
const recordData = await queryRecord({ taskid: taskId, tplid: tplid });
const valueObj = {}; await setTplItemMap();
for (const data of recordData) { const recordData = await queryRecord({ taskid: taskId, tplid: tplid });
const key = `${data.itempid}_${data.itemid}_${data.rind}`;
valueObj[key] = data.content;
}
const shouldSkip = (key) => {
const refsToCheck = [childMultiTableRefs.value, childAttachTableRefs.value];
for (const refs of refsToCheck) { const valueObj = {};
for (const pcode of Object.keys(refs)) { for (const data of recordData) {
if (key.startsWith(pcode + '_')) { const key = `${data.itempid}_${data.itemid}_${data.rind}`;
return true; valueObj[key] = data.content;
}
const shouldSkip = (key) => {
const refsToCheck = [childMultiTableRefs.value, childAttachTableRefs.value];
for (const refs of refsToCheck) {
for (const pcode of Object.keys(refs)) {
if (key.startsWith(pcode + '_')) {
return true;
}
} }
} }
} return false;
return false; };
};
for (const strKey of Object.keys(tplItemMap.value)) { for (const strKey of Object.keys(tplItemMap.value)) {
if (shouldSkip(strKey)) { if (shouldSkip(strKey)) {
continue; continue;
} }
const item = tplItemMap.value[strKey]; const item = tplItemMap.value[strKey];
if (!item) continue; if (!item) continue;
const { pid, itemid, formTp } = item; const { pid, itemid, formTp } = item;
const dataVal = valueObj[`${pid}_${itemid}_1`]; const dataVal = valueObj[`${pid}_${itemid}_1`];
if (!dataVal) continue; if (!dataVal) continue;
if (formTp === 'checkbox') { if (formTp === 'checkbox') {
formData[strKey] = dataVal?.split(',') || []; formData[strKey] = dataVal?.split(',') || [];
} else { } else {
formData[strKey] = dataVal || ''; formData[strKey] = dataVal || '';
}
} }
}
for (const pcode of Object.keys(childAttachTableRefs.value)) { for (const pcode of Object.keys(childAttachTableRefs.value)) {
const child = childAttachTableRefs.value[pcode]; const child = childAttachTableRefs.value[pcode];
const matchingKeys = Object.keys(tplItemMap.value).filter((key) => key.startsWith(pcode + '_')); const matchingKeys = Object.keys(tplItemMap.value).filter((key) => key.startsWith(pcode + '_'));
const attachTableData = {}; const attachTableData = {};
for (const key of matchingKeys) { for (const key of matchingKeys) {
const tmpData = tplItemMap.value[key]; const tmpData = tplItemMap.value[key];
if (!tmpData) continue; if (!tmpData) continue;
const { pid, itemid, formTp, code } = tmpData; const { pid, itemid, formTp, code } = tmpData;
const valueKey = `${pid}_${itemid}_1`; const valueKey = `${pid}_${itemid}_1`;
if (formTp === 'checkbox') { if (formTp === 'checkbox') {
attachTableData[code] = valueObj[valueKey]?.split(',') || []; attachTableData[code] = valueObj[valueKey]?.split(',') || [];
} else { } else {
attachTableData[code] = valueObj[valueKey] || ''; attachTableData[code] = valueObj[valueKey] || '';
}
} }
child.setFormData(attachTableData);
} }
child.setFormData(attachTableData); for (const pcode of Object.keys(childMultiTableRefs.value)) {
} const child = childMultiTableRefs.value[pcode];
const matchingKeys = Object.keys(tplItemMap.value).filter((key) => key.startsWith(pcode + '_'));
for (const pcode of Object.keys(childMultiTableRefs.value)) { const rowsMap = {};
const child = childMultiTableRefs.value[pcode]; for (const key of matchingKeys) {
const matchingKeys = Object.keys(tplItemMap.value).filter((key) => key.startsWith(pcode + '_')); const tmpData = tplItemMap.value[key];
if (!tmpData) continue;
const rowsMap = {}; const { pid, itemid, code } = tmpData;
for (const key of matchingKeys) { const valKeys = Object.keys(valueObj).filter((k) => k.startsWith(`${pid}_${itemid}_`));
const tmpData = tplItemMap.value[key];
if (!tmpData) continue;
const { pid, itemid, code } = tmpData; for (const valKey of valKeys) {
const valKeys = Object.keys(valueObj).filter((k) => k.startsWith(`${pid}_${itemid}_`)); const parts = valKey.split('_');
const rind = parts[2];
for (const valKey of valKeys) { if (!rowsMap[rind]) {
const parts = valKey.split('_'); rowsMap[rind] = {};
const rind = parts[2]; }
if (!rowsMap[rind]) { rowsMap[rind][code] = valueObj[valKey];
rowsMap[rind] = {};
} }
rowsMap[rind][code] = valueObj[valKey];
} }
const tableDatas = Object.values(rowsMap);
nextTick();
child.setFormData(tableDatas);
} }
} catch (error) {
VxeUI.modal.message({
content: `加载数据失败: ${error.message}`,
status: 'error',
});
} finally {
loading.value = false;
}
}
const tableDatas = Object.values(rowsMap); async function setTplItemMap() {
nextTick(); try {
child.setFormData(tableDatas); const taskId = queryParam.value.taskId;
const tplid = queryParam.value.tplId;
const tplItem = await allTplItems({
tplid: tplid,
});
tplItem.forEach((item) => {
let strKey = item.pcode + '_' + item.xmlcode;
tplItemMap.value[strKey] = { pid: item.pid, itemid: item.id, formTp: item.formTp, code: item.xmlcode };
});
} catch (error) {
VxeUI.modal.message({ content: `加载数据失败: ${error.message}`, status: 'error' });
} finally {
} }
} catch (error) {
VxeUI.modal.message({
content: `加载数据失败: ${error.message}`,
status: 'error',
});
} finally {
loading.value = false;
} }
} }
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 { try {
const taskId = queryParam.value.taskId; const fieldMatch = formula.match(/\[(\w+)\]/)
const tplid = queryParam.value.tplId; if (!fieldMatch) {
const tplItem = await allTplItems({ process.push(`跳过: 无法解析公式字段 ${formula}`)
tplid: tplid, return null
}); }
tplItem.forEach((item) => { const fieldName = fieldMatch[1]
let strKey = item.pcode + '_' + item.xmlcode; const row = tableFormData.find((r: any) => r.content?.some((c: any) => c.field === fieldName))
tplItemMap.value[strKey] = { pid: item.pid, itemid: item.id, formTp: item.formTp, code: item.xmlcode }; 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) { } catch (error) {
VxeUI.modal.message({ content: `加载数据失败: ${error.message}`, status: 'error' }); process.push(`公式执行错误: ${error instanceof Error ? error.message : String(error)}`)
} finally { 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>
...@@ -485,95 +702,152 @@ ...@@ -485,95 +702,152 @@
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% {
display: inline-block; transform: rotate(0deg);
}
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 {
.attachments { font-size: 18px;
margin-top: 20px; font-weight: bold;
width: 100%; color: #333;
} }
.attachments h4 { .summary-value.success {
font-size: 14px; color: #52c41a;
font-weight: bold;
margin-bottom: 10px;
} }
.attachment-table { .summary-value.error {
width: 100%; color: #ff4d4f;
border: 1px solid #000;
} }
.attachment-table .vxe-header--column, .results-list {
.attachment-table .vxe-body--column { max-height: 500px;
border-right: 1px solid #000; overflow-y: auto;
border-bottom: 1px solid #000;
padding: 5px;
} }
.vxe-input { .result-item {
border-bottom: 1px solid #333; padding: 12px;
text-align: center; margin-bottom: 8px;
margin: 5px 3px; border-radius: 4px;
padding: 0;
border-top: none;
border-left: none;
border-right: none;
background: transparent;
line-height: 50px;
}
blockquote {
padding: 0px 5px;
color: black;
cursor: pointer; 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>
<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;
} }
......
...@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.StringUtils; ...@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.stm.baosong.entity.BaosongDataValid; import org.jeecg.modules.stm.baosong.entity.BaosongDataValid;
import org.jeecg.modules.stm.baosong.entity.BaosongTaskAlloc;
import org.jeecg.modules.stm.baosong.entity.BaosongTaskRecord; import org.jeecg.modules.stm.baosong.entity.BaosongTaskRecord;
import org.jeecg.modules.stm.baosong.entity.BaosongTplItem; import org.jeecg.modules.stm.baosong.entity.BaosongTplItem;
import org.jeecg.modules.stm.baosong.service.IBaosongDataValidService; import org.jeecg.modules.stm.baosong.service.IBaosongDataValidService;
...@@ -224,4 +225,14 @@ public class BaosongDataValidController extends JeecgController<BaosongDataValid ...@@ -224,4 +225,14 @@ public class BaosongDataValidController extends JeecgController<BaosongDataValid
return Result.OK(retMap); return Result.OK(retMap);
} }
@GetMapping(value = "/getTblvalidFormula")
public Result<List<BaosongDataValid>> getTblvalidFormula(BaosongDataValid dataValid, HttpServletRequest req) {
List<BaosongDataValid> listFormula = baosongDataValidService.lambdaQuery()
.eq(BaosongDataValid::getTplid, dataValid.getTplid())
.list();
return Result.OK(listFormula);
}
} }
...@@ -268,4 +268,31 @@ public class BaosongTaskAllocController extends JeecgController<BaosongTaskAlloc ...@@ -268,4 +268,31 @@ public class BaosongTaskAllocController extends JeecgController<BaosongTaskAlloc
list.addAll(0,rootItemList); list.addAll(0,rootItemList);
} }
} }
@GetMapping(value = "/findUserRightForTplItem")
public Result<Set<String>> findUserRightForTplItem(BaosongTaskAlloc taskAlloc,HttpServletRequest req) {
Set<String> retCodeSet = new HashSet<>();
try {
String userId = UserUtil.getUserId();
List<BaosongTaskAlloc> allocTplList = baosongTaskAllocService.lambdaQuery()
.eq(BaosongTaskAlloc::getFillUser, userId)
.eq(BaosongTaskAlloc::getTaskid,taskAlloc.getTaskid())
.eq(BaosongTaskAlloc::getTplid,taskAlloc.getTplid())
.list();
Set<Integer> tplIdSet = new HashSet<>();
for (BaosongTaskAlloc alloc : allocTplList) {
tplIdSet.add(alloc.getItemid());
}
List<BaosongTplItem> itemList = baosongTplItemService.lambdaQuery()
.in(BaosongTplItem::getId,tplIdSet)
.list();
for (BaosongTplItem item : itemList) {
retCodeSet.add(item.getPcode()+"_"+item.getCode());
}
return Result.OK(retCodeSet);
} catch (Exception e) {
log.error("查询用户模板权限失败", e);
return Result.error("查询失败,请稍后重试");
}
}
} }
package org.jeecg.modules.stm.baosong.controller; package org.jeecg.modules.stm.baosong.controller;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.List; import com.aliyun.oss.ServiceException;
import java.util.Map; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
...@@ -40,6 +40,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; ...@@ -40,6 +40,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
public class BaosongTaskRecordController extends JeecgController<BaosongTaskRecord, IBaosongTaskRecordService> { public class BaosongTaskRecordController extends JeecgController<BaosongTaskRecord, IBaosongTaskRecordService> {
@Autowired @Autowired
private IBaosongTaskRecordService baosongTaskRecordService; private IBaosongTaskRecordService baosongTaskRecordService;
/** /**
* 分页列表查询 * 分页列表查询
...@@ -261,4 +262,72 @@ public class BaosongTaskRecordController extends JeecgController<BaosongTaskReco ...@@ -261,4 +262,72 @@ public class BaosongTaskRecordController extends JeecgController<BaosongTaskReco
} }
@GetMapping(value = "/findFillHistoryForCheck")
public Result<List<Map<String, Object>>> findFillHistoryForCheckTraditional(BaosongQuery baosongQuery, HttpServletRequest req) {
// 参数校验
if (baosongQuery == null) {
return Result.error("查询参数不能为空");
}
List<Map<String, Object>> retList = new ArrayList<>();
try {
List<BaosongQuery> list = baosongTaskRecordService.findTaskRecords(baosongQuery);
if (CollectionUtils.isEmpty(list)) {
return Result.OK(Collections.emptyList());
}
// 使用 LinkedHashMap 保持插入顺序
Map<Integer, List<Map<String, Object>>> tmpMap = new LinkedHashMap<>();
Map<Integer, String> itemNameMap = new HashMap<>();
Map<Integer,String> taskNameMap = new HashMap<>();
for (BaosongQuery taskRecord : list) {
Integer itemId = taskRecord.getItemid();
// 获取或创建列表
List<Map<String, Object>> cList = tmpMap.computeIfAbsent(itemId,
k -> new ArrayList<>());
// 构建值Map
Map<String, Object> tmpVal = new HashMap<>(3);
tmpVal.put("value", taskRecord.getContent());
tmpVal.put("taskid", taskRecord.getTaskid());
tmpVal.put("taskName", taskRecord.getTaskName());
cList.add(tmpVal);
// 存储 itemName(如果已存在则跳过)
itemNameMap.putIfAbsent(itemId, taskRecord.getTplItemName());
if(taskNameMap.get(taskRecord.getTaskid())!=null){
taskNameMap.put(taskRecord.getTaskid(),taskRecord.getTaskName());
}
}
// 构建结果
for (Map.Entry<Integer, List<Map<String, Object>>> entry : tmpMap.entrySet()) {
Integer itemId = entry.getKey();
Map<String, Object> resultItem = new HashMap<>(3);
resultItem.put("itemId", itemId);
resultItem.put("itemName", itemNameMap.get(itemId));
resultItem.put("values", entry.getValue());
retList.add(resultItem);
}
// 可选:按 itemId 排序
retList.sort(Comparator.comparing(m -> (Integer) m.get("itemId")));
} catch (ServiceException e) {
log.error("查询填报历史记录失败,查询参数:{}", baosongQuery, e);
return Result.error("查询失败:" + e.getMessage());
} catch (Exception e) {
log.error("系统异常,查询填报历史记录失败", e);
return Result.error("系统异常,请稍后重试");
}
return Result.OK(retList);
}
} }
...@@ -290,13 +290,14 @@ public class BaosongTplController extends JeecgController<BaosongTpl, IBaosongTp ...@@ -290,13 +290,14 @@ public class BaosongTplController extends JeecgController<BaosongTpl, IBaosongTp
} }
@GetMapping(value = "/findUserRightForTpl") @GetMapping(value = "/findUserRightForTpl")
public Result<List<BaosongTpl>> findUserRightForTpl(HttpServletRequest req) { public Result<List<BaosongTpl>> findUserRightForTpl(Integer taskId,HttpServletRequest req) {
List<BaosongTpl> retTplList = new ArrayList<>(); List<BaosongTpl> retTplList = new ArrayList<>();
try { try {
String userId = UserUtil.getUserId(); String userId = UserUtil.getUserId();
List<BaosongTaskAlloc> allocTplList = baosongTaskAllocService.lambdaQuery() List<BaosongTaskAlloc> allocTplList = baosongTaskAllocService.lambdaQuery()
.eq(BaosongTaskAlloc::getFillUser, userId) .eq(BaosongTaskAlloc::getFillUser, userId)
.eq(BaosongTaskAlloc::getTaskid,taskId)
.list(); .list();
Set<Integer> tplIdSet = new HashSet<>(); Set<Integer> tplIdSet = new HashSet<>();
for (BaosongTaskAlloc alloc : allocTplList) { for (BaosongTaskAlloc alloc : allocTplList) {
...@@ -319,4 +320,6 @@ public class BaosongTplController extends JeecgController<BaosongTpl, IBaosongTp ...@@ -319,4 +320,6 @@ public class BaosongTplController extends JeecgController<BaosongTpl, IBaosongTp
return Result.error("查询失败,请稍后重试"); return Result.error("查询失败,请稍后重试");
} }
} }
} }
...@@ -34,30 +34,24 @@ public class BaosongDataValid implements Serializable { ...@@ -34,30 +34,24 @@ public class BaosongDataValid implements Serializable {
/**id*/ /**id*/
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private java.lang.Integer id; private java.lang.Integer id;
/**模板ID*/ /**模板ID*/
@Excel(name = "模板", width = 15) @Excel(name = "模板", width = 15)
private Integer tplid; private Integer tplid;
/**公式编号*/ /**公式编号*/
@Excel(name = "公式编号", width = 15) @Excel(name = "公式编号", width = 15)
private java.lang.String code; private java.lang.String code;
/**名称*/ /**名称*/
@Excel(name = "名称", width = 15) @Excel(name = "名称", width = 15)
private java.lang.String name; private java.lang.String name;
/**验证公式*/ /**验证公式*/
@Excel(name = "验证公式", width = 15) @Excel(name = "验证公式", width = 15)
private java.lang.String formula; private java.lang.String formula;
/**类型*/ /**类型*/
@Excel(name = "类型", width = 15) @Excel(name = "类型", width = 15)
private java.lang.Integer ctp; private java.lang.Integer ctp;
/**分类 /**分类
* 1=表内校验 * 1=表内校验
...@@ -66,32 +60,24 @@ public class BaosongDataValid implements Serializable { ...@@ -66,32 +60,24 @@ public class BaosongDataValid implements Serializable {
* 4=不同年份校验 * 4=不同年份校验
* */ * */
@Excel(name = "分类", width = 15) @Excel(name = "分类", width = 15)
private java.lang.Integer tp; private java.lang.Integer tp;
/**预期值*/ /**预期值*/
@Excel(name = "预期值", width = 15) @Excel(name = "预期值", width = 15)
private java.lang.String expectedValue; private java.lang.String expectedValue;
/**更新时间*/ /**更新时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern="yyyy-MM-dd") @DateTimeFormat(pattern="yyyy-MM-dd")
private java.util.Date updateTime; private java.util.Date updateTime;
/**创建时间*/ /**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern="yyyy-MM-dd") @DateTimeFormat(pattern="yyyy-MM-dd")
private java.util.Date createTime; private java.util.Date createTime;
/**参数*/ /**参数*/
@Excel(name = "参数", width = 15) @Excel(name = "参数", width = 15)
private java.lang.String params; private java.lang.String params;
/**说明*/ /**说明*/
@Excel(name = "说明", width = 15) @Excel(name = "说明", width = 15)
private java.lang.String des; private java.lang.String des;
@Excel(name = "itemid", width = 15) @Excel(name = "itemid", width = 15)
private java.lang.Integer tplItemid; private java.lang.Integer tplItemid;
} }
...@@ -151,9 +151,13 @@ spring: ...@@ -151,9 +151,13 @@ spring:
slow-sql-millis: 5000 slow-sql-millis: 5000
datasource: datasource:
master: master:
url: jdbc:mysql://47.98.203.68:3306/zrch_stm_db_3.9?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai # url: jdbc:mysql://47.98.203.68:3306/zrch_stm_db_3.9?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
# username: root
# password: ZhongRunChangHong/123
url: jdbc:mysql://localhost:3306/zrch_stm_db_3.9?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root username: root
password: ZhongRunChangHong/123 password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
# # shardingjdbc数据源 # # shardingjdbc数据源
# sharding-db: # sharding-db:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论