Compare commits

...

1 Commits

Author SHA1 Message Date
973ba159b4 flowChart初期実装 2023-09-08 13:31:41 +09:00
6 changed files with 507 additions and 19 deletions

View File

@@ -0,0 +1,107 @@
<template>
<div class="row justify-center" :style="{marginLeft:node.inputPoint!==''?'240px':''}">
<div class="row">
<q-card class="action-node" :class="{'root-node':node.isRoot,'text-white':node.isRoot}" :square="false">
<q-card-section>
<div class="text-h6">{{ node.title }}</div>
<div class="text-subtitle2">{{ node.subTitle }}</div>
</q-card-section>
<template v-if="hasBranch">
<q-separator />
<q-card-actions align="around">
<q-btn flat v-for="(point,index) in node.outputPoints" :key="index">
{{ point }}
</q-btn>
</q-card-actions>
</template>
</q-card>
</div>
</div>
<template v-if="hasBranch">
<div class="row justify-center" :style="{marginLeft:node.inputPoint!==''?'240px':''}">
<div v-for="(point,index) in node.outputPoints" :key="index">
<node-line :action-node="node" :mode="getMode(point)" @addNode="addNode" :input-point="point" ></node-line>
</div>
</div>
</template>
<template v-if="!hasBranch">
<div class="row justify-center" :style="{marginLeft:node.inputPoint!==''?'240px':''}">
<node-line :action-node="node" :mode="getMode('')" @addNode="addNode" input-point=""></node-line>
</div>
</template>
</template>
<script lang="ts">
import {defineComponent,computed} from 'vue';
import {IActionNode } from '../../types/ActionTypes';
import NodeLine,{Direction} from '../main/NodeLine.vue';
export default defineComponent({
name: 'NodeItem',
components: {
NodeLine
},
props: {
actionNode:{
type:Object as ()=>IActionNode,
required:true
}
},
emits: [
'addNode'
],
setup(props,context){
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const getMode =(point:string)=>{
if(point==='' || props.actionNode.outputPoints.length===0){
return Direction.Default;
}
if(point===props.actionNode.outputPoints[0]){
if(props.actionNode.nextNodes.get(point)){
return Direction.Left;
}else{
return Direction.LeftNotNext;
}
}else{
if(props.actionNode.nextNodes.get(point)){
return Direction.Right;
}else{
return Direction.RightNotNext;
}
}
}
const addNode=(point:string)=>{
context.emit('addNode',props.actionNode,point);
}
return {
node:props.actionNode,
hasBranch,
isRoot:props.actionNode.isRoot,
getMode,
addNode
}
}
});
</script>
<style lang="scss">
.action-node{
min-width:250px !important;
}
.line{
height:20px;
}
.line:after{
content:'';
background-color:$blue-7;
display:block;
width:3px;
}
.add-icon{
font-size: 2em;
color:$blue-7;
}
.root-node{
background-color:$blue-7;
border-radius: 20px;
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<div>
<svg class="node-line">
<polyline :points="points.linePoints" class="line" ></polyline>
<text class="add-icon" @click="addNode(node)" :x="points.iconPoint.x" :y="points.iconPoint.y" font-family="Arial" font-size="25" text-anchor="middle" dy=".3em" style="cursor: pointer;" >
</text>
</svg>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, computed, PropType } from 'vue';
import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes';
export enum Direction {
Default = "None",
Left = "LEFT",
Right = "RIGHT",
LeftNotNext = "LEFTNOTNEXT",
RightNotNext = "RIGHTNOTNEXT",
}
export default defineComponent({
name: 'NodeLine',
props: {
actionNode: {
type: Object as PropType<IActionNode>,
required: true
},
mode: {
type: String as PropType<Direction>,
required: true
},
inputPoint:{
type:String
}
},
emits: ['addNode'],
setup(props,context) {
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const points = computed(() => {
switch (props.mode) {
case Direction.Left:
return {
linePoints: '180, 0, 180, 40, 120, 40, 120, 60',
iconPoint: { x: 180, y: 20 }
};
case Direction.Right:
return {
linePoints: '60, 0, 60, 40, 120, 40, 120, 60',
iconPoint: { x: 60, y: 20 }
};
case Direction.LeftNotNext:
return {
linePoints: '180, 0, 180, 40',
iconPoint: { x: 180, y: 20 }
};
case Direction.RightNotNext:
return {
linePoints: '60, 0, 60, 40',
iconPoint: { x: 60, y: 30 }
};
default:
return {
linePoints: '120, 0, 120, 60',
iconPoint: { x: 120, y: 30 }
};
}
});
const addNode=(prveNode:IActionNode)=>{
context.emit('addNode',props.inputPoint);
}
return {
node: props.actionNode,
hasBranch,
points,
addNode
}
}
});
</script>
<style lang="scss">
.node-line {
height: 60px;
width: 240px;
}
.line {
stroke: $blue-7;
fill: none;
stroke-width: 2;
}
.add-icon {
stroke: $blue-7;
fill: $blue-7;
font-family: Arial;
pointer-events: all;
}
</style>

