FatLogicTree 逻辑树 (试验)
逻辑树
逻辑树是一个树形容器,可以用来表示一些逻辑关系。
基本概念:
- 节点:树形结构中的一个节点。 对应到逻辑语句中, 比如
1
、1+1
、a + 1 > 2
,这些表达式都属于节点 - 分组:将多个节点组合起来,约束一些逻辑关系。 比如
(a > 1) && (b < 0)
就是一个分组,分组包含(a > 1)
和(b < 0)
两个节点 - 逻辑关系: 当前仅支持 AND、OR 两种关系。当然这个概念是可以互换的,比如
并行
和互斥
因此逻辑树和普通数在结构上是有一些约束的:
- 节点只能放在分组中
- 节点不能作为分组的上级
- 分组可以包含分组
示例
自定义树结构
FatLogicTree 本身并不耦合约束树的结构,可以声明树的结构、逻辑的标识等等
查看代码
vue
<template>
<FatLogicTree v-model="data" :tree-struct="customTreeStruct" and-text="互斥" or-text="并行">
<!-- 自定义节点渲染 -->
<template #node="scope">
<div class="node">
<div>VALUE: {{ scope.current.value }}</div>
<div>INDEX: {{ scope.index }}</div>
<div>PATH: {{ scope.path }}</div>
<div>DEPTH: {{ scope.depth }}</div>
</div>
</template>
</FatLogicTree>
</template>
<script setup lang="tsx">
import { ref } from 'vue';
import { FatLogicTree, LogicType, FatLogicTreeStruct } from '@wakeadmin/components';
type Group = {
uuid: string | number;
category: 'group';
logic?: '并行' | '互斥';
// eslint-disable-next-line no-use-before-define
list: Item[];
};
type Node = {
category: 'node';
uuid: string | number;
value: string;
};
type Item = Group | Node;
// 自定义树结构
const customTreeStruct: FatLogicTreeStruct<Item> = {
// 子节点获取
children: 'list',
// 唯一标识符获取
id: 'uuid',
// 判断是否为逻辑分组
isGroup: i => i.category === 'group',
// 获取逻辑类型
getLogicType: i => ((i as Group).logic === '并行' ? LogicType.OR : LogicType.AND),
// 写入逻辑类型
setLogicType: (i, type) => {
if (i.category === 'group') {
return {
...i,
logic: type === LogicType.OR ? '并行' : '互斥',
};
}
return i;
},
};
const data = ref<Item>({
uuid: 'root',
category: 'group',
logic: '互斥',
list: [
{
uuid: 1,
category: 'group',
logic: '并行',
list: [
{
uuid: 31,
category: 'node',
value: 'Item 1',
},
{
uuid: 32,
category: 'node',
value: 'Item 2',
},
],
},
{
uuid: 2,
category: 'group',
logic: '并行',
list: [
{
uuid: 31,
category: 'node',
value: 'Item 1',
},
{
uuid: 32,
category: 'node',
value: 'Item 2',
},
],
},
],
});
</script>
<style scoped>
.node {
background-color: white;
padding: 4px;
border-radius: 4px;
margin-bottom: 8px;
border-radius: 4px;
}
</style>
节点操作
查看代码
tsx
import { defineComponent, ref } from 'vue';
import { FatLogicTree, LogicType } from '@wakeadmin/components';
import { ElButton } from 'element-plus';
import s from './LogicTreeOperation.module.scss';
interface Group {
id: string | number;
category: 'group';
type?: LogicType;
children: Item[];
}
interface Node {
category: 'node';
id: string | number;
value: string;
}
type Item = Group | Node;
export default defineComponent({
setup() {
const data = ref<Item>({
category: 'group',
id: 'root',
type: LogicType.OR,
children: [],
});
return () => {
// 支持泛型
return (
<div>
<FatLogicTree<Item>
modelValue={data.value}
onUpdate:modelValue={v => (data.value = v)}
// 自定义分组渲染
// 用于复杂的样式定义
renderGroup={scope => {
const current = scope.current as Group;
if (scope.depth === 0) {
// 根节点
return (
<div class={s.root}>
{current.children.length === 0 && <div class={s.empty}>这里啥都没有</div>}
{scope.vdom}
<ElButton
class={s.button}
onClick={() => {
scope.append({
category: 'group',
id: 'group-' + current.children.length,
type: LogicType.AND,
children: [
{
category: 'node',
id: 'node',
value: 'Example',
},
],
});
}}
>
添加分组
</ElButton>
</div>
);
}
return (
<div class={s.group}>
<h3>分组: {scope.index + 1}</h3>
<div class={s.subGroup}>{scope.vdom}</div>
</div>
);
}}
// 自定义节点渲染
renderNode={scope => {
const current = scope.current as Node;
// 插入子节点
const handleAddChild = () => {
const id = Date.now();
scope.insertAfter({
category: 'node',
id,
value: `Example ${id}`,
});
};
return (
<div class={s.node}>
{current.value}-{scope.path} {scope.index === 0 && <ElButton onClick={handleAddChild}>添加</ElButton>}
<ElButton onClick={scope.remove}>删除</ElButton>
</div>
);
}}
></FatLogicTree>
<pre>
<code>{JSON.stringify(data.value, undefined, 2)}</code>
</pre>
</div>
);
};
},
});
scss
.root {
border: 1px solid rgb(180, 180, 180);
padding: 10px;
border-radius: 10px;
}
.group {
background-color: rgb(135, 208, 135);
border-radius: 4px;
padding: 5px;
margin-bottom: 10px;
}
.subGroup {
margin-top: 1em;
}
.node {
background-color: white;
padding: 4px;
border-radius: 4px;
margin-bottom: 4px;
}
.button {
margin-top: 5px;
}
和 FatForm 配合使用
查看代码
tsx
import { defineComponent, ref } from 'vue';
import { defineFatForm, FatFormItem, FatLogicTree, LogicType } from '@wakeadmin/components';
import { ElButton } from 'element-plus';
import s from './LogicTreeOperation.module.scss';
interface Group {
id: string | number;
category: 'group';
type?: LogicType;
children: Item[];
}
interface Node {
category: 'node';
id: string | number;
value: string;
}
type Item = Group | Node;
export default defineFatForm(({ group, consumer }) => {
return () => ({
submit: async value => {
console.log('保存', value);
},
onValidateFailed: errors => {
console.log('验证失败', errors);
},
children: [
group({
label: '逻辑分组',
prop: 'logic',
initialValue: {
category: 'group',
id: 'root',
type: LogicType.OR,
children: [],
},
rules: {
validator(rule, value, callback) {
if (value == null || !(value as Group).children?.length) {
callback(new Error('请至少添加一个分组'));
} else {
callback();
}
},
},
children: [
consumer(formScope => {
return (
<FatLogicTree<Item>
basePath="logic"
modelValue={formScope.getFieldValue('logic')}
onUpdate:modelValue={v => formScope.setFieldValue('logic', v)}
// 自定义分组渲染
// 用于复杂的样式定义
renderGroup={scope => {
const current = scope.current as Group;
if (scope.depth === 0) {
// 根节点
return (
<div class={s.root}>
{current.children.length === 0 && <div class={s.empty}>这里啥都没有</div>}
{scope.vdom}
<ElButton
class={s.button}
onClick={() => {
scope.append({
category: 'group',
id: 'group-' + current.children.length,
type: LogicType.AND,
children: [
{
category: 'node',
id: 'node',
value: 'Example',
},
],
});
}}
>
添加分组
</ElButton>
</div>
);
}
return (
<div class={s.group}>
<h3>分组: {scope.index + 1}</h3>
<div class={s.subGroup}>{scope.vdom}</div>
</div>
);
}}
// 自定义节点渲染
renderNode={scope => {
const current = scope.current as Node;
// 插入子节点
const handleAddChild = () => {
const id = Date.now();
scope.insertAfter({
category: 'node',
id,
value: `Example ${id}`,
});
};
return (
<div class={s.node}>
<FatFormItem
prop={scope.path + '.value'}
rules={{ required: true, message: '不能为空' }}
></FatFormItem>
<div>
{scope.index === 0 && <ElButton onClick={handleAddChild}>添加</ElButton>}
<ElButton onClick={scope.remove}>删除</ElButton>
</div>
</div>
);
}}
></FatLogicTree>
);
}),
],
}),
group({
label: '数据',
children: [
consumer(scope => {
return (
<pre>
<code>{JSON.stringify(scope.values, undefined, 2)}</code>
</pre>
);
}),
],
}),
],
});
});
多层嵌套
查看代码
vue
<template>
<FatLogicTree v-model="data">
<!-- 自定义节点渲染 -->
<template #node="scope">
<div class="node">
<div>VALUE: {{ scope.current.value }}</div>
<div>INDEX: {{ scope.index }}</div>
<div>PATH: {{ scope.path }}</div>
<div>DEPTH: {{ scope.depth }}</div>
</div>
</template>
</FatLogicTree>
</template>
<script setup lang="tsx">
import { ref } from 'vue';
import { FatLogicTree, LogicType } from '@wakeadmin/components';
type Item =
| {
id: string | number;
category: 'group';
type?: LogicType;
children: Item[];
}
| {
category: 'node';
id: string | number;
value: string;
};
const data = ref<Item>({
id: 'root',
category: 'group',
type: LogicType.OR,
children: [
{
category: 'node',
id: 1,
value: 'Item 1',
},
{
id: 2,
category: 'node',
value: 'Item 2',
},
{
id: 3,
category: 'group',
children: [
{
id: 31,
category: 'node',
value: 'Item 3-1',
},
{
id: 32,
category: 'node',
value: 'Item 3-2',
},
],
},
{
id: 4,
category: 'group',
children: [
{
id: 41,
value: 'Item 4-1',
category: 'node',
},
{
id: 42,
value: 'Item 4-2',
category: 'node',
},
],
},
{
id: 5,
category: 'group',
type: LogicType.OR,
children: [],
},
{
id: 6,
type: LogicType.OR,
category: 'group',
children: [
{
id: 61,
category: 'group',
children: [
{
id: 611,
category: 'group',
children: [
{
id: 6111,
value: 'Item 6-1-1-1',
category: 'node',
},
{
id: 6112,
category: 'group',
type: LogicType.OR,
children: [
{
id: 61121,
category: 'group',
type: LogicType.OR,
children: [
{
id: 611211,
value: 'Item 6-1-1-2-1-1',
category: 'node',
},
{
id: 611212,
value: 'Item 6-1-1-2-1-2',
category: 'node',
},
],
},
],
},
],
},
{
id: 612,
value: 'Item 6-1-2',
category: 'node',
},
],
},
{
id: 62,
value: 'Item 6-2',
category: 'node',
},
],
},
],
});
</script>
<style scoped>
.node {
background-color: white;
padding: 4px;
border-radius: 4px;
margin-bottom: 8px;
border-radius: 4px;
}
</style>
可拖拽
配合 sortablejs 实现