|
|
- <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>
|