View File

@@ -32,4 +32,3 @@ export interface AppInfo {
creator?:User; creator?:User;
modifier?:User; modifier?:User;
} }

View File

@@ -0,0 +1,43 @@
<template>
<q-page>
<div class="flowchart">
<node-item v-for="(node,index) in refFlow.actionNodes" :key="index" :actionNode="node" @addNode="addNode"></node-item>
</div>
</q-page>
<show-dialog v-model:visible="showAddAction" name="アクション" @close="closeDg">
<action-select ref="appDg" name="アクション" type="single"></action-select>
</show-dialog>
</template>
<script setup lang="ts">
import {ref} from 'vue';
import {IActionNode, ActionNode, IActionFlow, ActionFlow,RootAction } from 'src/types/ActionTypes';
import NodeItem from 'src/components/main/NodeItem.vue';
import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue';
const rootAction:RootAction =new RootAction("app.record.create.submit","レコード追加画面","保存するとき");
const actionFlow: ActionFlow = new ActionFlow(rootAction);
actionFlow.addNode(new ActionNode('自動採番','文書番号を自動採番する',''));
actionFlow.addNode(new ActionNode('入力データ取得','電話番号を取得する',''));
const branchNode = actionFlow.addNode(new ActionNode('条件分岐','電話番号入力形式チャック','',['はい','いいえ'] ));
actionFlow.addNode(new ActionNode('エラー表示','エラー表示して保存しない',''),branchNode,'いいえ' );
// ref関数を使ってtemplateとバインド
const refFlow = ref(actionFlow);
const showAddAction=ref(false);
const addActionNode=(action:IActionNode)=>{
refFlow.value.actionNodes.push(action);
}
const addNode=(node:IActionNode,inputPoint:string)=>{
showAddAction.value=true;
}
</script>
<style lang="scss">
.flowchart{
padding-top: 10px;
}
</style>

View File

