华西海圻ELN前端工程
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

552 lines
17 KiB

<template>
<div>
<div class="custom-table-wrapper">
<div class="custom-table-header">
<div class="custom-table-row">
<div v-for="(col, index) in columns" :key="index" class="custom-table-cell header-cell"
:style="{ width: col.width ? col.width + 'px' : 'auto' }">
<div class="header-cell-content">
<div>{{ col.label }}</div>
<template
v-if="col.headerSelectKey && col.headerOptions && (showHeaderSelect || $store.state.template.templateStatus === 'preFill')">
<HandleFormItem type="select" class="header-select" :item="getHeaderItem(col)"
v-model="headerSelectFields[col.headerSelectKey]" @change="onHeaderSelectChange(col, $event)"
:error="hasError(-1, index, col.headerSelectKey)"
@update:error="onErrorUpdate(-1, index, col.headerSelectKey, $event)" />
</template>
<span v-else-if="headerSelectFields[col.headerSelectKey]" class="fill-type-icon">({{
headerSelectFields[col.headerSelectKey] }})</span>
</div>
</div>
<!-- 默认操作栏 -->
<div class="custom-table-cell header-cell" :style="{ width: '180px' }" v-if="showOperation">
<div class="header-cell-content">
<div>操作</div>
</div>
</div>
</div>
</div>
<div class="custom-table-body">
<div v-for="(row, rowIndex) in localDataSource" :key="rowIndex" class="custometable-row">
<div v-for="(col, colIndex) in columns" :key="colIndex" class="custom-table-cell body-cell"
:style="{ width: col.width ? col.width + 'px' : 'auto' }">
<div class="inner-table-cell">
<div>
<template v-if="col.bodyType === 'input'">
<HandleFormItem type="input" @blur="onBlur(rowIndex, col.prop, $event)" @copy="onCopy(rowIndex, col)"
class="body-input" :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
@change="onBodyValueChange(rowIndex, colIndex, $event)"
:error="hasError(rowIndex, colIndex, col.prop)"
@update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)" />
</template>
<template v-else-if="col.bodyType === 'inputNumber'">
<HandleFormItem type="inputNumber" @copy="onCopy(rowIndex, col)" class="body-input-number"
:item="getBodyItem(col, rowIndex)" v-model="row[col.prop]" @blur="onBlur(rowIndex, col.prop, $event)"
@change="onBodyValueChange(rowIndex, colIndex, $event)"
:error="hasError(rowIndex, colIndex, col.prop)"
@update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)" />
</template>
<template v-else-if="col.bodyType === 'select'">
<HandleFormItem type="select" class="body-select" @blur="onBlur(rowIndex, col.prop, $event)"
:item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
@change="onBodyValueChange(rowIndex, colIndex, $event)"
:error="hasError(rowIndex, colIndex, col.prop)"
@update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)" />
</template>
<template v-else>
{{ row[col.prop] }}
</template>
</div>
<div class="m-l-5" v-if="col.showBodySub">
<template v-if="col.bodySubType === 'inputNumber'">
<HandleFormItem type="inputNumber" @blur="onSubBlur(rowIndex, col.bodySubKey, $event)"
@copy="onCopy(rowIndex, col)" :item="getBodySubItem(col)" v-model="row[col.bodySubKey]"
@change="onBodySubValueChange(rowIndex, colIndex, $event)"
:error="hasError(rowIndex, colIndex, col.bodySubKey)"
@update:error="onErrorUpdate(rowIndex, colIndex, col.bodySubKey, $event)" />
</template>
<template v-else>
{{ row[col.bodySubKey] }}
</template>
</div>
</div>
</div>
<!-- 默认操作栏 -->
<div class="custom-table-cell body-cell" :style="{ width: '180px' }" v-if="showOperation">
<div class="inner-table-cell">
<slot name="operation" :row="row" :rowIndex="rowIndex"></slot>
</div>
</div>
</div>
</div>
<div v-if="localDataSource.length == 0">
<div class="no-data">暂无数据</div>
</div>
</div>
<div class="add-row" v-if="isShowAddRos()">
<el-button type="primary" plain @click="addRow">添加行</el-button>
</div>
</div>
</template>
<script>
import HandleFormItem from "./HandleFormItem.vue"
export default {
name: 'CustomTable',
components: {
HandleFormItem
},
props: {
// 是否显示表头选择器
showHeaderSelect: {
type: Boolean,
default: false,
},
showAddRow: {
type: Boolean,
default: true,
},
// 是否显示操作栏
showOperation: {
type: Boolean,
default: true,
},
columns: {
type: Array,
required: true,
// 示例格式:
// [
// { label: '姓名', prop: 'name' },
// { label: '状态', prop: 'status', type: 'select', options: [{value:1,label:'启用'},...], selected: null }
// ]
},
formData: {
type: Object,
default: () => {
return {
stepTableFormData: [],
headerSelectFields: {}
}
}
},
},
data() {
return {
localDataSource: [],
headerSelectFields: {},
formErrors: [] // 表单错误状态管理
}
},
watch: {
formData: {
immediate: true,
handler(newData) {
console.log(newData, "newData")
const { stepTableFormData = [], headerSelectFields = {} } = newData;
this.updateDataSource(stepTableFormData);
this.headerSelectFields = JSON.parse(JSON.stringify(headerSelectFields))
}
},
},
mounted() {
// this.initHeaderSelectValues();
console.log(this.$store.state.template.templateStatus, "this.$store.state.template.templateStatus")
},
methods: {
isShowAddRos() {
if(!this.showAddRow) {
return false;
}
return this.$store.state.template.templateStatus === 'preFill';
},
// 复制值
onCopy(rowIndex, col) {
if (col.copyFrom) {
if (!this.localDataSource[rowIndex][col.copyFrom]) {//没有值就不用复制了
return
}
this.$set(this.localDataSource[rowIndex], col.prop, this.localDataSource[rowIndex][col.copyFrom])
}
},
// 初始化表头选择器值
initHeaderSelectValues() {
const headerSelectObj = {};
this.columns.map(col => {
if (col.headerSelectKey) {
headerSelectObj[col.headerSelectKey] = col.defaultValue || col.headerOptions[0].value || ""
}
});
this.headerSelectFields = headerSelectObj;
},
// 获取最新数据
getFormData() {
// 合并表头选择器值到 columns
// 数据校验
const validateResult = this.validateFormData();
return new Promise((resolve, reject) => {
if (validateResult.valid) {
resolve({
stepTableFormData: [...this.localDataSource],
headerSelectFields: this.headerSelectFields,
})
} else {
// this.$message.error("表单内容未填完,请填写后再提交");
reject(validateResult.errors[0].error)
}
})
},
// 表单数据校验
validateFormData() {
const templateStatus = this.$store.state.template.templateStatus;
const errors = [];
// 清空之前的错误状态
this.formErrors = [];
// 校验表头的 HandleFormItem
this.columns.forEach((col, colIndex) => {
if (col.headerSelectKey && col.headerOptions && col.fillType === templateStatus) {
const headerValue = this.headerSelectFields[col.headerSelectKey];
if (this.isValueEmpty(headerValue)) {
const errorItem = {
rowIndex: -1, // 表头特殊标记
colIndex,
field: col.headerSelectKey,
label: col.label,
error: `请选择${col.label}`
};
errors.push(errorItem);
this.formErrors.push(errorItem);
}
}
});
// 遍历数据行
this.localDataSource.forEach((row, rowIndex) => {
// 遍历列
this.columns.forEach((col, colIndex) => {
// 只校验 fillType 与当前模板状态匹配的字段
if (col.bodyFillType === templateStatus || col.bodySubFillType === templateStatus) {
// 检查主字段
const mainValue = row[col.prop];
if (this.isValueEmpty(mainValue) && !col.bodyDisabled) {
const errorItem = {
rowIndex,
colIndex,
field: col.prop,
label: col.label,
error: `请填写${col.label}`
};
errors.push(errorItem);
this.formErrors.push(errorItem);
}
// 检查子字段(如果有)
if (col.bodySubKey && !col.bodySubDisabled) {
const subValue = row[col.bodySubKey];
console.log(col, subValue, "subValue")
if (this.isValueEmpty(subValue)) {
const errorItem = {
rowIndex,
colIndex,
field: col.bodySubKey,
label: `${col.label}单位`,
error: `请填写${col.label}单位`
};
errors.push(errorItem);
this.formErrors.push(errorItem);
}
}
}
});
});
return {
valid: errors.length === 0,
errors: errors
};
},
// 判断值是否为空
isValueEmpty(value) {
if (value === null || value === undefined || value === '') {
return true;
}
if (typeof value === 'string' && value.trim() === '') {
return true;
}
if (Array.isArray(value) && value.length === 0) {
return true;
}
return false;
},
// 表头选择器变化
onHeaderSelectChange(col, value) {
this.headerSelectFields[col.headerSelectKey] = value;
// 输入时清除对应表单项的错误状态
this.formErrors = this.formErrors.filter(error =>
!(error.rowIndex === -1 &&
error.field === col.headerSelectKey)
);
},
// 表体值变化
onBodyValueChange(rowIndex, colIndex, value) {
const col = this.columns[colIndex];
this.localDataSource[rowIndex][col.prop] = value;
// 输入时清除对应表单项的错误状态
this.formErrors = this.formErrors.filter(error =>
!(error.rowIndex === rowIndex &&
error.colIndex === colIndex &&
error.field === col.prop)
);
this.$emit('body-value-change', rowIndex, colIndex, value);
},
// 表体子值变化
onBodySubValueChange(rowIndex, colIndex, value) {
const col = this.columns[colIndex];
this.localDataSource[rowIndex][col.bodySubKey] = value;
// 输入时清除对应表单项的错误状态
this.formErrors = this.formErrors.filter(error =>
!(error.rowIndex === rowIndex &&
error.colIndex === colIndex &&
error.field === col.bodySubKey)
);
this.$emit('body-sub-value-change', rowIndex, colIndex, value);
},
getHeaderItem(col) {
return {
fillType: col.fillType,
options: col.headerOptions,
label: ""
}
},
getBodyItem(col, rowIndex) {
const currentItem = this.localDataSource[rowIndex];
const item = {
fillType: col.bodyFillType,
options: col.bodyOptions,
maxlength: col.bodyMaxlength,
label: col.label,
precision: currentItem[col.bodyPrecisionKey] || col.precision || 0,
copyFrom: col.copyFrom || "",
};
if (col.bodyDisabled) {
item.disabled = col.bodyDisabled;
}
return item
},
getBodySubItem(col) {
const item = {
fillType: col.bodySubFillType,
options: col.bodySubOptions,
maxlength: col.bodySubMaxlength || 10,
label: "",
placeholder: col.bodySubPlaceholder || "请输入",
precision: col.subPrecision || 0,
}
if (col.bodySubDisabled) {
item.disabled = col.bodySubDisabled;
}
return item
},
// 删除行
deleteRow(rowIndex) {
this.localDataSource.splice(rowIndex, 1);
this.$emit('row-delete', rowIndex);
},
// 更新数据方法,可在formData变更时调用,也可由父组件调用
updateDataSource(dataSource = []) {
// 深拷贝数据以避免直接修改原始数据
this.localDataSource = JSON.parse(JSON.stringify(dataSource || []));
},
// 添加行
addRow(row = {}) {
this.localDataSource.push(row);
},
getDataSource() {
return this.localDataSource;
},
// 根据行索引更新数据
updateDataSourceByRowIndex(rowIndex, data) {
this.localDataSource[rowIndex] = { ...this.localDataSource[rowIndex], ...data };
console.log(this.localDataSource, "this.localDataSource")
this.localDataSource = [...this.localDataSource];
},
// 判断表单项是否有错误
hasError(rowIndex, colIndex, field) {
return this.formErrors.some(error =>
error.rowIndex === rowIndex &&
error.colIndex === colIndex &&
error.field === field
);
},
// 处理错误状态更新
onErrorUpdate(rowIndex, colIndex, field, isError) {
if (!isError) {
this.formErrors = this.formErrors.filter(error =>
!(error.rowIndex === rowIndex &&
error.colIndex === colIndex &&
error.field === field)
);
}
},
onSubBlur(rowIndex, colKey, value) {
this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
},
onBlur(rowIndex, colKey) {
const value = this.localDataSource[rowIndex][colKey];
this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
}
}
};
</script>
<style scoped>
.custom-table-wrapper {
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
font-size: 14px;
color: #606266;
margin-top: 20px;
}
.inner-table-cell {
display: flex;
align-items: center;
justify-content: center;
}
.m-l-5 {
margin-left: 5px;
}
.sub-input-number {
width: 145px;
.el-input-number--mini {
width: 145px;
}
}
/* 表头 */
.custom-table-header {
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
white-space: nowrap;
display: block;
}
.custom-table-body {
max-height: 300px;
/* 可根据需要调整或由父组件控制 */
}
.header-cell-content {
display: flex;
align-items: center;
justify-content: center;
}
/* 共同行样式 */
.custom-table-row {
display: table;
width: 100%;
table-layout: fixed;
}
.custometable-row {
display: table;
width: 100%;
table-layout: fixed;
&:not(:last-child) {
border-bottom: 1px solid #ebeef5;
}
}
/* 单元格 */
.custom-table-cell {
display: table-cell;
padding: 12px 10px;
text-align: left;
vertical-align: middle;
border-right: 1px solid #ebeef5;
box-sizing: border-box;
}
.custom-table-cell:last-child {
border-right: none;
}
.header-cell {
font-weight: bold;
color: #909399;
background-color: #f5f7fa;
}
.body-cell {
color: #606266;
background-color: #fff;
}
/* select 样式(模仿 Element UI) */
.header-cell select {
width: 100%;
padding: 4px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
outline: none;
background-color: #fff;
font-size: 13px;
color: #606266;
appearance: none;
/* 隐藏默认箭头(可选) */
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 14px;
padding-right: 28px;
}
/* 滚动容器:如果整体宽度超限,显示横向滚动条 */
.custom-table-wrapper {
display: flex;
flex-direction: column;
max-width: 100%;
/* 父容器决定宽度 */
overflow-x: auto;
}
.custom-table-header,
.custom-table-body {
min-width: 100%;
}
.header-select {
width: 100px;
margin-left: 5px;
}
.no-data {
text-align: center;
padding: 20px 0;
color: rgb(144, 147, 153)
}
.add-row {
display: flex;
justify-content: center;
padding: 20px 0;
margin-top: 20px;
}
</style>