提交 9f85ffd2 authored 作者: kxjia's avatar kxjia

修改 验证组件样式

上级 9ee7229c
<template> <template>
<div class="bank-report-table" style="height: 80vh"> <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">
...@@ -9,54 +9,47 @@ ...@@ -9,54 +9,47 @@
</div> </div>
<vxe-toolbar> <vxe-toolbar>
<template #button> <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-drawer v-model="drawerVisible" placement="right" @show="handleValidationDrawerShow" title="校验结果" width="40%" :footer="{ show: true }"> <ValidationDrawer
<template #default> ref="validationDrawerRef"
<div class="validation-results"> v-model="drawerVisible"
<div class="result-summary"> :tableFormData="tableFormData"
<div class="summary-item"> @validationResultClick="handleValidationResultClick"
<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
...@@ -68,13 +61,15 @@ ...@@ -68,13 +61,15 @@
:row-config="{ resizable: true }" :row-config="{ resizable: true }"
class="custom-table" class="custom-table"
> >
<vxe-column field="serialNumber" title="序号" width="80"></vxe-column> <vxe-column field="serialNumber" title="序号" width="50"></vxe-column>
<vxe-column field="project" title="项目" width="200">
<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>
<vxe-column field="content" title="内容">
<template #default="{ row }"> <template #default="{ row }">
<div class="content-cell"> <div class="content-cell">
<template v-if="row.type === 'MultiColumnTable'"> <template v-if="row.type === 'MultiColumnTable'">
...@@ -95,9 +90,12 @@ ...@@ -95,9 +90,12 @@
:calcSum="row.calcSum" :calcSum="row.calcSum"
:showFooter="row.showFooter" :showFooter="row.showFooter"
:ref="(el) => setAttachTableRef(el, row.code)" :ref="(el) => setAttachTableRef(el, row.code)"
style="margin: 0px; padding: 0px" :disabled="!row.hasRight"
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"
...@@ -107,7 +105,8 @@ ...@@ -107,7 +105,8 @@
:footerData="row.footerData" :footerData="row.footerData"
:showFooter="row.showFooter" :showFooter="row.showFooter"
:ref="(el) => setMyVxeTableRef(el, row.code)" :ref="(el) => setMyVxeTableRef(el, row.code)"
style="margin: 0px; padding: 0px" :disabled="!row.hasRight"
style="margin:0px;padding:0px"
/> />
</template> </template>
<template v-else> <template v-else>
...@@ -266,9 +265,63 @@ ...@@ -266,9 +265,63 @@
</div> </div>
</div> </div>
<template v-else> <template v-else>
<vxe-input :type="item.type" v-model="formData[row.code + '_' + item.field]" size="mini" class="table-input" :style="item.style"> <div class="input-wrapper">
</vxe-input> <vxe-input
:type="item.type"
v-model="formData[getFieldKey(row.code, item.field)]"
size="mini"
class="table-input"
:style="item.style"
@blur="handleInputBlur(row.code, item.field, item.matchedFormula?.formula)"
/>
<span class="unit"> {{ item.unit }}</span> <span class="unit"> {{ item.unit }}</span>
<!-- 帮助图标 -->
<span
v-if="item.hasValidFormula"
class="help-icon"
@click.stop="toggleTooltip(row.code, item.field)"
>
?
</span>
<!-- 错误图标 -->
<span
v-if="inputErrors[getFieldKey(row.code, item.field)]"
class="error-icon"
@click.stop="toggleErrorTooltip(row.code, item.field)"
>
<i class="vxe-icon-error"></i>
</span>
<!-- 错误提示 -->
<div
v-if="showErrorTooltip && hoveredErrorKey === getFieldKey(row.code, item.field)"
class="error-tooltip"
@click.stop
>
{{ inputErrors[getFieldKey(row.code, item.field)].message }}
<br>
<span style="color: #ffcccc;">
公式: {{ inputErrors[getFieldKey(row.code, item.field)].formula }}
</span>
<template v-if="inputErrors[getFieldKey(row.code, item.field)].error">
<br>
<span style="color: #ff9999; font-size: 11px;">
错误: {{ inputErrors[getFieldKey(row.code, item.field)].error }}
</span>
</template>
</div>
<!-- 校验规则提示 -->
<div
v-if="showTooltip && hoveredKey === getFieldKey(row.code, item.field)"
class="tooltip"
@click.stop
>
{{ item.matchedFormula?.des || '暂无校验规则' }}
</div>
</div>
</template> </template>
</template> </template>
</template> </template>
...@@ -279,15 +332,6 @@ ...@@ -279,15 +332,6 @@
<!-- 历史填报检查组件 --> <!-- 历史填报检查组件 -->
<HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @close-drawer="closeHistoryDrawer" /> <HistoryFillCheck ref="historyFillCheckRef" v-model="historyDrawerVisible" @close-drawer="closeHistoryDrawer" />
<!-- 验证公式帮助提示 -->
<div
v-if="validationTooltipVisible"
class="validation-tooltip"
:style="{ left: validationTooltipPosition.left + 'px', top: validationTooltipPosition.top + 'px' }"
>
<pre class="tooltip-content">{{ validationTooltipContent }}</pre>
</div>
<!-- 原有检查组件 --> <!-- 原有检查组件 -->
<CheckTbData ref="refCheckTbData"></CheckTbData> <CheckTbData ref="refCheckTbData"></CheckTbData>
</div> </div>
...@@ -299,8 +343,9 @@ ...@@ -299,8 +343,9 @@
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 HistoryFillCheck from './check/historyFillCheck.vue';
import ValidationDrawer from './check/ValidationDrawer.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, computed } from 'vue';
import { VxeUI, VxeToolbarInstance, VxeToolbarPropTypes } from 'vxe-table'; import { VxeUI, VxeToolbarInstance, VxeToolbarPropTypes } from 'vxe-table';
import { batchSaveOrUpdate, queryRecord, batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api'; import { batchSaveOrUpdate, queryRecord, batchSaveOrUpdateBeforeDelete } from '../../record/BaosongTaskRecord.api';
import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api'; import { queryAllTplItemForUser, findUserRightForTplItem } from '../../alloc/BaosongTaskAlloc.api';
...@@ -310,6 +355,7 @@ ...@@ -310,6 +355,7 @@
const refCheckTbData = ref(); const refCheckTbData = ref();
const historyFillCheckRef = ref<any>(null); const historyFillCheckRef = ref<any>(null);
const validationDrawerRef = ref();
const route = useRoute(); const route = useRoute();
const tableRef = ref(); const tableRef = ref();
...@@ -325,12 +371,12 @@ ...@@ -325,12 +371,12 @@
// 权限相关状态 // 权限相关状态
const userAllocItems = ref<string[]>([]); const userAllocItems = ref<string[]>([]);
const validFormula = ref<any[]>([]); const validFormula = ref<any[]>([]);
const validationResultsList = ref<any[]>([]);
const drawerVisible = ref(false); const drawerVisible = ref(false);
const historyDrawerVisible = ref(false); const historyDrawerVisible = ref(false);
const validationTooltipVisible = ref(false); const showTooltip = ref(false);
const validationTooltipPosition = ref({ left: 0, top: 0 }); const hoveredKey = ref('');
const validationTooltipContent = ref(''); const showErrorTooltip = ref(false);
const hoveredErrorKey = ref('');
interface FormData { interface FormData {
id: number | null; id: number | null;
...@@ -343,6 +389,18 @@ ...@@ -343,6 +389,18 @@
rind: number; rind: number;
} }
interface FormulaItem {
formula: string;
des: string;
[key: string]: any;
}
interface InputError {
message: string;
formula: string;
error?: string;
}
onMounted(async () => { onMounted(async () => {
if (route.query.taskId) { if (route.query.taskId) {
queryParam.value.taskId = Number(route.query.taskId); queryParam.value.taskId = Number(route.query.taskId);
...@@ -375,13 +433,20 @@ ...@@ -375,13 +433,20 @@
} }
await setTplItemMap(); await setTplItemMap();
await setFormItemRight();
await setData(); 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 inputErrors = ref<Record<string, InputError>>({});
const fieldKeys = computed(() => Object.keys(formData));
const getFieldKey = (rowCode: string, field: string): string => {
return `${rowCode}_${field}`;
};
const saveBatch = async () => { const saveBatch = async () => {
try { try {
...@@ -512,121 +577,146 @@ ...@@ -512,121 +577,146 @@
}; };
// 校验数据 // 校验数据
const checkData = () => { const validateData = () => {
drawerVisible.value = true; drawerVisible.value = true;
validationDrawerRef.value.setValidateData(validFormula.value,formData);
}; };
// 校验结果抽屉显示时触发 const handleInputBlur = (rowCode: string, field: string, formula?: string) => {
const handleValidationDrawerShow = () => { if (!formula) return;
performValidation();
};
// 执行校验 const key = getFieldKey(rowCode, field);
const performValidation = () => { const value = formData[key];
validationResultsList.value = [];
const process: string[] = [];
process.push('开始校验');
process.push(`找到 ${validFormula.value.length} 个验证公式`);
validFormula.value.forEach((formulaItem: any) => { if (!value || value === '') {
process.push(`开始校验公式: ${formulaItem.des}`); delete inputErrors.value[key];
const result = evaluateFormula(formulaItem.formula, formulaItem.des, process); return;
if (result) {
validationResultsList.value.push(result);
} }
});
process.push('校验完成'); validateFieldFormula(rowCode, field, formula);
}; };
// 解析和执行验证公式 const validateFieldFormula = (rowCode: string, field: string, formula: string) => {
const evaluateFormula = (formula: string, description: string, process: string[]): any => { const key = getFieldKey(rowCode, field);
try {
const fieldMatch = formula.match(/\[(\w+)\]/);
if (!fieldMatch) {
process.push(`跳过: 无法解析公式字段 ${formula}`);
return null;
}
const fieldName = fieldMatch[1]; try {
const row = tableFormData.find((r: any) => r.content?.some((c: any) => c.field === fieldName)); const expression = buildExpression(formula, rowCode);
if (!row) { const isValid = eval(expression);
process.push(`跳过: 未找到字段 ${fieldName}`); if (!isValid) {
return null; inputErrors.value[key] = {
message: "公式校验失败,请检查填写内容",
formula
};
} else {
delete inputErrors.value[key];
} }
} catch (error: any) {
const fieldItem = row.content.find((c: any) => c.field === fieldName); inputErrors.value[key] = {
if (!fieldItem) { message: "公式校验失败,请检查填写内容",
process.push(`跳过: 未找到字段 ${fieldName}`); formula,
return null; error: error.message
};
} }
};
const strKey = `${row.code}_${fieldName}`; const buildExpression = (formula: string, rowCode: string): string => {
const fieldValue = formData[strKey]; const fieldNames = formula.match(/\b[A-Za-z][A-Za-z0-9_]*\b/g) || [];
const uniqueFields = [...new Set(fieldNames.filter(f =>
!['and', 'or', 'not', 'equal', 'less', 'greater', 'if', 'else', 'true', 'false']
.includes(f.toLowerCase())
))];
// 检查是否有空字段规则 uniqueFields.sort((a, b) => b.length - a.length);
const emptyFieldRule = validFormula.value.find((f: any) => f.formula.includes(fieldName) && f.des.includes('空字段'));
if (emptyFieldRule && (!fieldValue || fieldValue === '')) { let expression = formula;
process.push(`跳过: 字段 ${fieldName} 为空,跳过空字段规则`); for (const fieldName of uniqueFields) {
return null; const key = getFieldKey(rowCode, fieldName);
const value = formData[key];
if (value !== undefined) {
expression = expression.replace(
new RegExp(`\\b${fieldName}\\b`, 'g'),
`Number(${value})`
);
}
} }
const expression = formula.replace(/\[(\w+)\]/g, (_, field) => { return expression;
const value = formData[`${row.code}_${field}`]; };
return value !== undefined ? `Number(${value})` : '0';
});
process.push(`执行公式: ${expression}`);
const isValid = eval(expression);
return { const toggleTooltip = (rowCode: string, field: string) => {
field: fieldName, const key = getFieldKey(rowCode, field);
description: description, if (hoveredKey.value === key) {
formula: formula, showTooltip.value = false;
isValid: isValid, hoveredKey.value = '';
fieldValue: fieldValue, } else {
rowCode: row.code, showTooltip.value = true;
hoveredKey.value = key;
}
}; };
} catch (error) {
process.push(`公式执行错误: ${error instanceof Error ? error.message : String(error)}`); const toggleErrorTooltip = (rowCode: string, field: string) => {
return null; const key = getFieldKey(rowCode, field);
if (hoveredErrorKey.value === key) {
showErrorTooltip.value = false;
hoveredErrorKey.value = '';
} else {
showErrorTooltip.value = true;
hoveredErrorKey.value = key;
} }
}; };
// 处理校验结果点击,定位到表格 const closeAllTooltips = () => {
const handleValidationResultClick = (result: any) => { showTooltip.value = false;
const row = tableFormData.find((r: any) => r.code === result.rowCode); hoveredKey.value = '';
if (row) { showErrorTooltip.value = false;
const fieldItem = row.content.find((c: any) => c.field === result.field); hoveredErrorKey.value = '';
if (fieldItem) { };
const strKey = `${row.code}_${result.field}`;
const inputElement = document.querySelector(`[data-field="${strKey}"]`) as HTMLElement; const setFormItemRight = () => {
if (inputElement) { tableFormData.forEach((row: any) => {
inputElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (row.content && Array.isArray(row.content)) {
inputElement.focus(); row.content.forEach((item: any) => {
inputElement.style.border = '2px solid #ff4d4f'; if (item.field) {
setTimeout(() => { const key = getFieldKey(row.code, item.field);
inputElement.style.border = ''; item.hasRight = userAllocItems.value.includes(key);
}, 2000);
if (item.hasRight) {
const { formula } = findEarliestFormulaForField(validFormula.value, item.field);
item.hasValidFormula = !!formula;
item.matchedFormula = formula;
} }
} }
});
} }
});
}; };
// 显示验证公式帮助 const findEarliestFormulaForField = (
const showValidationHelp = (formulaItem: any, event: MouseEvent) => { formulas: FormulaItem[],
const rect = (event.target as HTMLElement).getBoundingClientRect(); field: string
validationTooltipPosition.value = { ): { formula: FormulaItem | null, index: number } => {
left: rect.left + rect.width / 2, let result = { formula: null, index: Infinity };
top: rect.top - 10,
}; formulas?.forEach((f: FormulaItem) => {
validationTooltipContent.value = `验证公式: ${formulaItem.formula}\n说明: ${formulaItem.des}`; const text = f.formula || '';
validationTooltipVisible.value = true; const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`\\b${escapedField}\\b(?!\\w)`, 'g');
const match = regex.exec(text);
if (match && match.index < result.index) {
result = { formula: f, index: match.index };
}
});
return result;
}; };
// 关闭验证公式帮助 // 处理校验结果点击
const closeTooltip = () => { const handleValidationResultClick = (result: any) => {
validationTooltipVisible.value = false; const message = `字段 ${result.field} 的校验结果: ${result.isValid ? '通过' : '失败'}`;
const status = result.isValid ? 'success' : 'error';
VxeUI.modal.message({ content: message, status });
}; };
// 打开历史填报抽屉 // 打开历史填报抽屉
...@@ -829,109 +919,8 @@ ...@@ -829,109 +919,8 @@
font-size: 14px; 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%;
...@@ -1081,4 +1070,101 @@ ...@@ -1081,4 +1070,101 @@
color: black; color: black;
cursor: pointer; cursor: pointer;
} }
// 输入框包装器
.input-wrapper {
position: relative;
display: inline-block;
}
// 单位文本
.unit {
margin-left: 5px;
font-size: 12px;
}
// 帮助图标
.help-icon {
margin-left: 5px;
cursor: pointer;
color: #1890ff;
font-weight: bold;
font-size: 14px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background: #e6f7ff;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background: #bae7ff;
}
}
// 错误图标
.error-icon {
margin-left: 5px;
cursor: pointer;
color: #ff4d4f;
font-size: 16px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
// 提示框
.tooltip, .error-tooltip {
position: absolute;
top: 100%;
left: 0;
margin-top: 5px;
padding: 10px;
font-size: 12px;
border-radius: 4px;
z-index: 1000;
white-space: pre-wrap;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tooltip {
background: #333;
color: #fff;
&::before {
content: '';
position: absolute;
bottom: 100%;
left: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #333 transparent;
}
}
.error-tooltip {
background: #8b0000;
color: #fff;
border: 1px solid #ff4d4f;
&::before {
content: '';
position: absolute;
bottom: 100%;
left: 5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #8b0000 transparent;
}
}
</style> </style>
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
title="校验结果" title="校验结果"
width="55%" width="55%"
:footer="{ show: true }" :footer="{ show: true }"
height="100%"
> >
<template #default> <template #default>
<div class="validation-results"> <div class="validation-results">
...@@ -302,8 +303,10 @@ defineExpose({ ...@@ -302,8 +303,10 @@ defineExpose({
<style lang="less" scoped> <style lang="less" scoped>
.validation-results { .validation-results {
padding: 20px; padding: 20px;
height: 600px; height: calc(100vh-10px);
overflow-y: auto; overflow-y: auto;
display: flex;
flex-direction: column;
} }
.result-summary { .result-summary {
...@@ -371,6 +374,8 @@ defineExpose({ ...@@ -371,6 +374,8 @@ defineExpose({
.results-list { .results-list {
margin-top: 10px; margin-top: 10px;
flex: 1;
overflow-y: auto;
} }
.result-item { .result-item {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论