@@ -4,25 +4,14 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: () => import('layouts/MainLayout.vue'), component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/IndexPage.vue') }], children: [
{ path: '', component: () => import('pages/IndexPage.vue') },
{ path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') },
{ path: 'test', component: () => import('pages/testQursar.vue') },
{ path: 'flow', component: () => import('pages/testFlow.vue') },
{ path: 'flowchart', component: () => import('pages/FlowChartTest.vue') }
],
}, },
{
path: '/ruleEditor/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/RuleEditor.vue') }],
},
{
path: '/test/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/testQursar.vue') }],
},
,
{
path: '/flow/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/testFlow.vue') }],
},
// Always leave this as last one, // Always leave this as last one,
// but you can also remove it // but you can also remove it
{ {

View File

@@ -0,0 +1,251 @@
/**
* アクションのプロパティ定義
*/
interface IActionProperty {
component: string;
props: {
//プロパティ名
name: string;
//プロパティ表示名
displayName:string;
placeholder: string;
//プロパティ設定値
modelValue: any;
};
}
/**
* アクションタイプ定義
*/
export interface IActionNode{
id:string;
//アクション名
name:string;
title:string;
subTitle:string;
inputPoint:string;
//出力ポイント(条件分岐以外未使用)
outputPoints:Array<string>;
//ルートアクションKintone event
isRoot:boolean;
//アクションのプロパティ定義
actionProps:Array<IActionProperty>;
//アクションのプロパティ設定値抽出
ActionValue:object
prevNode?: IActionNode;
nextNodes:Map<string,IActionNode>;
}
/**
* アクションフローの定義
*/
export interface IActionFlow {
actionNodes:Array<IActionNode>
}
/**
* アクションのプロパティ定義に基づいたクラス
*/
class ActionProperty implements IActionProperty {
component: string;
props: {
// プロパティ名
name: string;
// プロパティ表示名
displayName: string;
placeholder: string;
// プロパティ設定値
modelValue: any;
};
static defaultProperty():IActionProperty{
return new ActionProperty("InputText","displayName","表示名","表示を入力してください","");
};
constructor(
component: string,
name: string,
displayName: string,
placeholder: string,
modelValue: any
) {
this.component = component;
this.props = {
name: name,
displayName: displayName,
placeholder: placeholder,
modelValue: modelValue
};
}
}
/**
* IActionNodeの実装、RootActionNode以外のアクション定義
*/
export class ActionNode implements IActionNode {
id:string;
name: string;
title:string;
get subTitle():string{
return this.name;
};
inputPoint:string;
//出力ポイント(条件分岐以外未使用)
outputPoints:Array<string>;
actionProps: Array<IActionProperty>;
get isRoot(): boolean{
return false;
};
get ActionValue():object{
const propValue:any={};
this.actionProps.forEach((value)=>{
propValue[value.props.name]=value.props.modelValue
});
return propValue;
};
prevNode?: IActionNode;
nextNodes:Map<string,IActionNode>;
constructor(
name: string,
title:string,
inputPoint:string,
outputPoint: Array<string> = [],
actionProps: Array<IActionProperty> =[ActionProperty.defaultProperty()]
) {
this.id='';
this.name = name;
this.title=title;
this.inputPoint=inputPoint;
this.outputPoints = outputPoint;
const defProp =ActionProperty.defaultProperty();
defProp.props.displayName=title;
this.actionProps =actionProps;
const prop = this.actionProps.find((prop)=>prop.props.name===defProp.props.name);
if(prop===undefined){
this.actionProps.unshift(defProp);
}
this.nextNodes=new Map();
}
}
/**
* ルートアクション定義
*/
export class RootAction implements IActionNode {
id:string;
name: string;
title:string;
subTitle:string;
inputPoint:string;
//出力ポイント(条件分岐以外未使用)
outputPoints:Array<string>;
isRoot: boolean;
actionProps: Array<IActionProperty>;
ActionValue:object;
prevNode?: IActionNode=undefined;
nextNodes:Map<string,IActionNode>;
constructor(
name: string,
title:string,
subTitle:string,
) {
this.id='';
this.name = name;
this.title=title;
this.subTitle=subTitle;
this.inputPoint='';
this.outputPoints = [];
this.isRoot = true;
this.actionProps=[];
this.ActionValue={};
this.nextNodes=new Map();
}
}
/**
* アクションフローの定義
*/
export class ActionFlow implements IActionFlow {
nextId:number;
actionNodes:Array<IActionNode>;
constructor(actionNodes:Array<IActionNode>|RootAction){
this.nextId=0;
if(actionNodes instanceof Array){
this.actionNodes=actionNodes;
}else{
this.actionNodes=[actionNodes];
}
}
/**
* ノードを追加する
* 1.ID採番する
* 2.前のノードを関連付け
* @param newNode 新規追加するノード
* @param prevNode 前のノード
* @param inputPoint 入力ポイント
* @returns 追加されたノード
*/
addNode(
newNode:IActionNode,
prevNode?:IActionNode,
inputPoint?:string):IActionNode
{
newNode.id = this.generateId();
if(inputPoint!==undefined){
newNode.inputPoint=inputPoint;
}
if(prevNode!==undefined){
newNode.prevNode=prevNode;
const nextNodes = prevNode.nextNodes;
if(nextNodes!==undefined && nextNodes.size>0){
const nextNode=inputPoint!==undefined?nextNodes.get(inputPoint):nextNodes.get('');
if(nextNode ){
nextNode.prevNode=newNode;
if(newNode.outputPoints.length>0){
if(!newNode.outputPoints.some((point)=>point==nextNode.inputPoint)){
nextNode.inputPoint=newNode.outputPoints[0];
}
}else{
nextNode.inputPoint='';
}
newNode.nextNodes.set(nextNode.inputPoint,nextNode);
}
}
prevNode.nextNodes.set(inputPoint?inputPoint:'',newNode);
}else{
prevNode=this.actionNodes[this.actionNodes.length-1];
prevNode.nextNodes.set(inputPoint?inputPoint:'',newNode);
newNode.prevNode=prevNode;
}
this.actionNodes.push(newNode);
return newNode;
}
/**
* ノードを削除する
* @param delNode
*/
removeNode(delNode:IActionNode){
const prevNode = delNode;
const nextNode = this.findNextNode(delNode);
if(nextNode!==undefined){
nextNode.prevNode=prevNode;
nextNode.inputPoint=delNode.inputPoint;
}
}
/**
* 次のノードを探す
* @param delNode
*/
findNextNode(targetNode: IActionNode):ActionNode|undefined {
return this.actionNodes.find((node)=>node.prevNode===targetNode);
}
generateId():string{
const no = this.nextId++;
return no.toString().padStart(10,'0');
}
}