提交 69ccea72 authored 作者: kxjia's avatar kxjia

校验

上级 7120df35
<template> <template>
<div class="bank-report-table" style="height:80vh" @click="closeTooltip"> <div
class="bank-report-table"
style="height:80vh"
@click="closeAllTooltips"
>
<!-- 加载遮罩层 --> <!-- 加载遮罩层 -->
<div v-if="loading" class="loading-overlay"> <div v-if="loading" class="loading-overlay">
<div class="loading-spinner"> <div class="loading-spinner">
...@@ -8,19 +12,43 @@ ...@@ -8,19 +12,43 @@
</div> </div>
</div> </div>
<!-- 工具栏 -->
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #buttons>
<div style="margin:10px"> <div style="margin:10px">
<span style="font-weight: bold"> 填报任务:{{ queryParam?.taskName }} </span> <span class="table-info-text">填报任务:{{ queryParam.taskName }}</span>
<span style="font-weight: bold;margin-left:20px"> 表格:{{ queryParam?.tplCode }} {{ queryParam?.tplName }}</span> <span class="table-info-text" style="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
<vxe-button status="primary" icon="vxe-icon-save" @click="checkData()">校验</vxe-button> status="primary"
<vxe-button status="primary" icon="vxe-icon-save" @click="saveBatch()">保存</vxe-button> icon="vxe-icon-edit"
@click="handleOpenHistoryDrawer"
:disabled="loading"
>
近5年数据填报
</vxe-button>
<vxe-button
status="primary"
icon="vxe-icon-save"
@click="validateData"
>
校验
</vxe-button>
<vxe-button
status="primary"
icon="vxe-icon-save"
@click="saveBatch"
>
保存
</vxe-button>
</template> </template>
</vxe-toolbar> </vxe-toolbar>
<!-- 主表格 -->
<vxe-table <vxe-table
border border
ref="tableRef" ref="tableRef"
...@@ -32,15 +60,17 @@ ...@@ -32,15 +60,17 @@
class="custom-table" class="custom-table"
> >
<vxe-column field="serialNumber" title="序号" width="50"></vxe-column> <vxe-column field="serialNumber" title="序号" width="50"></vxe-column>
<vxe-column field="project" title="项目" width="100"> <vxe-column field="project" title="项目" width="100">
<template #default="{ row }"> <template #default="{ row }">
<span style="font-weight: bold;size:20px">{{ row.project }}</span> <span class="project-name">{{ row.project }}</span>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="content" title="内容" row-resize>
<template #default="{ row }">
<vxe-column field="content" title="内容">
<template #default="{ row }">
<div class="content-cell"> <div class="content-cell">
<!-- 多列表格 -->
<template v-if="row.type === 'MultiColumnTable'"> <template v-if="row.type === 'MultiColumnTable'">
<MultiColumnTable <MultiColumnTable
:title="row.project" :title="row.project"
...@@ -50,6 +80,8 @@ ...@@ -50,6 +80,8 @@
style="margin:0px;padding:0px" style="margin:0px;padding:0px"
/> />
</template> </template>
<!-- 附件表格 -->
<template v-else-if="row.type === 'AttachTable'"> <template v-else-if="row.type === 'AttachTable'">
<AttachTable <AttachTable
:title="row.project" :title="row.project"
...@@ -63,6 +95,8 @@ ...@@ -63,6 +95,8 @@
style="margin:0px;padding:0px" style="margin:0px;padding:0px"
/> />
</template> </template>
<!-- Vxe表格 -->
<template v-else-if="row.type === 'VxeTable'"> <template v-else-if="row.type === 'VxeTable'">
<MyVxeTable1 <MyVxeTable1
:title="row.project" :title="row.project"
...@@ -76,83 +110,159 @@ ...@@ -76,83 +110,159 @@
style="margin:0px;padding:0px" style="margin:0px;padding:0px"
/> />
</template> </template>
<!-- 普通字段渲染 -->
<template v-else> <template v-else>
<template v-for="(item, index) in row.content" :key="index"> <template v-for="(item, index) in row.content" :key="index">
<!-- 文本类型 -->
<span v-if="item.type === 'text'" v-html="item.value"></span> <span v-if="item.type === 'text'" v-html="item.value"></span>
<!-- 换行 -->
<span v-else-if="item.type === 'br'"><br></span> <span v-else-if="item.type === 'br'"><br></span>
<!-- 带缩进的换行 -->
<span v-else-if="item.type === 'brspace'"> <span v-else-if="item.type === 'brspace'">
<br> <br>
<template v-for="i in item.value||1" :key="i"> <template v-for="i in (item.value || 1)" :key="i">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</template> </template>
</span> </span>
<!-- 单选组 -->
<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
:disabled="!item.hasRight || (item.relatedFiled&&formData[row.code +'_'+ item.relatedFiled]!='是')" v-model="formData[getFieldKey(row.code, item.field)]"
:disabled="!item.hasRight || isRadioDisabled(row.code, item)"
> >
<vxe-radio <vxe-radio
v-for="(opt, optIndex) in item.options" v-for="(opt, optIndex) in item.options"
:key="optIndex" :key="optIndex"
:label="opt" :label="opt"
>{{ opt }}</vxe-radio
> >
{{ opt }}
</vxe-radio>
</vxe-radio-group> </vxe-radio-group>
</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]" :disabled="!item.hasRight"> <vxe-checkbox-group
<vxe-checkbox v-for="(opt, optIndex) in item.options" :key="optIndex" :label="opt"> v-model="formData[getFieldKey(row.code, item.field)]"
:disabled="!item.hasRight"
>
<vxe-checkbox
v-for="(opt, optIndex) in item.options"
:key="optIndex"
:label="opt"
>
{{ opt }} {{ opt }}
</vxe-checkbox> </vxe-checkbox>
</vxe-checkbox-group>
<!-- 其他选项 -->
<template v-if="item.otherField"> <template v-if="item.otherField">
<div style="width:100%"> <div style="width:100%">
<vxe-checkbox label="其他" :disabled="!item.hasRight"> <vxe-checkbox label="其他" :disabled="!item.hasRight">
其他: 其他:
</vxe-checkbox> </vxe-checkbox>
<vxe-input v-model="formData[row.code + '_'+ item.otherField]" <vxe-input
:disabled="!(formData[row.code+'_'+item.field]?.indexOf('其他')>-1) || !item.hasRight" v-model="formData[getFieldKey(row.code, item.otherField)]"
:disabled="!isOtherFieldEnabled(row.code, item) || !item.hasRight"
style="width: 50%" style="width: 50%"
> @blur="handleInputBlur(row.code, item.otherField, item.matchedFormula?.formula)"
</vxe-input> />
</div> </div>
</template> </template>
</vxe-checkbox-group>
</span> </span>
</div> </div>
<!-- 多选带输入框 -->
<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]" :disabled="!item.hasRight"> <vxe-checkbox-group
<vxe-checkbox v-for="(opt, optIndex) in item.options" :key="optIndex" :label="opt" :disabled="!item.hasRight" style="margin-left: 0px;display: block;"> v-model="formData[getFieldKey(row.code, item.field)]"
:disabled="!item.hasRight"
>
<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]]" :disabled="!item.hasRight" style="width: 100px;margin: 0px;"> <vxe-input
</vxe-input> type="number"
v-model="formData[getFieldKey(row.code, item.optionItemField[optIndex])]"
:disabled="!item.hasRight"
style="width: 100px;margin: 0px;"
@blur="handleInputBlur(row.code, item.optionItemField[optIndex], item.matchedFormula?.formula)"
/>
<span class="unit"> {{ item.optionItemUint }}</span> <span class="unit"> {{ item.optionItemUint }}</span>
</vxe-checkbox> </vxe-checkbox>
</vxe-checkbox-group> </vxe-checkbox-group>
</span> </span>
</div> </div>
<!-- 普通输入框 -->
<template v-else> <template v-else>
<div class="input-wrapper"> <div class="input-wrapper">
<vxe-input <vxe-input
:disabled="!item.hasRight || (item.relatedFiled&&formData[row.code +'_'+ item.relatedFiled]!='是')" :disabled="!item.hasRight || isInputDisabled(row.code, item)"
:type="item.type" :type="item.type"
v-model="formData[row.code +'_'+ item.field]" v-model="formData[getFieldKey(row.code, item.field)]"
size="mini" size="mini"
class="table-input" class="table-input"
:style="item.style" :style="item.style"
:status="getInputStatus(row.code, item.field)" @blur="handleInputBlur(row.code, item.field, item.matchedFormula?.formula)"
@input="handleInputChange(row.code, item.field)" />
<span class="unit">{{ item.unit }}</span>
<!-- 帮助图标 -->
<span
v-if="item.hasValidFormula"
class="help-icon"
@click.stop="toggleTooltip(row.code, item.field)"
>
?
</span>
<!-- 错误图标 -->
<span
v-if="inputErrors[getFieldKey(row.code, item.field)]"
class="error-icon"
@click.stop="toggleErrorTooltip(row.code, item.field)"
>
<i class="vxe-icon-error"></i>
</span>
<!-- 错误提示 -->
<div
v-if="showErrorTooltip && hoveredErrorKey === getFieldKey(row.code, item.field)"
class="error-tooltip"
@click.stop
> >
</vxe-input> {{ inputErrors[getFieldKey(row.code, item.field)].message }}
<span class="unit"> {{ item.unit }}</span> <br>
<span v-if="item.hasValidFormula" class="help-icon" @click.stop="toggleTooltip(row.code, item.field)">?</span> <span style="color: #ffcccc;">
<div v-if="getInputStatus(row.code, item.field) === 'error'" class="error-tip"> 公式: {{ inputErrors[getFieldKey(row.code, item.field)].formula }}
{{ getErrorMessage(row.code, item.field) }} </span>
<template v-if="inputErrors[getFieldKey(row.code, item.field)].error">
<br>
<span style="color: #ff9999; font-size: 11px;">
错误: {{ inputErrors[getFieldKey(row.code, item.field)].error }}
</span>
</template>
</div> </div>
<div v-if="showTooltip && hoveredKey === row.code +'_'+ item.field" class="tooltip" @click.stop>
{{ getValidationRule(row.code, item.field) }} <!-- 校验规则提示 -->
<div
v-if="showTooltip && hoveredKey === getFieldKey(row.code, item.field)"
class="tooltip"
@click.stop
>
{{ item.matchedFormula?.des || '暂无校验规则' }}
</div> </div>
</div> </div>
</template> </template>
...@@ -161,760 +271,762 @@ ...@@ -161,760 +271,762 @@
</div> </div>
</template> </template>
</vxe-column> </vxe-column>
<!-- 备注列 -->
<vxe-column field="remarks" title="备注" width="60"> <vxe-column field="remarks" title="备注" width="60">
<template #default="{ row }"> <template #default="{ row }">
<vxe-textarea :rows="row.remarks.rows" v-model="formData[row.code+'_'+row.remarks.field]"> <vxe-textarea
</vxe-textarea> :rows="row.remarks.rows"
v-model="formData[getFieldKey(row.code, row.remarks.field)]"
:disabled="!row.hasRight"
/>
</template> </template>
</vxe-column> </vxe-column>
</vxe-table> </vxe-table>
<!-- 校验结果抽屉 --> <!-- 校验抽屉 -->
<vxe-drawer <ValidationDrawer
ref="validationDrawerRef"
v-model="drawerVisible" v-model="drawerVisible"
placement="right" :tableFormData="tableFormData"
@show="handleValidationDrawerShow" @validationResultClick="handleValidationResultClick"
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 <HistoryFillCheck
v-for="(item, index) in validationResultsList" ref="historyFillCheckRef"
:key="index" v-model="historyDrawerVisible"
class="result-item" @closeDrawer="closeHistoryDrawer"
: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>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { VxeUI, VxeTableInstance } from 'vxe-table'
import 'vxe-table/lib/style.css'
import { useRoute } from 'vue-router'
// 组件导入
import MultiColumnTable from '../tableComponents/MultiColumnTable.vue' import MultiColumnTable from '../tableComponents/MultiColumnTable.vue'
import AttachTable from '../tableComponents/AttachTable.vue' import AttachTable from '../tableComponents/AttachTable.vue'
import MyVxeTable1 from '../tableComponents/MyVxeTable.vue' import MyVxeTable1 from '../tableComponents/MyVxeTable.vue'
import HistoryFillCheck from './check/HistoryFillCheck.vue' import HistoryFillCheck from './check/HistoryFillCheck.vue'
import ValidationDrawer from './check/ValidationDrawer.vue'
import { tableFormData } from '../../data/tb1.data';
import { ref, reactive,nextTick,onMounted,toRaw } from 'vue' // 数据导入
import { VxeUI,VxeToolbarInstance,VxeToolbarPropTypes } from 'vxe-table' import { tableFormData } from '../../data/tb1.data'
import { batchSaveOrUpdate, queryRecord,batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api'
import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api' // API 导入
import {
batchSaveOrUpdate,
queryRecord,
batchSaveOrUpdateBeforeDelete
} from '../../record/BaosongTaskRecord.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 { getTblvalidFormula } from '../../tpl/BaosongDataValid.api'
import { useRoute } from 'vue-router'; // 类型定义
interface QueryParam {
const historyFillCheckRef = ref(); taskId: number
const historyDrawerVisible = ref(false); taskName: string
tplId: number
const route = useRoute(); tplName: string
const tableRef = ref(); tplCode: string
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({ interface FormDataItem {
taskId:-1,
taskName:'',
tplId:-1,
tplName:'',
tplCode:''
})
interface FormData {
id: number | null id: number | null
taskid: number taskid: number
tplid: number tplid: number
itemid?: number itemid?: number
itempid?:number itempid?: number
content: string content: string
tplcode:string tplcode: string
rind:number rind: number
} }
// 初始化loading状态 interface FormulaItem {
const loading = ref(true) formula: string
des: string
[key: string]: any
}
interface ContentItem {
type: string
field: string
value?: any
hasRight?: boolean
hasValidFormula?: boolean
matchedFormula?: FormulaItem
relatedFiled?: string
otherField?: string
optionItemField?: string[]
[key: string]: any
}
interface TableRow {
code: string
project: string
type?: string
content: ContentItem[] | any
remarks: { rows: number; field: string }
hasRight?: boolean
[key: string]: any
}
interface InputError {
message: string
formula: string
error?: string
}
onMounted(async ()=>{ interface TplItemMapValue {
if (route.query.taskId) { pid: number
queryParam.value.taskId = Number(route.query.taskId); itemid: number
} formTp: string
if (route.query.taskName) { code: string
queryParam.value.taskName = String(route.query.taskName); }
}
if (route.query.tplId) {
queryParam.value.tplId = Number(route.query.tplId);
}
if (route.query.tplName) {
queryParam.value.tplName = String(route.query.tplName);
}
if (route.query.tplCode) {
queryParam.value.tplCode = String(route.query.tplCode);
}
try { // 路由和组件引用
userAllocItems.value = await findUserRightForTplItem({ const route = useRoute()
tplid:queryParam.value.tplId, const tableRef = ref<VxeTableInstance>()
taskid:queryParam.value.taskId const historyFillCheckRef = ref()
}); const validationDrawerRef = ref()
validFormula.value = await getTblvalidFormula({ // 响应式状态
tplid:queryParam.value.tplId, const loading = ref(true)
}); const historyDrawerVisible = ref(false)
const drawerVisible = ref(false)
await setFormItemRight(); const showTooltip = ref(false)
await setTplItemMap() const hoveredKey = ref('')
await setData(); const showErrorTooltip = ref(false)
} catch (error) { const hoveredErrorKey = ref('')
VxeUI.modal.message({
content: `初始化页面失败: ${error.message}`, // 数据状态
status: 'error' const queryParam = ref<QueryParam>({
}); taskId: -1,
} finally { taskName: '',
loading.value = false tplId: -1,
} tplName: '',
tplCode: ''
}) })
const setFormItemRight = async () => { const formData = reactive<Record<string, any>>({})
tableFormData.forEach(row => { const formValues = ref<FormDataItem[]>([])
if (row.content && Array.isArray(row.content)) { const inputErrors = ref<Record<string, InputError>>({})
row.content.forEach(item => { const userAllocItems = ref<string[]>([])
if (item.field) { const validFormula = ref<FormulaItem[]>([])
let tmpKey = `${row.code}_${item.field}`; const tplItemMap = ref<Record<string, TplItemMapValue>>({})
item["hasRight"] = userAllocItems.value.indexOf(tmpKey)>-1
item["hasValidFormula"] = validFormula.value?.length > 0 && // 子组件引用
validFormula.value.some((f: any) => { const childMultiTableRefs = ref<Record<string, any>>({})
const formulaText = (f.formula || '').toString() const childAttachTableRefs = ref<Record<string, any>>({})
return formulaText.includes(item.field) const childMyVexTableRefs = ref<Record<string, any>>({})
})
} // 计算属性
}); const fieldKeys = computed(() => Object.keys(formData))
}
}); // 工具函数
const getFieldKey = (rowCode: string, field: string): string => {
return `${rowCode}_${field}`
} }
const saveBatch = async () => { const isRadioDisabled = (rowCode: string, item: ContentItem): boolean => {
try { if (!item.relatedFiled) return false
const saveLoading = ref(true) const relatedKey = getFieldKey(rowCode, item.relatedFiled)
formValues.value = [] return formData[relatedKey] !== '是'
}
for (const strKey in formData) { const isInputDisabled = (rowCode: string, item: ContentItem): boolean => {
const valData = formData[strKey] if (!item.relatedFiled) return false
if (valData) { const relatedKey = getFieldKey(rowCode, item.relatedFiled)
let tmpAry = strKey.split("_") return formData[relatedKey] !== '是'
await setFormValues(tmpAry[0],tmpAry[1],valData,1) }
}
} const isOtherFieldEnabled = (rowCode: string, item: ContentItem): boolean => {
await getAttachTableFormData(); const checkboxKey = getFieldKey(rowCode, item.field)
await getAllMultiTableFormData() const checkboxValue = formData[checkboxKey] || []
if (formValues.value.length > 0) { return checkboxValue.includes('其他')
await batchSaveOrUpdateBeforeDelete(formValues.value) }
VxeUI.modal.message({ content: '保存成功', status: 'success' })
} else { // 初始化
VxeUI.modal.message({ content: '没有需要保存的数据', status: 'warning' }) onMounted(async () => {
} await initPage()
} catch (error) { })
VxeUI.modal.message({ content: `保存失败: ${error.message}`, status: 'error' })
const initPage = async () => {
try {
loading.value = true
await initQueryParams()
await Promise.all([
loadUserRights(),
loadValidationFormulas(),
loadTemplateItems()
])
await setFormItemRight()
await loadTableData()
} catch (error: any) {
showErrorMessage(`初始化页面失败: ${error.message}`)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
const handleOpenHistoryDrawer = () => { const initQueryParams = () => {
historyFillCheckRef.value.onDrawerShow(queryParam.value.tplId); const { taskId, taskName, tplId, tplName, tplCode } = route.query
historyDrawerVisible.value = true;
if (taskId) queryParam.value.taskId = Number(taskId)
if (taskName) queryParam.value.taskName = String(taskName)
if (tplId) queryParam.value.tplId = Number(tplId)
if (tplName) queryParam.value.tplName = String(tplName)
if (tplCode) queryParam.value.tplCode = String(tplCode)
} }
const closeHistoryDrawer = () => { const loadUserRights = async () => {
historyDrawerVisible.value = false; userAllocItems.value = await findUserRightForTplItem({
tplid: queryParam.value.tplId,
taskid: queryParam.value.taskId
})
} }
const checkData = async () => { const loadValidationFormulas = async () => {
validationResultsList.value = [] validFormula.value = await getTblvalidFormula({
drawerVisible.value = true tplid: queryParam.value.tplId
})
} }
const handleValidationDrawerShow = () => { const loadTemplateItems = async () => {
performValidation() const items = await allTplItems({
tplid: queryParam.value.tplId
})
tplItemMap.value = items.reduce((acc: Record<string, TplItemMapValue>, item) => {
const key = `${item.pcode}_${item.xmlcode}`
acc[key] = {
pid: item.pid,
itemid: item.id,
formTp: item.formTp,
code: item.xmlcode
}
return acc
}, {})
} }
const performValidation = () => { const setFormItemRight = () => {
validationResultsList.value = [] tableFormData.forEach((row: TableRow) => {
const process: string[] = [] if (row.content && Array.isArray(row.content)) {
process.push('开始校验') row.content.forEach((item: ContentItem) => {
process.push(`找到 ${validFormula.value.length} 个验证公式`) if (item.field) {
const key = getFieldKey(row.code, item.field)
item.hasRight = userAllocItems.value.includes(key)
validFormula.value.forEach((formulaItem: any) => { if (item.hasRight) {
process.push(`开始校验公式: ${formulaItem.des}`) const { formula } = findEarliestFormulaForField(validFormula.value, item.field)
const result = evaluateFormula(formulaItem.formula, formulaItem.des, process) item.hasValidFormula = !!formula
if (result) { item.matchedFormula = formula
validationResultsList.value.push(result) }
}
})
} }
}) })
process.push('校验完成')
} }
const evaluateFormula = (formula: string, formulaDes: string, process: string[]): ValidationResult | null => { // 保存相关
const fieldRegex = /COL\d+/g const saveBatch = async () => {
const fields = [...new Set(formula.match(fieldRegex) || [])] try {
process.push(`公式包含字段: ${fields.join(', ')}`) if (!await validateForm()) return
const fieldValues: Record<string, any> = {} await prepareFormData()
let hasEmptyField = false
for (const field of fields) { if (formValues.value.length === 0) {
const value = getFieldValue(field) showWarningMessage('没有需要保存的数据')
fieldValues[field] = value return
if (value === '' || value === null || value === undefined) {
hasEmptyField = true
process.push(`字段 ${field} 值为空,跳过此公式校验`)
} }
await batchSaveOrUpdateBeforeDelete(formValues.value)
showSuccessMessage('保存成功')
} catch (error: any) {
showErrorMessage(`保存失败: ${error.message}`)
} }
}
if (hasEmptyField) { const validateForm = async (): Promise<boolean> => {
return { const $table = tableRef.value
fieldTitle: formulaDes, if (!$table) return true
fieldName: fields.join(', '),
formula, const checkResult = $table.validate()
isValid: true, if (!checkResult) {
resultMessage: '跳过校验 (有字段值为空)', showErrorMessage('表单验证失败,请检查填写内容')
process: [...process] return false
}
return true
}
const prepareFormData = async () => {
formValues.value = []
// 处理主表单数据
for (const [key, value] of Object.entries(formData)) {
if (value !== undefined && value !== null && value !== '') {
const [pcode, code] = key.split('_')
await addFormValue(pcode, code, value, 1)
} }
} }
process.push('所有字段值都已获取,开始计算') // 处理子表格数据
process.push(`字段值: ${JSON.stringify(fieldValues)}`) await Promise.all([
collectAttachTableData(),
collectMultiTableData(),
collectVxeTableData()
])
}
try { const addFormValue = async (pcode: string, code: string, value: any, rind: number) => {
const sanitizedFormula = sanitizeAndEvaluate(formula, fieldValues, process) if (!value) return
process.push(`计算公式: ${sanitizedFormula}`)
const result = evaluateString(sanitizedFormula) const stringValue = Array.isArray(value) ? value.join(',') : String(value)
process.push(`计算结果: ${result}`) const key = getFieldKey(pcode, code)
const item = tplItemMap.value[key]
if (result) { if (!item) return
return {
fieldTitle: formulaDes, const formItem: FormDataItem = {
fieldName: fields.join(', '), id: null,
formula, rind,
isValid: true, taskid: queryParam.value.taskId,
resultMessage: '校验通过', tplid: queryParam.value.tplId,
fieldValues, tplcode: code,
process: [...process] itemid: item.itemid,
} itempid: item.pid,
} else { content: stringValue
return {
fieldTitle: formulaDes,
fieldName: fields.join(', '),
formula,
isValid: false,
resultMessage: '校验失败 - 验证公式返回 false',
fieldValues,
process: [...process]
} }
formValues.value.push(formItem)
}
// 数据收集函数
const collectAttachTableData = async () => {
for (const [pcode, child] of Object.entries(childAttachTableRefs.value)) {
if (child) {
const datas = await child.getFormData()
for (const [code, value] of Object.entries(datas)) {
await addFormValue(pcode, code, value, 1)
} }
} 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 => { const collectMultiTableData = async () => {
for (const row of tableFormData) { for (const [pcode, child] of Object.entries(childMultiTableRefs.value)) {
if (row.content && Array.isArray(row.content)) { if (child) {
for (const item of row.content) { const datas = await child.getFormData()
if (item.field === field) { let rind = 0
return formData[`${row.code}_${field}`] for (const obj of datas) {
rind++
for (const [code, value] of Object.entries(obj)) {
await addFormValue(pcode, code, value, rind)
} }
} }
} }
} }
return null
} }
const sanitizeAndEvaluate = (formula: string, fieldValues: Record<string, any>, process: string[]): string => { const collectVxeTableData = async () => {
let sanitizedFormula = formula for (const [pcode, child] of Object.entries(childMyVexTableRefs.value)) {
if (child) {
const wordBoundaryRegex = /COL\d+/g const formData = await child.getFormData()
sanitizedFormula = sanitizedFormula.replace(wordBoundaryRegex, (match) => { let rind = 0
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, '!==') for (const obj of formData.datas) {
rind++
for (const [code, value] of Object.entries(obj)) {
await addFormValue(pcode, code, value, rind)
}
}
return sanitizedFormula // 表尾数据
if (formData.footer) {
for (const [code, value] of Object.entries(formData.footer)) {
await addFormValue(pcode, code, value, 1)
}
}
}
}
} }
const evaluateString = (str: string): any => { // 数据加载
const loadTableData = async () => {
try { try {
const func = new Function('return ' + str) loading.value = true
return func()
} catch (error) { // 清空现有数据
throw new Error(`公式计算错误: ${str}`) Object.keys(formData).forEach(key => delete formData[key])
const recordData = await queryRecord({
taskid: queryParam.value.taskId,
tplid: queryParam.value.tplId
})
// 转换记录数据为便于查找的结构
const valueMap = recordData.reduce((acc: Record<string, string>, data) => {
const key = `${data.itempid}_${data.itemid}_${data.rind}`
acc[key] = data.content
return acc
}, {})
// 填充表格数据
await fillTableData(valueMap)
} catch (error: any) {
showErrorMessage(`加载数据失败: ${error.message}`)
} finally {
loading.value = false
} }
} }
const getInputStatus = (rowCode: string, field: string): 'success' | 'error' | '' => { const fillTableData = async (valueMap: Record<string, string>) => {
const key = `${rowCode}_${field}` for (const row of tableFormData) {
return validationResults.value[key] === 'error' ? 'error' : '' if (!row.type && row.content) {
await fillSimpleFields(row, valueMap)
} else if (row.type === 'AttachTable' && row.datas) {
await fillAttachTable(row, valueMap)
} else if (row.type === 'MultiColumnTable' && row.content) {
await fillMultiColumnTable(row, valueMap)
} else if (row.type === 'VxeTable' && row.columns) {
await fillVxeTable(row, valueMap)
}
}
} }
const getErrorMessage = (rowCode: string, field: string): string => { const fillSimpleFields = async (row: TableRow, valueMap: Record<string, string>) => {
const key = `${rowCode}_${field}` if (!row.content || !Array.isArray(row.content)) return
return validationMessages.value[key] || ''
}
const handleInputChange = (rowCode: string, field: string) => { for (const item of row.content) {
validateField(rowCode, field) if (!item.field) continue
}
const validateField = (rowCode: string, field: string) => { const key = getFieldKey(row.code, item.field)
const key = `${rowCode}_${field}` const tplItem = tplItemMap.value[key]
validationResults.value[key] = '' if (!tplItem) continue
validationMessages.value[key] = ''
const relevantFormulas = validFormula.value.filter((f: any) => const { pid, itemid, formTp } = tplItem
f.formula.includes(field) const valueKey = `${pid}_${itemid}_1`
) const dataVal = valueMap[valueKey]
if (relevantFormulas.length > 0) { if (dataVal === undefined) continue
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 if (formTp === 'checkbox') {
for (const fld of fields) { formData[key] = dataVal.split(',').filter(Boolean)
const value = getFieldValue(fld) } else {
fieldValues[fld] = value formData[key] = dataVal
if (value === '' || value === null || value === undefined) {
hasEmptyField = true
}
} }
if (!hasEmptyField) { // 处理特殊字段类型
try { if (item.type === 'checkboxAndInput' && item.optionItemField) {
const sanitizedFormula = sanitizeAndEvaluate(formula.formula, fieldValues, []) await fillCheckboxInputFields(row, item, valueMap)
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 : '未知错误'}`
} }
if (item.otherField) {
await fillOtherField(row, item, valueMap)
} }
} }
} }
const toggleTooltip = (rowCode: string, field: string) => { const fillCheckboxInputFields = async (
const key = `${rowCode}_${field}` row: TableRow,
if (hoveredKey.value === key) { item: ContentItem,
showTooltip.value = false valueMap: Record<string, string>
hoveredKey.value = '' ) => {
} else { for (const field of item.optionItemField || []) {
showTooltip.value = true const key = getFieldKey(row.code, field)
hoveredKey.value = key const tplItem = tplItemMap.value[key]
if (!tplItem) continue
const { pid, itemid } = tplItem
const valueKey = `${pid}_${itemid}_1`
const dataVal = valueMap[valueKey]
if (dataVal !== undefined) {
formData[key] = dataVal
}
} }
} }
const closeTooltip = () => { const fillOtherField = async (
showTooltip.value = false row: TableRow,
hoveredKey.value = '' item: ContentItem,
} valueMap: Record<string, string>
) => {
const key = getFieldKey(row.code, item.otherField!)
const tplItem = tplItemMap.value[key]
if (!tplItem) return
const getValidationRule = (rowCode: string, field: string): string => { const { pid, itemid } = tplItem
const relevantFormulas = validFormula.value.filter((f: any) => const valueKey = `${pid}_${itemid}_1`
f.formula.includes(field) const dataVal = valueMap[valueKey]
)
if (relevantFormulas.length > 0) { if (dataVal !== undefined) {
return `${relevantFormulas[0].des}\n公式: ${relevantFormulas[0].formula}` formData[key] = dataVal
} }
return '暂无校验规则'
} }
const formatJSON = (obj: any): string => { const fillAttachTable = async (row: TableRow, valueMap: Record<string, string>) => {
return JSON.stringify(obj, null, 2) const child = childAttachTableRefs.value[row.code]
} if (!child) return
const getFillDatas = async() => { const tableData: Record<string, any> = {}
try { for (const data of row.datas || []) {
formValues.value = [] for (const [key, value] of Object.entries(data)) {
if (typeof value === 'object' && value.field) {
const fieldKey = getFieldKey(row.code, value.field)
const tplItem = tplItemMap.value[fieldKey]
if (!tplItem) continue
for (const strKey in formData) { const { pid, itemid, formTp, code } = tplItem
const valData = formData[strKey] const valueKey = `${pid}_${itemid}_1`
if (valData!==undefined&&valData!=="") { const dataVal = valueMap[valueKey]
let tmpAry = strKey.split("_")
await setFormValues(tmpAry[0],tmpAry[1],valData,1) if (dataVal) {
tableData[code] = formTp === 'checkbox' ? dataVal.split(',') : dataVal
}
} }
} }
await getAttachTableFormData();
await getAllMultiTableFormData()
await getVxeTableFormData()
} catch (error) {
console.log(error)
} finally {
} }
child.setFormData(tableData)
} }
const fillMultiColumnTable = async (row: TableRow, valueMap: Record<string, string>) => {
const child = childMultiTableRefs.value[row.code]
if (!child || !Array.isArray(row.content)) return
const childMultiTableRefs = ref({}) const rowsMap: Record<string, Record<string, string>> = {}
const setMultiColumnTableRef = (el: any, index: string) => {
if (el) { for (const item of row.content) {
childMultiTableRefs.value[index] = el; if (!item.field) continue
}
};
const key = getFieldKey(row.code, item.field)
const tplItem = tplItemMap.value[key]
if (!tplItem) continue
const getAllMultiTableFormData = async () => { const { pid, itemid, code } = tplItem
for (const pcode in childMultiTableRefs.value) { const valueKeys = Object.keys(valueMap).filter(k =>
const child = childMultiTableRefs.value[pcode]; k.startsWith(`${pid}_${itemid}_`)
if (child) { )
const datas = await child.getFormData()
let rind = 0 for (const valueKey of valueKeys) {
for(const obj of datas) { const [, , rind] = valueKey.split('_')
rind++ if (!rowsMap[rind]) {
Object.keys(obj).forEach(code => { rowsMap[rind] = {}
setFormValues(pcode,code,obj[code],rind)
});
} }
rowsMap[rind][code] = valueMap[valueKey]
} }
} }
};
const tableDatas = Object.values(rowsMap)
child.setFormData(tableDatas)
}
const childAttachTableRefs = ref({}) const fillVxeTable = async (row: TableRow, valueMap: Record<string, string>) => {
const setAttachTableRef = (el: any, index: string) => { const child = childMyVexTableRefs.value[row.code]
if (el) { if (!child || !Array.isArray(row.columns)) return
childAttachTableRefs.value[index] = el; // 保存子组件实例
}
};
const childMyVexTableRefs = ref({}) const rowsMap: Record<string, Record<string, string>> = {}
const setMyVxeTableRef = (el: any, index: string) => {
if (el) {
childMyVexTableRefs.value[index] = el; // 保存子组件实例
}
};
const getAttachTableFormData = async () => { for (const column of row.columns) {
for (const pcode in childAttachTableRefs.value) { if (!column.field) continue
const child = childAttachTableRefs.value[pcode];
if (child) {
const datas = await child.getFormData()
for(const code in datas) {
await setFormValues(pcode,code,datas[code],1)
}
}
}
};
const getVxeTableFormData = async () => { const key = getFieldKey(row.code, column.field)
for (const pcode in childMyVexTableRefs.value) { const tplItem = tplItemMap.value[key]
const child = childMyVexTableRefs.value[pcode]; if (!tplItem || Object.keys(tplItem).length === 0) continue
if (child) {
const formData = await child.getFormData() const { pid, itemid, code } = tplItem
console.log("formData",formData) const valueKeys = Object.keys(valueMap).filter(k =>
let rind = 0 k.startsWith(`${pid}_${itemid}_`)
for(const obj of formData.datas) { )
rind++
Object.keys(obj).forEach(code => { for (const valueKey of valueKeys) {
setFormValues(pcode,code,obj[code],rind) const [, , rind] = valueKey.split('_')
}); if (!rowsMap[rind]) {
} rowsMap[rind] = {}
if(formData.footer) {
Object.keys(formData.footer).forEach(code => {
setFormValues(pcode,code,formData.footer[code],1)
});
} }
rowsMap[rind][code] = valueMap[valueKey]
} }
} }
}; const tableDatas = Object.values(rowsMap)
child.setFormData(tableDatas)
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, const validateData = () => {
rind:rind, drawerVisible.value = true
taskid: queryParam.value.taskId, validationDrawerRef.value.setValidateData(validFormula.value,formData);
tplid: queryParam.value.tplId,
tplcode: code,
itemid: itemid,
itempid: pid,
content:vals
};
formValues.value.push(tempForm)
} }
async function setData() { const handleInputBlur = (rowCode: string, field: string, formula?: string) => {
try { if (!formula) return
loading.value = true;
const taskId = queryParam.value.taskId;
const tplid = queryParam.value.tplId;
Object.keys(formData).forEach(key => delete formData[key]); const key = getFieldKey(rowCode, field)
const value = formData[key]
await setTplItemMap(); if (!value || value === '') {
const recordData = await queryRecord({ taskid: taskId, tplid: tplid }); delete inputErrors.value[key]
const valueObj = {}; return
for (const data of recordData) {
const key = `${data.itempid}_${data.itemid}_${data.rind}`;
valueObj[key] = data.content;
}
for (const row of tableFormData) {
if(!row.type&&row.content) {
for (const cdata of row.content) {
if(cdata.field&&cdata.field.length>0) {
const strKey = `${row.code}_${cdata.field}`;
const item = tplItemMap.value[strKey];
if (!item) continue;
const { pid, itemid, formTp } = item;
const dataVal = valueObj[`${pid}_${itemid}_1`];
if (!dataVal) continue;
if (formTp === 'checkbox') {
formData[strKey] = dataVal?.split(",") || [];
} else {
formData[strKey] = dataVal || "";
} }
if(cdata.type&&cdata.type=="checkboxAndInput") { validateFieldFormula(rowCode, field, formula)
for (const itemField of cdata.optionItemField) { }
const strItemKey = `${row.code}_${itemField}`;
const item2 = tplItemMap.value[strItemKey];
if (!item2) continue;
const { pid:pid2, itemid:itemid2 } = item2;
const dataVal2 = valueObj[`${pid2}_${itemid2}_1`];
formData[strItemKey] = dataVal2 || "";
}
}
if(cdata.otherField){ const validateFieldFormula = (rowCode: string, field: string, formula: string) => {
const strItemKey3 = `${row.code}_${cdata.otherField}`; const key = getFieldKey(rowCode, field)
const item3 = tplItemMap.value[strItemKey3];
if (!item3) continue;
const { pid:pid3, itemid:itemid3 } = item3;
const dataVal3 = valueObj[`${pid3}_${itemid3}_1`];
formData[strItemKey3] = dataVal3 || ""
}
try {
const expression = buildExpression(formula, rowCode)
const isValid = eval(expression)
if (!isValid) {
inputErrors.value[key] = {
message: "公式校验失败,请检查填写内容",
formula
} }
}
} else if(row.type=="AttachTable"&& row.datas) {
const curAttachTable = childAttachTableRefs.value[row.code];
const attachTableData = {};
for (const cdata of row.datas) {
Object.keys(cdata).forEach(key => {
if(cdata[key]["field"]){
const strKey = `${row.code}_${cdata[key]["field"]}`;
const item = tplItemMap.value[strKey];
if (item) {
const { pid, itemid, formTp, code } = item;
const dataVal = valueObj[`${pid}_${itemid}_1`];
if (dataVal) {
if (formTp === 'checkbox') {
attachTableData[code] = dataVal?.split(",") || [];
} else { } else {
attachTableData[code] = dataVal || ""; delete inputErrors.value[key]
}
}
} }
} catch (error: any) {
inputErrors.value[key] = {
message: "公式校验失败,请检查填写内容",
formula,
error: error.message
} }
});
} }
}
curAttachTable.setFormData(attachTableData); const buildExpression = (formula: string, rowCode: string): string => {
} else if(row.type=="MultiColumnTable"&& row.content) { const fieldNames = formula.match(/\b[A-Za-z][A-Za-z0-9_]*\b/g) || []
const curMultiTable = childMultiTableRefs.value[row.code]; const uniqueFields = [...new Set(fieldNames.filter(f =>
!['and', 'or', 'not', 'equal', 'less', 'greater', 'if', 'else', 'true', 'false']
.includes(f.toLowerCase())
))]
const rowsMap = {}; uniqueFields.sort((a, b) => b.length - a.length)
for (const cdata of row.content) {
if(cdata.field&&cdata.field.length>0) {
const strKey = `${row.code}_${cdata.field}`;
const tmpData = tplItemMap.value[strKey];
if (!tmpData) continue;
const { pid, itemid, code } = tmpData;
const valKeys = Object.keys(valueObj).filter(k =>
k.startsWith(`${pid}_${itemid}_`)
);
for (const valKey of valKeys) { let expression = formula
const parts = valKey.split('_'); for (const fieldName of uniqueFields) {
const rind = parts[2]; const key = getFieldKey(rowCode, fieldName)
if (!rowsMap[rind]) { const value = formData[key]
rowsMap[rind] = {}; if (value !== undefined) {
expression = expression.replace(
new RegExp(`\\b${fieldName}\\b`, 'g'),
`Number(${value})`
)
} }
rowsMap[rind][code] = valueObj[valKey];
} }
return expression
}
// 提示工具相关
const toggleTooltip = (rowCode: string, field: string) => {
const key = getFieldKey(rowCode, field)
if (hoveredKey.value === key) {
showTooltip.value = false
hoveredKey.value = ''
} else {
showTooltip.value = true
hoveredKey.value = key
} }
}
const toggleErrorTooltip = (rowCode: string, field: string) => {
const key = getFieldKey(rowCode, field)
if (hoveredErrorKey.value === key) {
showErrorTooltip.value = false
hoveredErrorKey.value = ''
} else {
showErrorTooltip.value = true
hoveredErrorKey.value = key
} }
}
const tableDatas = Object.values(rowsMap); const closeAllTooltips = () => {
curMultiTable.setFormData(tableDatas); showTooltip.value = false
} else if(row.type=="VxeTable"&& row.columns) { hoveredKey.value = ''
debugger; showErrorTooltip.value = false
const curVexTable = childMyVexTableRefs.value[row.code]; hoveredErrorKey.value = ''
const rowsMap = {}; }
for (const cdata of row.columns) {
if(cdata.field&&cdata.field.length>0) {
const strKey = `${row.code}_${cdata.field}`;
const tmpData = tplItemMap.value[strKey];
if (!tmpData || Object.keys(tmpData).length === 0) continue; // 历史填报
const { pid, itemid, code } = tmpData; const handleOpenHistoryDrawer = () => {
historyFillCheckRef.value?.onDrawerShow(queryParam.value.tplId)
historyDrawerVisible.value = true
}
console.log("tmpData",tmpData) const closeHistoryDrawer = () => {
historyDrawerVisible.value = false
console.log("valueObj",valueObj) }
const valKeys = Object.keys(valueObj).filter(k => // 校验结果处理
k.startsWith(`${pid}_${itemid}_`) const handleValidationResultClick = (result: any) => {
); const message = `字段 ${result.field} 的校验结果: ${result.isValid ? '通过' : '失败'}`
const status = result.isValid ? 'success' : 'error'
console.log("valKeys",valKeys) VxeUI.modal.message({ content: message, status })
}
// 工具函数
const findEarliestFormulaForField = (
formulas: FormulaItem[],
field: string
): { formula: FormulaItem | null, index: number } => {
let result = { formula: null, index: Infinity }
for (const valKey of valKeys) { formulas?.forEach((f: FormulaItem) => {
const parts = valKey.split('_'); const text = f.formula || ''
const rind = parts[2]; const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
if (!rowsMap[rind]) { const regex = new RegExp(`\\b${escapedField}\\b(?!\\w)`, 'g')
rowsMap[rind] = {}; const match = regex.exec(text)
}
rowsMap[rind][code] = valueObj[valKey]; if (match && match.index < result.index) {
result = { formula: f, index: match.index }
} }
})
} return result
} }
const tableDatas = Object.values(rowsMap);
curVexTable.setFormData(tableDatas);
}
}
} catch (error) {
console.log(error)
VxeUI.modal.message({
content: `加载数据失败:`,
status: 'error'
});
} finally {
loading.value = false;
}
// 消息提示
const showSuccessMessage = (message: string) => {
VxeUI.modal.message({ content: message, status: 'success' })
} }
async function setTplItemMap() { const showWarningMessage = (message: string) => {
try { VxeUI.modal.message({ content: message, status: 'warning' })
const tplid = queryParam.value.tplId }
const tplItem = await allTplItems({
tplid: tplid
})
tplItemMap.value = {} const showErrorMessage = (message: string) => {
tplItem.forEach(item => { VxeUI.modal.message({ content: message, status: 'error' })
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' }) const setMultiColumnTableRef = (el: any, code: string) => {
} finally { if (el) childMultiTableRefs.value[code] = el
}
} const setAttachTableRef = (el: any, code: string) => {
if (el) childAttachTableRefs.value[code] = el
} }
const setMyVxeTableRef = (el: any, code: string) => {
if (el) childMyVexTableRefs.value[code] = el
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
...@@ -925,6 +1037,7 @@ async function setTplItemMap() { ...@@ -925,6 +1037,7 @@ async function setTplItemMap() {
color: #000; color: #000;
margin: 5px auto; margin: 5px auto;
width: 90%; width: 90%;
position: relative;
// Shared table styles // Shared table styles
.custom-table, .attachment-table { .custom-table, .attachment-table {
...@@ -940,35 +1053,43 @@ async function setTplItemMap() { ...@@ -940,35 +1053,43 @@ async function setTplItemMap() {
} }
} }
// 表格信息文本
.table-info-text {
font-weight: bold;
}
// 项目名称
.project-name {
font-weight: bold;
font-size: 14px;
}
// Cell styles // Cell styles
.content-cell { .content-cell {
line-height: 1.8; line-height: 1.8;
} }
// 表单控件组
.radio-group, .checkbox-group { .radio-group, .checkbox-group {
display: inline-block; display: inline-block;
margin-left: 10px; margin-left: 10px;
.vxe-radio, .vxe-checkbox { .vxe-radio, .vxe-checkbox {
margin-left: 0px; margin-left: 0px;
white-space: nowrap; white-space: nowrap;
line-height: 30px; line-height: 30px;
padding: 0px 10px; padding: 0px 10px;
&--label { &--label {
font-size: 12px; font-size: 12px;
overflow: display;
} }
&--icon { &--icon {
font-size: 12px; font-size: 12px;
} }
} }
} }
.checkbox-group {
}
// VXE specific overrides // VXE specific overrides
.vxe { .vxe {
&-radio-group, &-checkbox-group { &-radio-group, &-checkbox-group {
...@@ -991,16 +1112,19 @@ async function setTplItemMap() { ...@@ -991,16 +1112,19 @@ async function setTplItemMap() {
background: transparent; background: transparent;
} }
// 输入框包装器
.input-wrapper { .input-wrapper {
position: relative; position: relative;
display: inline-block; display: inline-block;
} }
// 单位文本
.unit { .unit {
margin-left: 5px; margin-left: 5px;
font-size: 12px; font-size: 12px;
} }
// 帮助图标
.help-icon { .help-icon {
margin-left: 5px; margin-left: 5px;
cursor: pointer; cursor: pointer;
...@@ -1014,32 +1138,51 @@ async function setTplItemMap() { ...@@ -1014,32 +1138,51 @@ async function setTplItemMap() {
text-align: center; text-align: center;
background: #e6f7ff; background: #e6f7ff;
border-radius: 50%; border-radius: 50%;
transition: all 0.3s;
&:hover {
background: #bae7ff;
}
} }
.error-tip { // 错误图标
position: absolute; .error-icon {
bottom: -20px; margin-left: 5px;
left: 0; cursor: pointer;
color: #f5222d; color: #ff4d4f;
font-size: 12px; font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
} }
.tooltip { // 提示框
.tooltip, .error-tooltip {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
margin-top: 5px; margin-top: 5px;
padding: 10px; padding: 10px;
background: #333;
color: #fff;
font-size: 12px; font-size: 12px;
border-radius: 4px; border-radius: 4px;
z-index: 1000; z-index: 1000;
white-space: pre-wrap; white-space: pre-wrap;
max-width: 300px; max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
} }
.tooltip::before { .tooltip {
background: #333;
color: #fff;
&::before {
content: ''; content: '';
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
...@@ -1047,152 +1190,26 @@ async function setTplItemMap() { ...@@ -1047,152 +1190,26 @@ async function setTplItemMap() {
border-width: 5px; border-width: 5px;
border-style: solid; border-style: solid;
border-color: transparent transparent #333 transparent; border-color: transparent transparent #333 transparent;
}
} }
.validation-results { .error-tooltip {
max-height: calc(100vh - 200px); background: #8b0000;
overflow-y: auto; color: #fff;
} border: 1px solid #ff4d4f;
.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 { &::before {
display: inline-block; content: '';
width: 20px; position: absolute;
height: 20px; bottom: 100%;
line-height: 20px; left: 5px;
text-align: center; border-width: 5px;
background: #e8e8e8; border-style: solid;
border-radius: 50%; border-color: transparent transparent #8b0000 transparent;
font-size: 10px; }
font-weight: bold;
} }
// 加载遮罩
.loading-overlay { .loading-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
......
<template>
<div>
<vxe-drawer
v-model="drawerVisible"
placement="right"
@show="handleValidationDrawerShow"
title="校验结果"
width="55%"
: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="process-log" v-if="showProcessLog && validationProcess.length > 0">
<h4>执行过程:</h4>
<div class="process-content">
<div v-for="(log, index) in validationProcess" :key="index" class="process-item">
{{ log }}
</div>
</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-formula">公式:{{ result.formula }}</div>
<div class="result-value">值:{{ formatFieldValue(result.fieldValue) }}</div>
<div v-if="!result.isValid" class="result-error">失败原因:{{ result.description }}</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="drawer-footer">
<vxe-button @click="toggleProcessLog">
{{ showProcessLog ? '隐藏' : '显示' }}执行过程
</vxe-button>
<vxe-button status="primary" @click="drawerVisible = false">关闭</vxe-button>
</div>
</template>
</vxe-drawer>
<!-- 验证公式帮助提示 -->
<div
v-if="validationTooltipVisible"
class="validation-tooltip"
:style="{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }"
@mouseleave="closeTooltip"
>
<pre class="tooltip-content">{{ validationTooltipContent }}</pre>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
interface ValidationResult {
field: string
description: string
formula: string
isValid: boolean
fieldValue: any
rowCode: string
}
interface ValidationFormula {
formula: string
des: string
}
interface TableRow {
code: string
content: Array<{
field: string
[key: string]: any
}>
}
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
tableFormData: {
type: Array as () => TableRow[],
default: () => []
},
})
const emit = defineEmits(['update:modelValue', 'validationResultClick', 'showValidationHelp'])
const drawerVisible = ref(false)
const validationResultsList = ref<ValidationResult[]>([])
const validationProcess = ref<string[]>([])
const showProcessLog = ref(false)
const validationTooltipVisible = ref(false)
const validationTooltipPosition = ref({ left: 0, top: 0 })
const validationTooltipContent = ref('')
// 通过外部传入的数据
const validFormula = ref<ValidationFormula[]>([])
const formData = ref<Record<string, any>>({})
watch(() => props.modelValue, (newVal) => {
drawerVisible.value = newVal
})
watch(drawerVisible, (newVal) => {
emit('update:modelValue', newVal)
})
const handleValidationDrawerShow = () => {
performValidation()
}
const performValidation = () => {
validationResultsList.value = []
validationProcess.value = []
validationProcess.value.push('开始校验')
validationProcess.value.push(`找到 ${validFormula.value.length} 个验证公式`)
validFormula.value.forEach((formulaItem: ValidationFormula) => {
validationProcess.value.push(`开始校验公式: ${formulaItem.des}`)
const result = evaluateFormula(formulaItem.formula, formulaItem.des)
if (result) {
validationResultsList.value.push(result)
}
})
validationProcess.value.push('校验完成')
validationProcess.value.push(`总计处理: ${validationResultsList.value.length} 条结果`)
}
const evaluateFormula = (formula: string, description: string): ValidationResult | null => {
try {
// 修复正则表达式 - $$需要转义
const fieldMatch = formula.match(/([A-Za-z_]\w*)/)
if (!fieldMatch) {
validationProcess.value.push(`跳过: 无法解析公式字段 ${formula}`)
return null
}
const fieldName = fieldMatch[1]
validationProcess.value.push(`解析到字段: ${fieldName}`)
// 查找字段所在行
const row = props.tableFormData.find((r: TableRow) =>
r.content?.some((c: any) => c.field === fieldName)
)
if (!row) {
validationProcess.value.push(`跳过: 未找到字段 ${fieldName} 所在的行`)
return null
}
validationProcess.value.push(`找到字段所在行: ${row.code}`)
const fieldItem = row.content.find((c: any) => c.field === fieldName)
if (!fieldItem) {
validationProcess.value.push(`跳过: 未找到字段 ${fieldName}`)
return null
}
const strKey = `${row.code}_${fieldName}`
const fieldValue = formData.value[strKey]
validationProcess.value.push(`字段值: ${fieldValue} (key: ${strKey})`)
// 检查空字段规则
const emptyFieldRule = validFormula.value.find((f: ValidationFormula) =>
f.formula.includes(fieldName) && f.des.includes('空字段')
)
if (emptyFieldRule && (!fieldValue || fieldValue === '')) {
validationProcess.value.push(`跳过: 字段 ${fieldName} 为空,跳过空字段规则`)
return null
}
// 构建表达式 - 修复变量引用问题
const expression = formula.replace(/\$\$(\w+)\$\$/g, (_, field) => {
const key = `${row.code}_${field}`
const value = formData.value[key]
return value !== undefined && value !== '' ? `Number(${value})` : '0'
})
validationProcess.value.push(`执行公式: ${expression}`)
// 安全地执行表达式
const isValid = safeEvalExpression(expression)
validationProcess.value.push(`校验结果: ${isValid ? '通过' : '失败'}`)
return {
field: fieldName,
description: description,
formula: formula,
isValid: isValid,
fieldValue: fieldValue,
rowCode: row.code
}
} catch (error) {
validationProcess.value.push(`公式执行错误: ${error instanceof Error ? error.message : String(error)}`)
return null
}
}
// 安全的表达式求值函数
const safeEvalExpression = (expression: string): boolean => {
try {
// 使用 Function 构造函数创建安全的求值环境
const func = new Function(`return ${expression}`)
const result = func()
// 处理布尔值返回
if (typeof result === 'boolean') {
return result
}
// 处理数值比较结果
if (typeof result === 'number') {
return Boolean(result)
}
// 处理字符串
if (typeof result === 'string') {
return result.toLowerCase() === 'true'
}
return Boolean(result)
} catch (error) {
validationProcess.value.push(`表达式求值错误: ${error}`)
return false
}
}
const formatFieldValue = (value: any): string => {
if (value === undefined || value === null) {
return '空'
}
if (value === '') {
return '空字符串'
}
return String(value)
}
const handleValidationResultClick = (result: ValidationResult) => {
emit('validationResultClick', result)
}
const showValidationHelp = (formulaItem: ValidationFormula, 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 toggleProcessLog = () => {
showProcessLog.value = !showProcessLog.value
}
const setValidateData = (formulaData: ValidationFormula[], formDataObj: Record<string, any>) => {
validFormula.value = formulaData
formData.value = formDataObj
}
defineExpose({
performValidation,
showValidationHelp,
closeTooltip,
setValidateData
})
</script>
<style lang="less" scoped>
.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;
}
.process-log {
margin: 15px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #1890ff;
h4 {
margin: 0 0 10px 0;
color: #1890ff;
font-size: 14px;
}
}
.process-content {
max-height: 200px;
overflow-y: auto;
font-family: monospace;
}
.process-item {
font-size: 12px;
color: #666;
padding: 2px 0;
border-bottom: 1px dashed #eee;
&:last-child {
border-bottom: none;
}
}
.results-list {
margin-top: 10px;
}
.result-item {
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.result-item.success {
background-color: #f6ffed;
border-left: 4px solid #52c41a;
&:hover {
background-color: #e8f7e0;
}
}
.result-item.error {
background-color: #fff2f0;
border-left: 4px solid #ff4d4f;
&:hover {
background-color: #ffe9e6;
}
}
.result-field {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.result-desc {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.result-formula {
font-size: 12px;
color: #888;
font-family: monospace;
margin-bottom: 5px;
background-color: rgba(0, 0, 0, 0.02);
padding: 2px 4px;
border-radius: 2px;
word-break: break-all;
}
.result-value {
font-size: 12px;
color: #999;
}
.result-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 5px;
font-weight: bold;
}
.validation-tooltip {
position: fixed;
background-color: #333;
color: #fff;
padding: 12px;
border-radius: 6px;
font-size: 12px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 400px;
transform: translateX(-50%);
&::before {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
border-width: 5px 5px 0;
border-style: solid;
border-color: #333 transparent transparent;
}
}
.tooltip-content {
margin: 0;
white-space: pre-wrap;
line-height: 1.5;
}
.drawer-footer {
display: flex;
justify-content: space-between;
padding: 10px;
border-top: 1px solid #eee;
}
</style>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论