要实现的效果是: 点击保存回校验当前页面的所有输入框
首先 分成两个上下两个子组件,
上面的子组件是一个表单包括规则名称和区域
下面的子组件是一个表格,表格可以是多行的,需要校验每一行的输入框
父组件调用两个子组件的校验方法, 第一个子组件可以直接校验,第二个子组件在分别调用下面的子组件
最外层-父组件
<template>
<div>
<!-- 基础信息 -->
<baseForm
ref="baseFormRef"
:mode="mode"
@cityUpdate="cityUpdate"
/>
<h3>规则配置</h3>
<ruleConfigurationTable
ref="ruleConfigRef"
v-model="detailsData.ruleConfigModels"
:mode="mode"
:details-data="detailsData"
/>
<div>
<el-button @click="saveRule" > 保存 </el-button>
</div>
</div>
</template>
<script>
import {
defineComponent, reactive, toRefs, getCurrentInstance,
} from 'vue';
import baseForm from '../components/BaseForm';
import ruleConfigurationTable from '../components/RuleConfigurationTable';
import { ruleDefaultData } from '../constants';
export default defineComponent({
name: 'PriceComparisonRulesManagement',
components: {
baseForm,
ruleConfigurationTable,
},
setup() {
const { proxy } = getCurrentInstance();
const mode = proxy.$query.get('mode') || '';
const id = +proxy.$query.get('id') || '';
// 如果是创建,默认输入框都是空
const detailsData = mode === 'create' ? ruleDefaultData : {};
const initData = reactive({
mode,
detailsData,
});
// 详情接口
const initialization = async () => {
try {
const ruleDetail = await proxy.$hPost('/detail', {id});
// 基础信息,这里调用子组件的setFormData对每个输入框进行赋值
const { ruleName, managementCityId } = ruleDetail;
proxy.$refs.baseFormRef?.setFormData({ ruleName, managementCityId });
// 规则配置,这里赋值给一个变量,然后传递给子组件
initData.detailsData = ruleDetail;
} catch (e) {
console.log(e);
}
};
initialization();
// 返回
const cancel = () => {//自定义返回地址};
// 保存规则接口
const saveCompetition = async () => {
try {
const params = {
...initData.detailsData,
};
// 拿基础信息输入框的信息
const { managementCityId, managementCityName, ruleName } = await proxy.$refs.baseFormRef?.getUpdatedFormData();
Object.assign(params, { managementCityId, managementCityName, ruleName });
await proxy.$hPost('/price/parity/rule/insertOrUpdate', params);
cancel();
} catch (e) {
console.log(e);
}
};
// 保存规则校验
const saveRule = async () => {
// 校验基础信息
const res1 = await proxy.$refs.baseFormRef?.validate().catch(() => false);
// 规则配置
const res2 = await proxy.$refs.ruleConfigRef?.validate().catch(() => false);
if (!res1 || !res2) return false;
// 如果这两个子组件通过校验的话, 那么就可以调保存接口了
saveCompetition();
}
return {
...toRefs(initData),
cancel,
saveRule,
cityUpdate,
};
},
});
</script>
constants.js
// 竞价规则初始化数据
export const ruleDefaultData = {
id: '',
ruleName: '', // 规则名称,比价区域城市名/ID
managementCityId: '',
managementCityName: '',
ruleConfigModels: [
{
procurementOrgIds: [], // 第一列 组织
strategyConfigByCategories: [ // 第二列 商品分层
{
priceType: 2, // 对标价格类型
productType: [], // 第三列
cptType: [], // 竞对设置
},
],
},
],
};
// 规则配置表头
export function ruleConfigurationTableHeader() {
return [
{
label: '商品所属组织',
width: '20%',
},
{
label: '商品分层',
width: '20%',
},
{
label: '对标价格类型',
width: '20%',
},
{
label: '竞对设置',
width: '40%',
},
];
}
baseForm - 子组件
<template>
<el-form
ref="baseFormRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item
label="规则名称"
prop="ruleName"
>
<el-input
v-model="formData.ruleName"
/>
</el-form-item>
<el-form-item
label="比价区域"
prop="managementCityId"
>
<!-- 这里其实就是el-selsct -->
<manageCitySelect
ref="manageCitySelectRef"
v-model="formData.managementCityId"
/>
</el-form-item>
</el-form>
</template>
<script>
const getBaseFormRules = () => ({
ruleName: [
{ required: true, message: '请输入规则名称', trigger: 'blur' },
],
managementCityId: [
{ required: true, message: '请选择比价区域', trigger: 'blur' },
],
});
import manageCitySelect from 'components/ManageCitySelect';
import {
defineComponent, computed, getCurrentInstance, reactive, toRefs,
} from 'vue';
// 获取初始化的数据
import {
ruleDefaultData,
} from '../constants';
export default defineComponent({
name: 'BaseForm',
components: {
manageCitySelect,
},
setup() {
const { proxy } = getCurrentInstance();
const initData = reactive({
formData: ruleDefaultData,
});
// 自动校验
const rules = computed(() => getBaseFormRules());
// 手动校验, 父组件点击保存的时候调用validate方法
const validate = () => proxy.$refs.baseFormRef?.validate();
// 回显. 父组件调用setFormData把数据带过来
const setFormData = (newFormData) => {
Object.assign(initData.formData, newFormData);
};
// 父组件点击保存的时候调用getUpdatedFormData获取最新数据
const getUpdatedFormData = async () => {
const { formData } = initData;
return { ...formData };
};
return {
...toRefs(initData),
rules,
validate,
setFormData,
getUpdatedFormData,
};
},
});
</script>
RuleConfigurationTable 子组件
<template>
<div class="content">
<dl class="multi-level-table">
<dt>
<div
v-for="(column, c1) in columns"
:key="c1"
class="th"
:style="`width: ${column.width}`"
>
<span v-text="column.label"></span>
</div>
</dt>
<dd
v-for="(item, i) in detailsData.ruleConfigModels"
:key="`${i}_1`"
>
<!-- 第一行的第一个表格---所属组织-->
<div
class="td p10"
:style="`width: ${columns[0].width}`"
>
<BelongOrganization
ref="purchasingOrganizationRef"
v-model="item.procurementOrgIds"
:index="i"
:details-data="detailsData"
/>
<AddDelButtonGroup
:index-list="[i]"
:disabled="detailsData.ruleConfigModels.length === 1"
@operator="operatorRow"
/>
</div>
<!-- 第一行的第二个表格 商品分层-->
<div
v-for="(strategy, s) in item.strategyConfigByCategories"
:key="`${s}_2`"
class="td-line"
>
<div
class="td p10"
:style="`width: ${columns[1].width}`"
>
<GoodsStratification
ref="commodityStratification"
v-model="strategy.productType"
:strategy-config-by-categories="item.strategyConfigByCategories"
/>
<AddDelButtonGroup
:index-list="[i, s]"
:disabled="item.strategyConfigByCategories.length === 1"
@operator="operatorRow"
/>
</div>
<!-- 第一行的第三个表格 对标价格类型 -->
<div
class="td p10"
:style="`width: ${columns[2].width};`"
>
<div
class="td p10"
:style="`width: ${columns[2].width};`"
>
<!-- 忽略 -->
</div>
</div>
<!-- 第一行的第四个表格 -->
<div
class="p10 td"
:style="`width: ${columns[3].width}`"
>
<!-- 忽略 -->
</div>
</div>
</dd>
</dl>
</div>
</template>
<script>
import {
defineComponent, getCurrentInstance, reactive, toRefs,
} from 'vue';
import {
ruleConfigurationTableHeader, ruleDefaultData,
} from '../constants';
import AddDelButtonGroup from './AddDelButtonGroup';
import GoodsStratification from './GoodsStratification';
import BelongOrganization from './BelongOrganization';
export default defineComponent({
name: 'ConventionalPricingTableRef',
components: {
AddDelButtonGroup,
GoodsStratification,
BelongOrganization,
},
props: {
detailsData: {
type: Object,
default: () => ({}),
},
},
setup(props, { emit }) {
const { proxy } = getCurrentInstance();
// 从库里引入的深拷贝函数
const { cloneDeep } = proxy.$tools;
// 初始化
const initData = reactive({
// 表头
columns: ruleConfigurationTableHeader(),
// 添加的空数据列
defaultDataSimulation: ruleDefaultData,
});
// 父组件调用校验
const validate = () => {
// 商品分层的校验
const proArr1 = proxy.$refs.commodityStratification.map((item, index) => (
proxy.$refs.commodityStratification[index].validate()
));
// 所属组织的校验
const proArr2 = proxy.$refs.purchasingOrganizationRef.map((item, index) => (
proxy.$refs.purchasingOrganizationRef[index].validate()
));
return Promise.all([...proArr1, ...proArr2]);
};
// 添加/复制/删除每行数据
const operatorRow = (obj) => {
const { isAdd, indexList } = obj || {};
const indexLen = indexList.length;
// 拷贝一下原数据(ruleConfigModels是规则配置的数据),等下操作这个数据的增删
const sourceData = cloneDeep(props.detailsData.ruleConfigModels);
switch (indexLen) {
case 1: // 添加第一列
if (isAdd === 'copy') {
// 实现逻辑是如果复制的是组织,那么需要将组织项清空,如果不深拷贝的话,会影响原数据
const copyValue = sourceData[indexList[0]];
const deepCloneCopy = cloneDeep(copyValue);
deepCloneCopy.procurementOrgIds = [];
sourceData.splice(+indexList[0] + 1, 0, cloneDeep(deepCloneCopy));
} else if (isAdd === 'add') {
sourceData.splice(+indexList[0] + 1, 0, cloneDeep(initData.defaultDataSimulation.ruleConfigModels[0]));
} else {
sourceData.splice(+indexList[0], 1);
}
break;
case 2:// 添加第二列
if (isAdd === 'copy') {
// 要复制的数据
const copyValue = sourceData[indexList[0]].strategyConfigByCategories[indexList[1]];
const deepCloneCopy = cloneDeep(copyValue);
deepCloneCopy.productType = [];
sourceData[+indexList[0]].strategyConfigByCategories.splice(+indexList[1] + 1, 0, cloneDeep(deepCloneCopy));
} else if (isAdd === 'add') {
sourceData[+indexList[0]].strategyConfigByCategories.splice(+indexList[1] + 1, 0, cloneDeep(initData.defaultDataSimulation.ruleConfigModels[0].strategyConfigByCategories[0]));
} else {
sourceData[+indexList[0]].strategyConfigByCategories.splice(+indexList[1], 1);
}
break;
default:
break;
}
emit('input', sourceData);
};
return {
...toRefs(initData),
operatorRow,
validate,
};
},
});
</script>
<style scoped lang="scss">
$tBorderColor: #edf0f5;
.multi-table-wrapper {
width: 100%;
overflow-x: auto;
}
.multi-level-table {
min-width: 100%;
display: table;
border: $tBorderColor 1px solid;
border-bottom: none;
dt,
dd {
width: 100%;
margin: 0;
padding: 0;
border-bottom: $tBorderColor 1px solid;
display: table;
table-layout: fixed;
line-height: 20px;
.th,
.td {
display: table-cell;
vertical-align: middle!important;
font-size: 12px;
border-right: $tBorderColor 1px solid;
&:last-child {
border-right: none;
}
line-height: 22px;
box-sizing: border-box;
&.p10 {
padding: 0 10px;
}
&.inner {
padding: 0 10px;
height: 120px;
}
&.inner-mini {
padding: 0 10px;
height: 80px;
}
}
.th {
padding: 10px;
}
.td-line {
width: 100%;
display: table;
table-layout: fixed;
border-bottom: $tBorderColor 1px solid;
&:last-child {
border-bottom: none;
}
}
.error-tips {
margin: 0;
padding: 5px 0;
color: #ff0000;
}
}
dt {
background: #f5f6f7;
font-weight: bold;
}
.border-box {
box-sizing: border-box
}
.table {
display: table;
}
}
</style>
AddDelButtonGroup 孙组件
<template>
<div class="btn-wrapper">
<el-button
type="text"
@click="operatorRow('copy')"
>
复制
</el-button>
<el-button
type="text"
@click="operatorRow('add')"
>
添加
</el-button>
<el-button
type="text"
v-bind="$attrs"
@click="operatorRow('delete')"
>
删除
</el-button>
</div>
</template>
<script>
export default {
name: 'AddDelButtonGroup',
props: {
indexList: {
type: Array,
required: true,
},
},
setup(props, { emit }) {
// 复制/添加\删除行
const operatorRow = (isAdd) => {
emit('operator', { isAdd, indexList: props.indexList.map((index) => `${index}`) });
};
return {
operatorRow,
};
},
};
</script>
<style lang="scss" scoped>
.btn-wrapper {
margin-top: 10px;
}
.del-btn {
color: #ff0000;
&:disabled {
color: #B0B3B8;
}
}
</style>
BelongOrganization 组织组件(孙组件)
<template>
<el-form
ref="BelongOrganizationRef"
:model="ruleForm"
:rules="rules"
style="padding-top: 10px;"
>
<el-form-item prop="procurementOrgIds">
<!-- 这里其实是一个el-tree -->
<PurchasingOrganizationDeletable
ref="purchasingOrganizationRef"
v-model="ruleForm.procurementOrgIds"
:multiple="true"
node-key="id"
:tag-tender="true"
:merge-value="true"
:organization-data="organizationData"
:clone-deep-organization-data="cloneDeepOrganizationData"
:remote="false"
:disabled-ids="getPurchasingOrgDisList(index)"
@change="change"
/>
</el-form-item>
</el-form>
</template>
<script>
import PurchasingOrganizationDeletable from 'components/PurchasingOrganizationDeletable';
import {
reactive, getCurrentInstance, toRefs, watch,
} from 'vue';
export default {
name: 'BelongOrganization',
components: {
PurchasingOrganizationDeletable,
},
props: {
value: {
type: Array,
default: () => ([]),
},
index: {
type: Number,
required: true,
},
detailsData: {
type: Object,
default: () => ({}),
},
},
setup(props, { emit }) {
const { proxy } = getCurrentInstance();
const { cloneDeep } = proxy.$tools;
const init = reactive({
ruleForm: {
procurementOrgIds: props.value,
},
// 所有的组织树
organizationData: [],
// 深拷贝的所有的组织树
cloneDeepOrganizationData: [],
rules: {
procurementOrgIds: [
{
required: true, type: 'array', message: '请选择商品所属组织', trigger: 'change',
},
],
},
});
watch(() => props.value, (v) => {
init.ruleForm.procurementOrgIds = v;
});
// 获取组织列表
const getOrganizationList = async () => {
try {
const list = await proxy.$hPost('/getAll');
// 默认全部可选
init.organizationData = list.map((item) => ({ ...item, disabled: false }));
init.cloneDeepOrganizationData = cloneDeep(init.organizationData);
} catch (error) {
console.log(error);
}
};
getOrganizationList();
// 子组件调用validate进行校验
const validate = () => proxy.$refs.BelongOrganizationRef?.validate();
// 组织发生变化
const change = (v) => {
emit('input', v);
};
// 组织不能重复选择
const getPurchasingOrgDisList = (i) => {
const { detailsData: { ruleConfigModels } } = props;
const arr = cloneDeep(ruleConfigModels);
arr.splice(i, 1);
return arr.reduce((tol, cur) => [...tol, ...cur.procurementOrgIds], []);
};
return {
...toRefs(init),
getPurchasingOrgDisList,
validate,
change,
};
},
};
</script>
GoodsStratification 商品分层组件(孙组件)
<template>
<el-form
ref="GoodsStratificationRef"
:model="ruleForm"
:rules="rules"
style="padding-top: 10px;"
>
<el-form-item prop="productType">
<el-select
v-model="ruleForm.productType"
multiple
@change="changeStratification"
>
<el-option
v-for="mbl in getDisabledMblOptions(strategyConfigByCategories.reduce((pre,cur) => pre.concat(cur.productType),[]))"
:key="mbl.code"
:label="mbl.value"
:value="mbl.code"
:disabled="mbl.disabled"
>
<span>{{ mbl.value }}</span>
</el-option>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
import {
reactive, getCurrentInstance, watch,
} from 'vue';
export default {
name: 'GoodsStratification',
props: {
value: {
type: Array,
default: () => ([]),
},
// 当前的商品分层信息
strategyConfigByCategories: {
type: Array,
required: true,
},
},
setup(props, { emit }) {
const { proxy } = getCurrentInstance();
const init = reactive({
ruleForm: {
productType: props.value,
},
// 全部的商品分层
commodityStratificationList: [],
rules: {
productType: [
{
required: true, type: 'array', message: '请选择商品分层', trigger: 'change',
},
],
},
});
watch(() => props.value, (v) => {
init.ruleForm.productType = v;
});
const validate = () => proxy.$refs.GoodsStratificationRef?.validate();
// 获取商品分层列表
const getStratificationList = async () => {
init.commodityStratificationList = await proxy.$hGet('/basic');
};
getStratificationList();
// 商品分层发生变化
const changeStratification = (v) => {
emit('input', v);
};
// 商品分层同一个组织不能重复选择商品分层
const getDisabledMblOptions = (disArr = []) => init.commodityStratificationList.map((oriItem) => {
const disabled = disArr.includes(oriItem.code);
return {
...oriItem,
disabled,
};
});
return {
...init,
getDisabledMblOptions,
changeStratification,
validate,
};
},
};
</script>
<style lang="scss" scoped>
</style>
组织组件 PurchasingOrganizationDeletable (业务组件和上面功能关系不大)
<template>
<div>
<treeselect
v-model="innerValue"
:data="showData"
:multiple="multiple"
:only-leaf="onlyLeaf"
:disabled="disabled"
:render-node="renderNode"
:render-tag="renderTag"
:merge-value="mergeValue"
:editable-method="isSelectLevel"
:placeholder="placeholder"
v-bind="$attrs"
@change="onChange"
@focus="filterDisabledList"
/>
</div>
</template>
<script lang="jsx">
import { cloneDeep } from '@lsfe/tools';
export default {
name: 'PurchasingOrganizationSelect',
props: {
multiple: {
type: Boolean,
default: false,
},
// 是否只选择子节点
onlyLeaf: {
type: Boolean,
default: false,
},
// 数据会自动收敛,即如果父节点被选中,则返回的值中不会存在子节点的值。
mergeValue: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
value: {
type: [String, Number, Array],
default: '',
},
maxLevel: {
type: Number,
default: Infinity,
},
selectLevel: {
type: [Number, String, Array],
default: '',
},
placeholder: {
type: String,
default: '请选择所属组织',
},
// 从外部传的data
organizationData: {
type: Array,
default: () => ([]),
},
usePermission: {
type: Boolean,
default: false,
},
tagTender: {
type: Boolean,
default: false,
},
canTagDel: {
type: Boolean,
default: true,
},
remote: {
type: Boolean,
default: true,
},
disabledIds: {
type: Array,
default: () => ([]),
},
cloneDeepOrganizationData: {
type: Array,
default: () => ([]),
},
},
data() {
return {
innerValue: this.value,
organizationList: [],
cloneDeepOrganization: [],
};
},
computed: {
showData() {
return this.organizationList
.filter((item) => !this.maxLevel || item.level <= this.maxLevel);
},
},
watch: {
value: {
// 详情和编辑的数据回显
handler(newValue) {
this.innerValue = newValue;
},
},
organizationData: {
deep: true,
handler(newValue) {
this.organizationList = newValue;
this.filterDisabledList(newValue);
},
},
},
mounted() {
if (this.remote) {
this.getOrganizationList();
} else if (this.organizationData.length) {
this.filterDisabledList(this.organizationData);
}
},
methods: {
async getOrganizationList() {
try {
const url = this.usePermission ? '/shepherd/product/bizcenter/purchaserOrgTService/getDeptListByMisId' : '/shepherd/product/bizcenter/purchaserOrgTService/getAllDept';
const list = await this.$hPost(url);
this.cloneDeepOrganization = cloneDeep(list);
this.$emit('getChildOrganizationList', cloneDeep(list));
this.filterDisabledList(list);
} catch (error) {
this.$message.error(error.msg || error.message || '获取所属组织列表失败');
} finally {
this.$emit('loaded', this.organizationList);
}
},
onChange(val, item) {
this.$emit('input', val);
this.$emit('change', val, item);
},
deleteNode(treeSelect, item) {
const tree = treeSelect.$refs.treeWrapper.$refs.tree;
const targetNode = tree.getNode(item);
// 被从组织中删除的某个组织
if (!targetNode) {
const i = this.innerValue.indexOf(item.id);
this.innerValue.splice(i, 1);
this.$emit('input', this.innerValue);
}
targetNode?.setChecked(false, true);
this.$nextTick(() => {
const store = tree.store;
tree.$emit('check', targetNode?.data, {
checkedNodes: store.getCheckedNodes(),
checkedKeys: store.getCheckedKeys(),
halfCheckedNodes: store.getHalfCheckedNodes(),
halfCheckedKeys: store.getHalfCheckedKeys(),
});
});
},
renderTag(h, data, treeSelect) {
const { canTagDel, tagTender } = this;
const arr = data.filter((item) => !item.children);
if (!tagTender) {
arr.slice(0, 1);
}
const tagList = arr.map((item) => (
<el-tag
size="small"
key={item.id}
disable-transitions={true}
closable={canTagDel}
onClose={() => { this.deleteNode(treeSelect, item); }}
>
{item.label}
</el-tag>
));
if (!tagTender) {
if (data.length > 1) {
tagList.push((
<el-tag size="small">+{data.length - 1}</el-tag>
));
}
}
return [tagList];
},
filterDisabledList(data = this.organizationList) {
const { disabledIds } = this;
if (disabledIds.length === 0) {
/*
这里用的是从父组件请求的组织列表, 传过来的有组织列表和深拷贝的组织列表,
如果disabledIds为空,使用深拷贝的组织列表,ss
如果不是使用的父组件传过来的数据,就用在本组织中深拷贝的数据,对初始化组织赋值
*/
if (!this.remote) {
this.organizationList = this.cloneDeepOrganizationData;
} else {
this.organizationList = this.cloneDeepOrganization;
}
} else {
this.organizationList = data.map((item) => ({ ...item, disabled: disabledIds.includes(item.id) }));
}
},
// eslint-disable-next-line no-unused-vars
renderNode(h, { node, data }) {
return (
<div>
<span>{data.id} -- {data.label}</span>
</div>
);
},
// eslint-disable-next-line consistent-return
isSelectLevel(node) {
const level = Number(node.level);
if (!this.selectLevel) {
// 没有selectLevel 直接通过
return true;
}
if (Array.isArray(this.selectLevel)) {
// selectLevel是数组
// eslint-disable-next-line no-bitwise
return ~this.selectLevel.indexOf(level);
}
if (typeof this.selectLevel === 'number' || typeof this.selectLevel === 'string') {
// selectLevel是数字 或 字符串
return Number(this.selectLevel) === level;
}
},
},
};
</script>
<style lang="scss" scoped>
::v-deep .el-select__input{
width: 0px;
}
::v-deep .tag-wrapper .el-input__inner {
overflow-y: auto;
max-height: 100px;
}
::v-deep .tag-wrapper .el-input__inner .tag-wrapper__inner {
overflow-y: auto;
max-height: 100px;
}
</style>