├── src ├── assets │ ├── .gitkeep │ ├── images │ │ ├── demo.png │ │ ├── flow_end.png │ │ ├── flow_arrow.png │ │ ├── flow_select.png │ │ ├── flow_start.png │ │ ├── flow_activity.png │ │ ├── FlowDesignerIcon.png │ │ ├── flow_autoActivity.png │ │ ├── flow_end_selected.png │ │ ├── flow_start_selected.png │ │ ├── flow_multiHumanActivity.png │ │ └── flow_singleHumanActivity.png │ ├── styles │ │ └── notie.min.css │ └── js │ │ └── notie@3.9.5.js ├── app │ ├── app.component.html │ ├── app-store.ts │ ├── models │ │ ├── authentication-data-def.ts │ │ ├── master-data-def.ts │ │ └── flow-data-def.ts │ ├── utilites │ │ ├── gen-guid.ts │ │ └── format-json.ts │ ├── actions │ │ ├── ActivityToolActions.ts │ │ ├── index.ts │ │ ├── ActivityNodeDataActions.ts │ │ ├── FlowDataActions.ts │ │ └── ActivityConnectionDataActions.ts │ ├── app.component.ts │ ├── containers │ │ ├── flow-frame.component.ts │ │ ├── tab.component.ts │ │ ├── tabset.component.ts │ │ ├── home.component.ts │ │ ├── main-menu.component.ts │ │ ├── slide-tool-bar.component.ts │ │ ├── flow-template-menu.component.ts │ │ └── property-panel.component.ts │ ├── services │ │ ├── property-control.service.ts │ │ ├── app-config.service.ts │ │ ├── logon-backend.service.ts │ │ ├── slide-tool-bar.config.ts │ │ ├── flow-template-backend.service.ts │ │ ├── new-flow-template-skeleton.service.ts │ │ ├── webtookit-base64.service.ts │ │ ├── flow-template-meta.service.ts │ │ ├── org-data.service.ts │ │ └── mock-data.ts │ ├── components │ │ ├── simple-modal-dialog.component.scss │ │ ├── activity-tool.component.ts │ │ ├── simple-modal-dialog.component.ts │ │ ├── dynamic-flow-property-control.component.ts │ │ ├── role-user-selector.component.ts │ │ ├── condition-rule-editor.component.ts │ │ ├── condition-rule-manager.component.ts │ │ ├── role-user-dialog.component.ts │ │ └── flow-chart-parent.component.ts │ ├── reducers │ │ ├── ActivityToolsReducer.ts │ │ ├── index.ts │ │ ├── FlowDataReducer.ts │ │ ├── ActivityNodeDataReducer.ts │ │ └── ActivityConnectionDataReducer.ts │ └── app.module.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── main.ts ├── tsconfig.json ├── index.html ├── polyfills.ts ├── test.ts └── styles.scss ├── screenshot.gif ├── .vscode └── settings.json ├── debug.log ├── e2e ├── app.po.ts ├── app.e2e-spec.ts └── tsconfig.json ├── .editorconfig ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── README.md ├── angular-cli.json ├── package.json └── tslint.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/screenshot.gif -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/demo.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.check.workspaceVersion": false, 3 | "vsicons.presets.angular": true 4 | } -------------------------------------------------------------------------------- /src/assets/images/flow_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_end.png -------------------------------------------------------------------------------- /src/assets/images/flow_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_arrow.png -------------------------------------------------------------------------------- /src/assets/images/flow_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_select.png -------------------------------------------------------------------------------- /src/assets/images/flow_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_start.png -------------------------------------------------------------------------------- /src/app/app-store.ts: -------------------------------------------------------------------------------- 1 | import { OpaqueToken } from '@angular/core'; 2 | 3 | export const AppStore = new OpaqueToken('App.store'); 4 | -------------------------------------------------------------------------------- /src/assets/images/flow_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_activity.png -------------------------------------------------------------------------------- /src/assets/images/FlowDesignerIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/FlowDesignerIcon.png -------------------------------------------------------------------------------- /src/assets/images/flow_autoActivity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_autoActivity.png -------------------------------------------------------------------------------- /src/assets/images/flow_end_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_end_selected.png -------------------------------------------------------------------------------- /src/assets/images/flow_start_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_start_selected.png -------------------------------------------------------------------------------- /src/assets/images/flow_multiHumanActivity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_multiHumanActivity.png -------------------------------------------------------------------------------- /src/assets/images/flow_singleHumanActivity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibamo/99-flow-designer/HEAD/src/assets/images/flow_singleHumanActivity.png -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [0114/221257:ERROR:tcp_listen_socket.cc(76)] Could not bind socket to 127.0.0.1:6004 2 | [0114/221257:ERROR:node_debugger.cc(86)] Cannot start debugger server 3 | -------------------------------------------------------------------------------- /src/app/models/authentication-data-def.ts: -------------------------------------------------------------------------------- 1 | export interface LogonRequestInfo { 2 | userAccount: string; 3 | password: string; 4 | authenticationKey?: string; 5 | } 6 | 7 | export interface UserInfo { 8 | userAccount: string; 9 | userName: string; 10 | } -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class FlowDesignerPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getAppTitleText() { 9 | return element(by.id('app-title-text')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/utilites/gen-guid.ts: -------------------------------------------------------------------------------- 1 | export function genGuid() { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 3 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 4 | return v.toString(16); 5 | }); 6 | }; 7 | 8 | /* 9 | 10 | 11 | 12 | 13 | 14 | */ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './app/app.module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { FlowDesignerPage } from './app.po'; 2 | 3 | describe('Flow-Template-Designer App', function() { 4 | let page: FlowDesignerPage; 5 | 6 | beforeEach(() => { 7 | page = new FlowDesignerPage(); 8 | }); 9 | 10 | it('should display app title in main menu', () => { 11 | page.navigateTo(); 12 | expect(page.getAppTitleText()).toEqual('流程模板定义器/Flow Template Designer'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/actions/ActivityToolActions.ts: -------------------------------------------------------------------------------- 1 | import {Action, ActionCreator} from 'redux'; 2 | 3 | //import {ActivityTool} from '../models/flow-data-def'; 4 | 5 | export const SET_CURRENT_TOOL = '[TOOL] Set Current'; 6 | export interface SetCurrentToolAction extends Action{ 7 | tool: string; 8 | } 9 | export const setCurrentTool: ActionCreator = 10 | (activityType) => ({type: SET_CURRENT_TOOL, tool: activityType}); -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { 4 | createStore, 5 | Store, 6 | compose, 7 | StoreEnhancer 8 | } from 'redux'; 9 | import { AppStore } from './app-store'; 10 | import { 11 | AppState, 12 | default as reducer 13 | } from './reducers'; 14 | 15 | 16 | @Component({ 17 | selector: 'app-root', 18 | templateUrl: './app.component.html' 19 | }) 20 | export class AppComponent { 21 | } 22 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": ["es6", "dom"], 8 | "mapRoot": "./", 9 | "module": "es6", 10 | "moduleResolution": "node", 11 | "outDir": "../dist/out-tsc", 12 | "sourceMap": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "../node_modules/@types" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/containers/flow-frame.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-flow-frame', 5 | template: ` 6 | 7 | 8 | 9 | ` 10 | }) 11 | export class FlowFrameComponent implements OnInit { 12 | 13 | constructor() { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/containers/tab.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tab', 5 | template: ` 6 |
8 | 9 | 10 | 11 |
12 | ` 13 | }) 14 | export class TabComponent implements OnInit { 15 | @Input() title: string; 16 | active: boolean = false; 17 | name: string; 18 | 19 | constructor() { } 20 | 21 | ngOnInit() { 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FlowDesigner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as ActivityToolActions from './ActivityToolActions'; 2 | import * as FlowDataActions from './FlowDataActions'; 3 | import * as ActivityNodeDataActions from './ActivityNodeDataActions' 4 | import * as ActivityConnectionDataActions from './ActivityConnectionDataActions' 5 | 6 | export { 7 | ActivityToolActions, 8 | FlowDataActions, 9 | ActivityNodeDataActions, 10 | ActivityConnectionDataActions 11 | }; 12 | 13 | export default [ 14 | ActivityToolActions, 15 | FlowDataActions, 16 | ActivityNodeDataActions, 17 | ActivityConnectionDataActions 18 | ]; -------------------------------------------------------------------------------- /src/app/services/property-control.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FormControl, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { FlowPropertyBase } from '../models/flow-data-def'; 5 | 6 | @Injectable() 7 | export class PropertyControlService { 8 | constructor() { } 9 | 10 | toFormGroup(properties: FlowPropertyBase[] ) { 11 | let group: any = {}; 12 | 13 | properties.forEach(p => { 14 | group[p.guid] = p.required ? 15 | new FormControl(p.value || '', Validators.required) : 16 | new FormControl(p.value || ''); 17 | }); 18 | 19 | return new FormGroup(group); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular 2 and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | #yarn 43 | yarn.lock 44 | 45 | #internal 46 | /internal -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/services/app-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class AppConfigService { 5 | private _serverAddress = 'http://' + window.location.hostname + ':8888/' ; // 'http://localhost:8888/'; 6 | private _logCommunication = false; 7 | 8 | // 不显示Org 9 | private _displayOrg = false; 10 | // 不显示OrgSchema 11 | private _displayOrgSchema = false; 12 | // 不显示BizEntitySchema 13 | private _displayBizEntitySchema = false; 14 | 15 | constructor() { } 16 | 17 | get serverAddress(): string { 18 | return this._serverAddress; 19 | } 20 | 21 | get logCommunication(): boolean { 22 | return this._logCommunication; 23 | } 24 | 25 | get displayOrgSchema(): boolean { 26 | return this._displayOrgSchema; 27 | } 28 | 29 | get displayBizEntitySchema(): boolean { 30 | return this._displayBizEntitySchema; 31 | } 32 | 33 | get displayOrg(): boolean { 34 | return this._displayOrg; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/containers/tabset.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | AfterContentInit, 5 | QueryList, 6 | ContentChildren 7 | } from '@angular/core'; 8 | 9 | import { TabComponent } from './tab.component' 10 | 11 | @Component({ 12 | selector: 'app-tabset', 13 | template: ` 14 | 25 | 26 | ` 27 | }) 28 | export class TabsetComponent implements AfterContentInit { 29 | @ContentChildren(TabComponent) tabs: QueryList; 30 | 31 | constructor() { } 32 | 33 | ngAfterContentInit() { 34 | this.tabs.toArray()[0].active = true; 35 | } 36 | 37 | setActive(tab: TabComponent) { 38 | this.tabs.toArray().forEach((t) => t.active = false); 39 | tab.active = true; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/containers/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | template: ` 6 |
7 | 12 | 15 |
16 | 左侧工具栏 17 | 中间绘图区域 18 | 右侧流程模板属性区 19 |
20 |
21 |

22 | © 2017 - OPAS 在线流程执行系统 V2.0 23 |

24 |
25 |
26 | ` 27 | }) 28 | export class HomeComponent implements OnInit { 29 | 30 | constructor() { } 31 | 32 | ngOnInit() { 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/components/simple-modal-dialog.component.scss: -------------------------------------------------------------------------------- 1 | .app-overlay { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | background-color: rgba(0, 0, 0, 0.5); 8 | z-index: 999; 9 | } 10 | 11 | .app-dialog { 12 | z-index: 1000; 13 | position: absolute; 14 | right: 0; 15 | left: 0; 16 | top: 20px; 17 | margin-right: auto; 18 | margin-left: auto; 19 | min-height: 200px; 20 | width: 600px; 21 | height: 600px; 22 | background-color: #fff; 23 | padding: 12px; 24 | box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), 0 13px 19px 2px rgba(0, 0, 0, 0.14), 0 5px 24px 4px rgba(0, 0, 0, 0.12); 25 | } 26 | 27 | @media (min-width: 768px) { 28 | .app-dialog { 29 | top: 40px; 30 | } 31 | } 32 | 33 | .app-dialog-command-btn { 34 | background: none; 35 | color: #2d2d2d; 36 | position: absolute; 37 | right: 20px; 38 | font-size: 1.2em; 39 | 40 | } 41 | .app-dialog-command-close-btn{ 42 | @extend .app-dialog-command-btn; 43 | top: 1px; 44 | border: 0; 45 | } 46 | .app-dialog-command-confirm-btn{ 47 | @extend .app-dialog-command-btn; 48 | bottom: 2px; 49 | } 50 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | let context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/app/services/logon-backend.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response, Headers } from '@angular/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AppConfigService } from './app-config.service'; 6 | import { LogonRequestInfo, UserInfo } from 7 | '../models/authentication-data-def'; 8 | 9 | @Injectable() 10 | export class LogonBackendService { 11 | private headers = new Headers({ 'Content-Type': 'application/json' }); 12 | 13 | constructor(private http: Http, 14 | private appConfig: AppConfigService) { } 15 | 16 | logon(value: LogonRequestInfo): 17 | Observable { 18 | return this.http.post( 19 | this.appConfig.serverAddress + 'api/Home/Logon', 20 | value, 21 | { headers: this.headers }) 22 | .map((r: Response) => { 23 | this.outputMonitorLog(r.json()); 24 | return r.json() as UserInfo; 25 | }) 26 | .catch(error => { 27 | console.error(error); 28 | return Observable.of<{}>(error); 29 | }); 30 | } 31 | 32 | outputMonitorLog(data) { 33 | if (this.appConfig.logCommunication) { 34 | console.log(data); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/app/reducers/ActivityToolsReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | import { createSelector } from 'reselect'; 3 | import { ActivityTool } from '../models/flow-data-def'; 4 | import { ActivityToolActions } from '../actions'; 5 | 6 | export interface ActivityToolsState { 7 | currentActivityTool: string; 8 | }; 9 | 10 | const initialState: ActivityToolsState = { 11 | currentActivityTool: "_" 12 | }; 13 | 14 | export const ActivityToolsReducer = 15 | function(state: ActivityToolsState = initialState, 16 | action: Action): ActivityToolsState 17 | { 18 | switch (action.type) { 19 | case ActivityToolActions.SET_CURRENT_TOOL: 20 | const tool: string = 21 | (action).tool; 22 | return { 23 | currentActivityTool: tool 24 | }; 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | export const getActivityToolsState = (state): ActivityToolsState => 31 | state.dummy; 32 | 33 | // The selector 34 | export const getCurrentActivityTool = createSelector( 35 | getActivityToolsState, 36 | ( state: ActivityToolsState ) => state.currentActivityTool ); -------------------------------------------------------------------------------- /src/app/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { FlowDataReducer, FlowDataState } from './FlowDataReducer'; 2 | import { 3 | Reducer, 4 | combineReducers 5 | } from 'redux'; 6 | 7 | import { 8 | ActivityToolsState, 9 | ActivityToolsReducer, 10 | } from './ActivityToolsReducer'; 11 | export * from './ActivityToolsReducer'; 12 | 13 | import { 14 | ActivityNodeDatasState , 15 | ActivityNodeDatasReducer 16 | } from './ActivityNodeDataReducer'; 17 | export * from './ActivityNodeDataReducer'; 18 | 19 | import { 20 | ActivityConnectionDatasState , 21 | ActivityConnectionDatasReducer 22 | } from './ActivityConnectionDataReducer'; 23 | export * from './ActivityConnectionDataReducer'; 24 | 25 | export interface AppState { 26 | flowData: FlowDataState; 27 | activityTools: ActivityToolsState; 28 | activityDataNodes: ActivityNodeDatasState; 29 | activityConnections: ActivityConnectionDatasState; 30 | } 31 | 32 | const rootReducer: Reducer = combineReducers({ 33 | flowData: FlowDataReducer, 34 | activityTools: ActivityToolsReducer, 35 | activityDataNodes: ActivityNodeDatasReducer, 36 | activityConnections: ActivityConnectionDatasReducer, 37 | }); 38 | 39 | export default rootReducer; 40 | -------------------------------------------------------------------------------- /src/app/containers/main-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { LogonBackendService } from 4 | '../services/logon-backend.service'; 5 | 6 | import { LogonRequestInfo, UserInfo } from '../models/authentication-data-def'; 7 | 8 | @Component({ 9 | selector: 'app-main-menu', 10 | template: ` 11 | 29 | `, 30 | }) 31 | export class MainMenuComponent implements OnInit { 32 | 33 | constructor(private logonBackendService: 34 | LogonBackendService) { } 35 | 36 | ngOnInit() { 37 | } 38 | 39 | logon() { 40 | } 41 | 42 | logoff() { 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | mime: { 21 | 'text/x-typescript': ['ts','tsx'] 22 | }, 23 | remapIstanbulReporter: { 24 | reports: { 25 | html: 'coverage', 26 | lcovonly: './coverage/coverage.lcov' 27 | } 28 | }, 29 | angularCli: { 30 | config: './angular-cli.json', 31 | environment: 'dev' 32 | }, 33 | reporters: config.angularCli && config.angularCli.codeCoverage 34 | ? ['progress', 'karma-remap-istanbul'] 35 | : ['progress'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/components/activity-tool.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Inject } from '@angular/core'; 2 | import { Store } from 'redux'; 3 | 4 | import { AppStore } from '../app-store'; 5 | import { 6 | ActivityToolActions 7 | } from '../actions'; 8 | 9 | import { 10 | AppState 11 | } from '../reducers'; 12 | 13 | import {ActivityTool, ActivityToolSet} from '../models/flow-data-def'; 14 | 15 | @Component({ 16 | selector: 'flow-activity-tool', 17 | template: ` 18 |
19 | 20 |
{{activityToolName}}
21 |
22 | ` 23 | }) 24 | export class ActivityToolComponent implements OnInit { 25 | @Input() isSelected: boolean = false; 26 | @Input() activityType: string; 27 | @Input() activityToolName: string; 28 | @Input() imagePath: string; 29 | @Input() tooltip?: string; 30 | @Input() tool?: ActivityTool; 31 | 32 | constructor(@Inject(AppStore) private store: Store){ 33 | 34 | } 35 | 36 | ngOnInit() { 37 | } 38 | 39 | onClick(event:any){ 40 | this.store.dispatch(ActivityToolActions.setCurrentTool(this.activityType)); 41 | event.preventDefault(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/app/services/slide-tool-bar.config.ts: -------------------------------------------------------------------------------- 1 | import {ActivityTool, ActivityToolSet} from '../models/flow-data-def'; 2 | 3 | export const activityToolsConfig = { 4 | activityToolsets: [ 5 | {name: "基本活动集", display: "基本活动集", 6 | tools:[{isSelected: true, activityType: "_", activityToolName: "选择/移动", 7 | imagePath: "assets/images/flow_select.png"}, 8 | {activityType: "_connection", activityToolName: "连接", 9 | imagePath: "assets/images/flow_arrow.png"}, 10 | {activityType: "st-start", activityToolName: "开始", 11 | imagePath: "assets/images/flow_start.png"}, 12 | {activityType: "st-end", activityToolName: "结束", 13 | imagePath: "assets/images/flow_end.png"}, 14 | {activityType: "st-singleHumanActivity", activityToolName: "单人处理", 15 | imagePath: "assets/images/flow_singleHumanActivity.png"}, 16 | {activityType: "st-multiHumanActivity", activityToolName: "多人处理", 17 | imagePath: "assets/images/flow_multiHumanActivity.png"}, 18 | {activityType: "st-autoActivity", activityToolName: "自动规则", 19 | imagePath: "assets/images/flow_autoActivity.png"}, 20 | ]}, 21 | // {name: "扩展活动集", display: "扩展活动集", 22 | // tools:[]} 23 | ], 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](screenshot.gif) 2 | 3 | # FlowDesigner 4 | A Flow Designer developed with Angular 2, Raphel, Redux ... 5 | 一个流程模板设计器, 目标是满足99%的项目流程设计要求. 6 | 尚在开发中. 目前可导入导出流程定义的JSON. 7 | 8 | This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.24. 9 | 10 | ## Development server 11 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 12 | 13 | ## Code scaffolding 14 | 15 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. 16 | 17 | ## Build 18 | 19 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 20 | 21 | ## Running unit tests 22 | 23 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 24 | 25 | ## Running end-to-end tests 26 | 27 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 28 | Before running the tests make sure you are serving the app via `ng serve`. 29 | 30 | ## Deploying to Github Pages 31 | 32 | Run `ng github-pages:deploy` to deploy to Github Pages. 33 | 34 | ## Further help 35 | 36 | To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 37 | -------------------------------------------------------------------------------- /src/app/components/simple-modal-dialog.component.ts: -------------------------------------------------------------------------------- 1 | /// Learned from https://coryrylan.com/blog/build-a-angular-modal-dialog-with-angular-animate 2 | import { Component, OnInit, Input, Output, OnChanges, EventEmitter, 3 | trigger, state, style, animate, transition } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'app-simple-modal-dialog', 7 | exportAs: '', 8 | outputs: ['visibleChange','confirmed'], 9 | styleUrls: ['./simple-modal-dialog.component.scss'], 10 | animations: [ 11 | trigger('dialog', [ 12 | transition('void => *', [ 13 | style({ transform: 'scale3d(.3, .3, .3)' }), 14 | animate(100) 15 | ]), 16 | transition('* => void', [ 17 | animate(100, style({ transform: 'scale3d(.0, .0, .0)' })) 18 | ]) 19 | ]) 20 | ], 21 | template: ` 22 |
23 | 25 | 27 | 28 |
29 |
30 | ` 31 | }) 32 | export class SimpleModalDialogComponent implements OnInit { 33 | 34 | @Input() closable = true; 35 | @Input() visible: boolean; 36 | @Output() visibleChange: EventEmitter = new EventEmitter(); 37 | @Output() confirmed: EventEmitter = new EventEmitter(); 38 | 39 | constructor() { } 40 | 41 | ngOnInit() { } 42 | 43 | close() { 44 | this.visible = false; 45 | this.visibleChange.emit(this.visible); 46 | } 47 | 48 | confirm() { 49 | this.visible = false; 50 | this.confirmed.emit(true); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/actions/ActivityNodeDataActions.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionCreator } from 'redux'; 2 | 3 | import { ActivityNodeData } from '../models/flow-data-def'; 4 | 5 | // Populate NodeDatas 6 | export const POPULATE_NODES_DATA = '[NodesData] Populate'; 7 | export interface PopulateNodesDataAction extends Action{ 8 | nodesData: ActivityNodeData[]; 9 | currentNodeGuid: string; 10 | } 11 | export const populateNodesData: ActionCreator = 12 | (nodesData, currentNodeGuid) => 13 | ({type: POPULATE_NODES_DATA, nodesData: nodesData, 14 | currentNodeGuid: currentNodeGuid}); 15 | 16 | // Create NodeData 17 | export const CREATE_NODE_DATA = '[NodeData] Create'; 18 | export interface CreateNodeDataAction extends Action{ 19 | nodeData: ActivityNodeData; 20 | } 21 | export const CreateNodeData: ActionCreator = 22 | (nodeData) => ({type: CREATE_NODE_DATA, nodeData: nodeData}); 23 | 24 | // Delete NodeData 25 | export const DELETE_NODE_DATA = '[NodeData] Delete'; 26 | export interface DeleteNodeDataAction extends Action{ 27 | nodeGuid: string; 28 | } 29 | export const DeleteNodeData: ActionCreator< DeleteNodeDataAction> = 30 | (nodeGuid) => ({type: DELETE_NODE_DATA, nodeGuid: nodeGuid}); 31 | 32 | // Update NodeData 33 | export const UPDATE_NODE_DATA = '[NodeData] Update'; 34 | export interface UpdateNodeDataAction extends Action{ 35 | nodeData: ActivityNodeData; 36 | } 37 | export const UpdateNodeData: ActionCreator< UpdateNodeDataAction> = 38 | (nodeData) => ({type: UPDATE_NODE_DATA, nodeData: nodeData}); 39 | 40 | // Set Current NodeData 41 | export const SET_CURRENT_NODE_DATA = '[NodeData] SetCurrent'; 42 | export interface SetCurrentNodeDataAction extends Action{ 43 | currentNodeGuid: string; 44 | } 45 | export const SetCurrentNodeData: ActionCreator< SetCurrentNodeDataAction> = 46 | (currentNodeGuid) => ({type: SET_CURRENT_NODE_DATA, currentNodeGuid: currentNodeGuid}); 47 | 48 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.24", 4 | "name": "flow-designer" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "test": "test.ts", 17 | "tsconfig": "tsconfig.json", 18 | "prefix": "app", 19 | "mobile": false, 20 | "styles": [ 21 | "styles.scss", 22 | "../node_modules/bootstrap/dist/css/bootstrap.min.css", 23 | "../node_modules/primeng/resources/themes/omega/theme.css", 24 | "../node_modules/primeng/resources/primeng.min.css", 25 | "../node_modules/font-awesome/css/font-awesome.min.css" 26 | ], 27 | "scripts": [ 28 | "../node_modules/jquery/dist/jquery.js", 29 | "../node_modules/tether/dist/js/tether.js", 30 | "../node_modules/bootstrap/dist/js/bootstrap.js", 31 | "../node_modules/guid/guid.js" 32 | ], 33 | "environmentSource": "environments/environment.ts", 34 | "environments": { 35 | "source": "environments/environment.ts", 36 | "dev": "environments/environment.ts", 37 | "prod": "environments/environment.prod.ts" 38 | } 39 | } 40 | ], 41 | "addons": [ 42 | ], 43 | "packages": [ 44 | ], 45 | "e2e": { 46 | "protractor": { 47 | "config": "./protractor.conf.js" 48 | } 49 | }, 50 | "test": { 51 | "karma": { 52 | "config": "./karma.conf.js" 53 | } 54 | }, 55 | "defaults": { 56 | "styleExt": "scss", 57 | "prefixInterfaces": false, 58 | "inline": { 59 | "style": false, 60 | "template": false 61 | }, 62 | "spec": { 63 | "class": false, 64 | "component": true, 65 | "directive": true, 66 | "module": false, 67 | "pipe": true, 68 | "service": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/actions/FlowDataActions.ts: -------------------------------------------------------------------------------- 1 | /**只提供流程模板级别的数据维护, 不包括对Activity和Connection的操作 */ 2 | 3 | import { Action, ActionCreator } from 'redux'; 4 | 5 | import { 6 | FlowData 7 | } from '../models/flow-data-def'; 8 | 9 | // Open FlowDatas 10 | export const POPULATE_FLOW_DATA = '[FlowData] Populate'; 11 | export interface PopulateFlowDataAction extends Action{ 12 | flowData: FlowData; 13 | currentFlowGuid: string; 14 | } 15 | export const populateFlowData: ActionCreator = 16 | (flowData, currentFlowGuid) => 17 | ({type: POPULATE_FLOW_DATA, flowData: flowData, 18 | currentFlowGuid: currentFlowGuid}); 19 | 20 | // Create FlowData 21 | export const CREATE_FLOW_DATA = '[FlowData] Create'; 22 | export interface CreateFlowDataAction extends Action{ 23 | flowData: FlowData; 24 | } 25 | export const CreateFlowData: ActionCreator = 26 | (flowData) => ({type: CREATE_FLOW_DATA, flowData: flowData}); 27 | 28 | // Update FlowData 29 | export const UPDATE_FLOW_DATA = '[FlowData] Update'; 30 | export interface UpdateFlowDataAction extends Action{ 31 | flowData: FlowData; 32 | } 33 | export const UpdateFlowData: ActionCreator< UpdateFlowDataAction> = 34 | (flowData) => ({type: UPDATE_FLOW_DATA, flowData: flowData}); 35 | 36 | /** 目前似乎无实现Delete FlowData的需要 37 | export const DELETE_FLOW_DATA = '[FlowData] Delete'; 38 | export interface DeleteFlowDataAction extends Action{ 39 | flowGuid: string; 40 | } 41 | export const DeleteFlowData: ActionCreator< DeleteFlowDataAction> = 42 | (flowGuid) => ({type: DELETE_FLOW_DATA, flowGuid: flowGuid}); 43 | */ 44 | 45 | /** 目前似乎无实现Set Current FlowData的需要 46 | export const SET_CURRENT_FLOW_DATA = '[FlowData] SetCurrent'; 47 | export interface SetCurrentFlowDataAction extends Action{ 48 | currentFlowGuid: string; 49 | } 50 | export const SetCurrentFlowData: ActionCreator< SetCurrentFlowDataAction> = 51 | (currentFlowGuid) => ({type: SET_CURRENT_FLOW_DATA, currentFlowGuid: currentFlowGuid}); 52 | */ -------------------------------------------------------------------------------- /src/app/containers/slide-tool-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject, ElementRef } from '@angular/core'; 2 | import { Store } from 'redux'; 3 | 4 | import {ActivityToolComponent} from '../components/activity-tool.component'; 5 | 6 | import {ActivityTool, ActivityToolSet} from '../models/flow-data-def'; 7 | 8 | import {activityToolsConfig} from '../services/slide-tool-bar.config'; 9 | 10 | import { AppStore } from '../app-store'; 11 | 12 | import { 13 | ActivityToolActions 14 | } from '../actions'; 15 | 16 | import { 17 | AppState, 18 | getCurrentActivityTool, 19 | } from '../reducers'; 20 | 21 | 22 | @Component({ 23 | selector: 'app-slide-tool-bar', 24 | template: ` 25 | 38 | ` 39 | }) 40 | export class SlideToolBarComponent implements OnInit { 41 | activityToolSets: ActivityToolSet[]; 42 | currentActivityType: string; 43 | 44 | constructor(@Inject(AppStore) private store: Store) { 45 | this.activityToolSets = activityToolsConfig.activityToolsets; 46 | store.subscribe(() => this.updateCurrentActivityTool()); 47 | this.updateCurrentActivityTool(); 48 | } 49 | 50 | updateCurrentActivityTool(){ 51 | let state = this.store.getState(); 52 | this.currentActivityType = state.activityTools.currentActivityTool; 53 | } 54 | 55 | ngOnInit() { 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/components/dynamic-flow-property-control.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | 4 | import { 5 | FlowPropertyBase, 6 | TextboxFlowProperty, 7 | DropdownFlowProperty, 8 | TextareaFlowProperty 9 | } from '../models/flow-data-def'; 10 | 11 | 12 | @Component({ 13 | selector: 'app-dynamic-flow-property-control', 14 | template: ` 15 | 16 | 17 |
18 | 19 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 |
44 | 45 |
"{{property.label}}"不能为空!
46 | ` 47 | }) 48 | export class DynamicFlowPropertyControlComponent implements OnInit { 49 | @Input() property: FlowPropertyBase; 50 | @Input() formGroup: FormGroup; 51 | 52 | get isValid() { 53 | return this.formGroup.controls[this.property.guid].valid; 54 | } 55 | 56 | constructor() { } 57 | 58 | ngOnInit() { 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-designer", 3 | "version": "0.0.1", 4 | "license": "LGPL", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "lint": "tslint \"src/**/*.ts\"", 10 | "test": "ng test", 11 | "pree2e": "webdriver-manager update --standalone false --gecko false", 12 | "e2e": "protractor" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "^2.3.1", 17 | "@angular/compiler": "^2.3.1", 18 | "@angular/core": "^2.3.1", 19 | "@angular/forms": "^2.3.1", 20 | "@angular/http": "^2.3.1", 21 | "@angular/platform-browser": "^2.3.1", 22 | "@angular/platform-browser-dynamic": "^2.3.1", 23 | "@angular/router": "^3.3.1", 24 | "base64-js": "^1.2.0", 25 | "core-js": "^2.4.1", 26 | "font-awesome": "^4.7.0", 27 | "immutable": "^3.8.1", 28 | "jquery": "^3.1.1", 29 | "lodash": "^4.17.2", 30 | "moment": "^2.17.1", 31 | "ng2-bootstrap": "^1.2.1", 32 | "normalizr": "^3.0.0-beta", 33 | "notie": "^3.9.5", 34 | "primeng": "^2.0.0-rc.2", 35 | "raphael": "^2.2.7", 36 | "redux": "^3.6.0", 37 | "reselect": "^2.5.4", 38 | "rxjs": "^5.0.1", 39 | "tether": "^1.4.0", 40 | "ts-helpers": "^1.1.1", 41 | "zone.js": "^0.7.2" 42 | }, 43 | "devDependencies": { 44 | "@angular/cli": "^1.1.1", 45 | "@angular/compiler-cli": "^2.3.1", 46 | "@types/base64-js": "^1.2.5", 47 | "@types/jasmine": "2.5.38", 48 | "@types/jquery": "^2.0.34", 49 | "@types/lodash": "^4.14.44", 50 | "@types/moment": "^2.13.0", 51 | "@types/node": "^6.0.42", 52 | "@types/notie": "0.0.27", 53 | "@types/raphael": "^2.1.30", 54 | "codelyzer": "~2.0.0-beta.1", 55 | "jasmine-core": "2.5.2", 56 | "jasmine-spec-reporter": "2.5.0", 57 | "karma": "1.2.0", 58 | "karma-chrome-launcher": "^2.0.0", 59 | "karma-cli": "^1.0.1", 60 | "karma-jasmine": "^1.0.2", 61 | "karma-remap-istanbul": "^0.2.1", 62 | "protractor": "^5.1.2", 63 | "ts-node": "1.2.1", 64 | "tslint": "^4.0.0", 65 | "typescript": "~2.0.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/utilites/format-json.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * from: http://www.cnblogs.com/buyucoder/p/4849518.html 3 | * json美化 4 | * formatJson(json)这样为格式化代码。 5 | * formatJson(json,true)为开启压缩模式 6 | * @param txt 7 | * @param compress 8 | * @returns {string} 9 | */ 10 | export function formatJson(txt: string, compress: boolean = false) { 11 | var indentChar = ' '; 12 | if (/^\s*$/.test(txt)) { 13 | console.error('数据为空,无法格式化! '); 14 | return; 15 | } 16 | // console.info(txt); 17 | txt = txt.replace(/\\n/g, '#x-x#'); // 将换行回车转码 18 | // console.info(txt); 19 | try { var data = eval('(' + txt + ')'); } 20 | catch (e) { 21 | console.error('数据源语法错误,格式化失败! 错误信息: ' + e.description, 'err'); 22 | return; 23 | }; 24 | var draw = [], last = false, This = this, line = compress ? '' : '\n', nodeCount = 0, maxDepth = 0; 25 | 26 | var notify = function (name, value, isLast, indent/*缩进*/, formObj) { 27 | nodeCount++;/*节点计数*/ 28 | for (var i = 0, tab = ''; i < indent; i++)tab += indentChar;/* 缩进HTML */ 29 | tab = compress ? '' : tab;/*压缩模式忽略缩进*/ 30 | maxDepth = ++indent;/*缩进递增并记录*/ 31 | if (value && value.constructor == Array) {/*处理数组*/ 32 | draw.push(tab + (formObj ? ('"' + name + '":') : '') + '[' + line);/*缩进'[' 然后换行*/ 33 | for (var i = 0; i < value.length; i++) 34 | notify(i, value[i], i == value.length - 1, indent, false); 35 | draw.push(tab + ']' + (isLast ? line : (',' + line)));/*缩进']'换行,若非尾元素则添加逗号*/ 36 | } else if (value && typeof value == 'object') {/*处理对象*/ 37 | draw.push(tab + (formObj ? ('"' + name + '":') : '') + '{' + line);/*缩进'{' 然后换行*/ 38 | var len = 0, i = 0; 39 | for (var key in value) len++; 40 | for (var key in value) notify(key, value[key], ++i == len, indent, true); 41 | draw.push(tab + '}' + (isLast ? line : (',' + line)));/*缩进'}'换行,若非尾元素则添加逗号*/ 42 | } else { 43 | if (typeof value == 'string') value = '"' + value + '"'; 44 | draw.push(tab + (formObj ? ('"' + name + '":') : '') + value + (isLast ? '' : ',') + line); 45 | }; 46 | }; 47 | var isLast = true, indent = 0; 48 | notify('', data, isLast, indent, false); 49 | return draw.join(''); 50 | } -------------------------------------------------------------------------------- /src/app/services/flow-template-backend.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response, Headers } from '@angular/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AppConfigService } from './app-config.service'; 6 | import { FlowTemplateDTO } from '../models/master-data-def'; 7 | 8 | @Injectable() 9 | export class FlowTemplateBackendService { 10 | private headers = new Headers({ 'Content-Type': 'application/json' }); 11 | 12 | constructor(private http: Http, 13 | private appConfig: AppConfigService) { } 14 | 15 | getFlowTemplates(): Observable { 16 | return this.http.get( 17 | this.appConfig.serverAddress + 'api/FlowTemplate', 18 | { headers: this.headers }) 19 | .map((r: Response) => { 20 | this.outputMonitorLog(r.json()); 21 | return r.json() as FlowTemplateDTO[]; 22 | }) 23 | .catch(error => { 24 | console.error(error); 25 | return Observable.of([]); 26 | }); 27 | } 28 | 29 | newFlowTemplate(value: FlowTemplateDTO): 30 | Observable { 31 | return this.http.post( 32 | this.appConfig.serverAddress + 'api/FlowTemplate', 33 | value, 34 | { headers: this.headers }) 35 | .map((r: Response) => { 36 | this.outputMonitorLog(r.json()); 37 | return r.json() as FlowTemplateDTO; 38 | }) 39 | .catch(error => { 40 | console.error(error); 41 | return Observable.of<{}>(error); 42 | }); 43 | } 44 | 45 | updateFlowTemplate(value: FlowTemplateDTO): 46 | Observable { 47 | return this.http.put( 48 | this.appConfig.serverAddress + 'api/FlowTemplate/' + value.guid, 49 | value, 50 | { headers: this.headers }) 51 | .map((r: Response) => { 52 | this.outputMonitorLog(r.json()); 53 | return r.json() as FlowTemplateDTO; 54 | }) 55 | .catch(error => { 56 | console.error(error); 57 | return Observable.of<{}>(error); 58 | }); 59 | } 60 | 61 | outputMonitorLog(data) { 62 | if (this.appConfig.logCommunication) { 63 | console.log(data); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/services/new-flow-template-skeleton.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { genGuid } from '../utilites/gen-guid'; 3 | import * as moment from 'moment'; 4 | 5 | @Injectable() 6 | export class NewFlowTemplateSkeletonService { 7 | constructor() { 8 | } 9 | 10 | getNewFlowTemplateSkeletonObj(): Object { 11 | return { 12 | 'basicInfo': { 13 | 'name': 'Untitled Flow', 14 | 'code': 'XXX', 15 | 'version': '1', 16 | 'guid': genGuid(), 17 | 'desc': 'Untitled Flow', 18 | 'creator': { 19 | 'name': 'Qin,Chao', 20 | 'guid': 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' 21 | }, 22 | 'createTime': moment().format('YYYY-MM-DD HH:mm:ssA'), 23 | 'lastUpdateTime': '', 24 | 'displayName': 'Untitled Flow/未命名流程', 25 | 'isPopular': '' 26 | }, 27 | 'advancedInfo': { 28 | 'arbitraryJumpAllowed': '', 29 | 'managers': '' 30 | }, 31 | 'customData': { 32 | }, 33 | 'activityNodes': { 34 | 'nodes': [ 35 | { 36 | 'type': 'st-start', 37 | 'guid': genGuid(), 38 | 'size': [ 39 | 0, 40 | 0 41 | ], 42 | 'position': [ 43 | 120, 44 | 22 45 | ], 46 | 'name': '开始', 47 | 'customData': { 48 | }, 49 | 'linkForm': '', 50 | 'beforeActions': '', 51 | 'afterActions': '', 52 | 'roles': [ 53 | ] 54 | }, 55 | { 56 | 'type': 'st-end', 57 | 'guid': genGuid(), 58 | 'size': [ 59 | 0, 60 | 0 61 | ], 62 | 'position': [ 63 | 300, 64 | 200 65 | ], 66 | 'name': '结束', 67 | 'customData': { 68 | }, 69 | 'linkForm': '', 70 | 'beforeActions': '', 71 | 'afterActions': '', 72 | 'roles': [ 73 | ] 74 | }, 75 | ] 76 | }, 77 | 'activityConnections': { 78 | 'connections': [ 79 | ] 80 | } 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/reducers/FlowDataReducer.ts: -------------------------------------------------------------------------------- 1 | import { __core_private__ } from '@angular/core'; 2 | import { Action } from 'redux'; 3 | import { createSelector } from 'reselect'; 4 | import * as _ from 'lodash'; 5 | 6 | import { FlowData } from '../models/flow-data-def'; 7 | import { FlowDataActions } from '../actions'; 8 | 9 | import { mockFlowDataObj } from '../services/mock-data'; 10 | 11 | export interface FlowDataState { 12 | flowData: FlowData; 13 | currentFlowGuid: string; 14 | }; 15 | 16 | const initialState: FlowDataState = { 17 | flowData: mockFlowDataObj(), 18 | currentFlowGuid: (mockFlowDataObj()).basicInfo.guid, 19 | }; 20 | 21 | export const FlowDataReducer = 22 | function (state: FlowDataState = initialState, 23 | action: Action): FlowDataState { 24 | switch (action.type) { 25 | case FlowDataActions.POPULATE_FLOW_DATA: 26 | const _actPopulate = 27 | (action); 28 | return { 29 | flowData: _actPopulate.flowData, 30 | currentFlowGuid: _actPopulate.flowData.basicInfo.guid 31 | }; 32 | 33 | case FlowDataActions.CREATE_FLOW_DATA: 34 | const _actCreate = action; 35 | return { 36 | flowData: _actCreate.flowData, 37 | currentFlowGuid: _actCreate.flowData.basicInfo.guid 38 | }; 39 | 40 | /** 为保持reducer的结构简单, 只实现对basicInfo,advancedInfo和customData的更新, 41 | * activityNodes和activityConnections的更新由特定的reducer来执行*/ 42 | case FlowDataActions.UPDATE_FLOW_DATA: 43 | const _actUpdate = (action); 44 | return { 45 | flowData: Object.assign({}, state.flowData, _actUpdate.flowData), 46 | currentFlowGuid: state.currentFlowGuid 47 | }; 48 | 49 | /** 目前似乎无实现Delete的需要 50 | case FlowDataActions.DELETE_FLOW_DATA: 51 | const _actDelete = (action); 52 | return { 53 | flowData: null, 54 | currentFlowGuid: null 55 | }; 56 | */ 57 | 58 | /** 目前似乎无实现SetCurrentFlowData的需要 59 | case FlowDataActions.SET_CURRENT_FLOW_DATA: 60 | const _actSetCurrent = 61 | (action); 62 | return { flowData: state.flowData, 63 | currentFlowGuid: _actSetCurrent.currentFlowGuid}; 64 | */ 65 | default: 66 | return state; 67 | } 68 | }; -------------------------------------------------------------------------------- /src/app/actions/ActivityConnectionDataActions.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionCreator } from 'redux'; 2 | 3 | import { ActivityConnectionData } from '../models/flow-data-def'; 4 | 5 | // Populate ConnectionDatas 6 | export const POPULATE_CONNECTIONS_DATA = '[ConnectionsData] Populate'; 7 | export interface PopulateConnectionsDataAction extends Action{ 8 | connectionsData: ActivityConnectionData[]; 9 | currentConnectionGuid: string; 10 | } 11 | export const populateConnectionsData: ActionCreator = 12 | (connectionsData, currentConnectionGuid) => ({type: POPULATE_CONNECTIONS_DATA, 13 | connectionsData: connectionsData, 14 | currentConnectionGuid: currentConnectionGuid}); 15 | 16 | // Create ConnectionData 17 | export const CREATE_CONNECTION_DATA = '[ConnectionData] Create'; 18 | export interface CreateConnectionDataAction extends Action{ 19 | connectionData: ActivityConnectionData; 20 | } 21 | export const CreateConnectionData: ActionCreator = 22 | (connectionData) => ({type: CREATE_CONNECTION_DATA, 23 | connectionData: connectionData}); 24 | 25 | // Delete ConnectionData 26 | export const DELETE_CONNECTION_DATA = '[ConnectionData] Delete'; 27 | export interface DeleteConnectionDataAction extends Action{ 28 | connectionGuid: string; 29 | } 30 | export const DeleteConnectionData: ActionCreator< DeleteConnectionDataAction> = 31 | (connectionGuid) => ({type: DELETE_CONNECTION_DATA, 32 | connectionGuid: connectionGuid}); 33 | 34 | // Update ConnectionData 35 | export const UPDATE_CONNECTION_DATA = '[ConnectionData] Update'; 36 | export interface UpdateConnectionDataAction extends Action{ 37 | connectionData: ActivityConnectionData; 38 | } 39 | export const UpdateConnectionData: ActionCreator< UpdateConnectionDataAction> = 40 | (connectionData) => ({type: UPDATE_CONNECTION_DATA, 41 | connectionData: connectionData}); 42 | 43 | // Set Current ConnectionData 44 | export const SET_CURRENT_CONNECTION_DATA = '[ConnectionData] SetCurrent'; 45 | export interface SetCurrentConnectionDataAction extends Action{ 46 | currentConnectionGuid: string; 47 | } 48 | export const SetCurrentConnectionData: ActionCreator< SetCurrentConnectionDataAction> = 49 | (currentConnectionGuid) => ({type: SET_CURRENT_CONNECTION_DATA, 50 | currentConnectionGuid: currentConnectionGuid}); 51 | 52 | -------------------------------------------------------------------------------- /src/app/reducers/ActivityNodeDataReducer.ts: -------------------------------------------------------------------------------- 1 | import { __core_private__ } from '@angular/core'; 2 | import { Action } from 'redux'; 3 | import { createSelector } from 'reselect'; 4 | import * as _ from 'lodash'; 5 | 6 | import { ActivityNodeData, FlowData } from '../models/flow-data-def'; 7 | import { ActivityNodeDataActions } from '../actions'; 8 | 9 | import {mockFlowDataObj} from '../services/mock-data'; 10 | 11 | export interface ActivityNodeDatasState { 12 | activityNodeDatas: ActivityNodeData[]; 13 | currentNodeGuid: string; 14 | }; 15 | 16 | const initialState: ActivityNodeDatasState = { 17 | activityNodeDatas: (mockFlowDataObj()).activityNodes.nodes, 18 | currentNodeGuid: null 19 | }; 20 | 21 | export const ActivityNodeDatasReducer = 22 | function(state: ActivityNodeDatasState = initialState, 23 | action: Action): ActivityNodeDatasState 24 | { 25 | switch (action.type) { 26 | case ActivityNodeDataActions.POPULATE_NODES_DATA: 27 | const _actPopulate = 28 | (action); 29 | return { activityNodeDatas: _actPopulate.nodesData, 30 | currentNodeGuid: null}; 31 | 32 | case ActivityNodeDataActions.CREATE_NODE_DATA: 33 | const _actCreate = action; 34 | return { activityNodeDatas: [...state.activityNodeDatas, 35 | _actCreate.nodeData], 36 | currentNodeGuid: _actCreate.nodeData.guid}; 37 | 38 | case ActivityNodeDataActions.DELETE_NODE_DATA: 39 | const _actDelete = (action); 40 | return { activityNodeDatas: _.filter(state.activityNodeDatas, 41 | (n)=>{return n.guid!=_actDelete.nodeGuid}), 42 | currentNodeGuid: null}; 43 | 44 | case ActivityNodeDataActions.UPDATE_NODE_DATA: 45 | const _actUpdate = (action); 46 | const _index = _.findIndex(state.activityNodeDatas, 47 | {guid: _actUpdate.nodeData.guid}); 48 | return { activityNodeDatas: [...state.activityNodeDatas.slice(0,_index), 49 | Object.assign({}, state.activityNodeDatas[_index], 50 | _actUpdate.nodeData), 51 | ...state.activityNodeDatas.slice(_index+1) 52 | ],currentNodeGuid: _actUpdate.nodeData.guid}; 53 | 54 | case ActivityNodeDataActions.SET_CURRENT_NODE_DATA: 55 | const _actSetCurrent = 56 | (action); 57 | return { activityNodeDatas: state.activityNodeDatas, 58 | currentNodeGuid: _actSetCurrent.currentNodeGuid}; 59 | 60 | default: 61 | return state; 62 | } 63 | }; -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | html, 2 | fieldet, 3 | legend, 4 | div, 5 | span, 6 | a, 7 | form, 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | body { 14 | font-size: 16px; 15 | } 16 | 17 | #TotalFlowArea { 18 | position: absolute; 19 | top: 0; 20 | bottom: 0; 21 | left: 0; 22 | right: 0; 23 | } 24 | 25 | .main-flow-area { 26 | z-index: 999; 27 | } 28 | 29 | $MainSubjectAreaHeight : 0px; 30 | #MainSubjectArea { 31 | height: $MainSubjectAreaHeight; 32 | background-color: lightsalmon; 33 | } 34 | 35 | $MainMenuBarHeight : 37px; 36 | #MainMenuBar { 37 | height: $MainMenuBarHeight; 38 | background-color: lightgoldenrodyellow; 39 | } 40 | 41 | $MainFooterAreaHeight : 20px; 42 | 43 | #MainDesignArea { 44 | top: $MainSubjectAreaHeight + $MainMenuBarHeight; 45 | left: 0px; 46 | right: 0px; 47 | bottom: $MainFooterAreaHeight; 48 | position: absolute; 49 | display: flex; 50 | height: auto; 51 | } 52 | 53 | #MainFooterArea { 54 | height: $MainFooterAreaHeight; 55 | position: absolute; 56 | bottom: 0px; 57 | left: 0px; 58 | right: 0px; 59 | background-color: lightsalmon; 60 | } 61 | 62 | #SlideToolBar { 63 | float: left; 64 | width: 110px; 65 | border-right-color: black; 66 | border-right-width: 2px; 67 | border-right-style: solid; 68 | } 69 | 70 | #FlowFrame { 71 | overflow: hidden; 72 | flex: 1; 73 | display: flex; 74 | flex-direction: column; 75 | } 76 | 77 | #PropertyPanel { 78 | float: right; 79 | width: 250px; 80 | background-color: white; 81 | border-left-color: rgb(0, 0, 0); 82 | border-left-width: 2px; 83 | border-left-style: dotted; 84 | } 85 | 86 | #FlowTemplateMenu { 87 | height: 28px; 88 | background-color: black; 89 | } 90 | 91 | #FlowChartParent { 92 | flex: 1; 93 | position: relative; 94 | overflow: auto; 95 | } 96 | 97 | #FlowChart { 98 | height: 100%; 99 | } 100 | 101 | .text-note { 102 | position: absolute; 103 | font-style: italic; 104 | } 105 | 106 | #TemplateNameNote { 107 | top: 0px; 108 | left: 5px; 109 | } 110 | 111 | .activity-tool, 112 | .activity-tool-title { 113 | text-align: center; 114 | margin-bottom: 8px; 115 | color: dimgray; 116 | } 117 | 118 | .activity-tool-selected { 119 | font-weight: bold; 120 | font-style: italic; 121 | color: black; 122 | >img { 123 | border: 2px dotted black; 124 | } 125 | } 126 | 127 | #InputActivityNodeName { 128 | position: absolute; 129 | left: 0px; 130 | top: 0px; 131 | width: 70px; 132 | border: 1px dotted gray; 133 | background: white; 134 | z-index: 1000; 135 | font-family: Verdana; 136 | font-size: 12px; 137 | display: none; 138 | } 139 | 140 | span.ui-dialog-title{ //改写PrimeNG的默认样式 141 | font-size: 1.2em !important; 142 | } 143 | 144 | .ui-dialog.ui-widget .ui-dialog-titlebar { //改写PrimeNG的默认样式 145 | padding: 0.5em 1.5em !important; 146 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "max-line-length": [ 20 | true, 21 | 140 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | "static-before-instance", 27 | "variables-before-functions" 28 | ], 29 | "no-arg": true, 30 | "no-bitwise": true, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-construct": true, 40 | "no-debugger": true, 41 | "no-duplicate-variable": true, 42 | "no-empty": false, 43 | "no-eval": true, 44 | "no-inferrable-types": true, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": false, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unused-expression": true, 50 | "no-use-before-declare": true, 51 | "no-var-keyword": true, 52 | "object-literal-sort-keys": false, 53 | "one-line": [ 54 | true, 55 | "check-open-brace", 56 | "check-catch", 57 | "check-else", 58 | "check-whitespace" 59 | ], 60 | "quotemark": [ 61 | true, 62 | "single" 63 | ], 64 | "radix": true, 65 | "semicolon": [ 66 | "always" 67 | ], 68 | "triple-equals": [ 69 | true, 70 | "allow-null-check" 71 | ], 72 | "typedef-whitespace": [ 73 | true, 74 | { 75 | "call-signature": "nospace", 76 | "index-signature": "nospace", 77 | "parameter": "nospace", 78 | "property-declaration": "nospace", 79 | "variable-declaration": "nospace" 80 | } 81 | ], 82 | "variable-name": false, 83 | "whitespace": [ 84 | true, 85 | "check-branch", 86 | "check-decl", 87 | "check-operator", 88 | "check-separator", 89 | "check-type" 90 | ], 91 | 92 | "directive-selector": [true, "attribute", "app", "camelCase"], 93 | "component-selector": [true, "element", "app", "kebab-case"], 94 | "use-input-property-decorator": true, 95 | "use-output-property-decorator": true, 96 | "use-host-property-decorator": true, 97 | "no-input-rename": true, 98 | "no-output-rename": true, 99 | "use-life-cycle-interface": true, 100 | "use-pipe-transform-interface": true, 101 | "component-class-suffix": true, 102 | "directive-class-suffix": true, 103 | "no-access-missing-member": true, 104 | "templates-use-public": true, 105 | "invoke-injectable": true 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/app/reducers/ActivityConnectionDataReducer.ts: -------------------------------------------------------------------------------- 1 | import { __core_private__ } from '@angular/core'; 2 | import { Action } from 'redux'; 3 | import { createSelector } from 'reselect'; 4 | import * as _ from 'lodash'; 5 | 6 | import { ActivityConnectionData, FlowData } from '../models/flow-data-def'; 7 | import { ActivityConnectionDataActions } from '../actions'; 8 | 9 | import { mockFlowDataObj } from '../services/mock-data'; 10 | 11 | export interface ActivityConnectionDatasState { 12 | activityConnectionDatas: ActivityConnectionData[]; 13 | currentConnectionGuid: string; 14 | }; 15 | 16 | const initialState: ActivityConnectionDatasState = { 17 | activityConnectionDatas: (mockFlowDataObj()).activityConnections.connections, 18 | currentConnectionGuid: null 19 | }; 20 | 21 | export const ActivityConnectionDatasReducer = 22 | function (state: ActivityConnectionDatasState = initialState, 23 | action: Action): ActivityConnectionDatasState { 24 | switch (action.type) { 25 | case ActivityConnectionDataActions.POPULATE_CONNECTIONS_DATA: 26 | const _actPopulate = 27 | (action); 28 | return { 29 | activityConnectionDatas: _actPopulate.connectionsData, 30 | currentConnectionGuid: null 31 | }; 32 | 33 | case ActivityConnectionDataActions.CREATE_CONNECTION_DATA: 34 | const _actCreate = (action); 35 | return { 36 | activityConnectionDatas: [...state.activityConnectionDatas, 37 | _actCreate.connectionData], 38 | currentConnectionGuid: _actCreate.connectionData.guid 39 | }; 40 | 41 | case ActivityConnectionDataActions.DELETE_CONNECTION_DATA: 42 | const _actDelete = (action); 43 | return { 44 | activityConnectionDatas: _.filter(state.activityConnectionDatas, 45 | (n) => { return n.guid != _actDelete.connectionGuid; }), 46 | currentConnectionGuid: state.currentConnectionGuid 47 | }; 48 | 49 | case ActivityConnectionDataActions.UPDATE_CONNECTION_DATA: 50 | const _actUpdate = (action); 51 | const _index = _.findIndex(state.activityConnectionDatas, 52 | { guid: _actUpdate.connectionData.guid }); 53 | return { 54 | activityConnectionDatas: [...state.activityConnectionDatas.slice(0, _index), 55 | Object.assign({}, state.activityConnectionDatas[_index], 56 | _actUpdate.connectionData), 57 | ...state.activityConnectionDatas.slice(_index + 1) 58 | ], currentConnectionGuid: state.currentConnectionGuid 59 | }; 60 | 61 | case ActivityConnectionDataActions.SET_CURRENT_CONNECTION_DATA: 62 | const _actSetCurrent = 63 | (action); 64 | return { 65 | activityConnectionDatas: state.activityConnectionDatas, 66 | currentConnectionGuid: _actSetCurrent.currentConnectionGuid 67 | }; 68 | 69 | default: 70 | return state; 71 | } 72 | }; -------------------------------------------------------------------------------- /src/app/components/role-user-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, forwardRef } from '@angular/core'; 2 | import { 3 | FormGroup, FormControl, 4 | ControlValueAccessor, NG_VALUE_ACCESSOR, 5 | Validator, NG_VALIDATORS, 6 | } from '@angular/forms'; 7 | import { OrderListModule, DialogModule, TabViewModule } from 'primeng/primeng'; 8 | import * as _ from 'lodash'; 9 | 10 | import { UserDTO, RoleDTO } 11 | from '../models/master-data-def'; 12 | 13 | import { Paticipant } from '../models/flow-data-def'; 14 | 15 | import { OrgDataService } from '../services/org-data.service'; 16 | 17 | // 实现参考 https://medium.com/@tarik.nzl/angular-2-custom-form-control-with-validation-json-input-2b4cf9bc2d73#.grea4w1io 18 | @Component({ 19 | selector: 'app-role-user-selector', 20 | template: ` 21 | 23 | 31 | 32 | 35 | 36 | 39 | 40 | `, 41 | providers: [ 42 | { 43 | provide: NG_VALUE_ACCESSOR, 44 | useExisting: forwardRef(() => RoleUserSelectorComponent), 45 | multi: true, 46 | }, 47 | { 48 | provide: NG_VALIDATORS, 49 | useExisting: forwardRef(() => RoleUserSelectorComponent), 50 | multi: true, 51 | } 52 | ] 53 | }) 54 | export class RoleUserSelectorComponent 55 | implements OnInit, ControlValueAccessor, Validator { 56 | @Input() required: boolean = false; 57 | 58 | roleAndUsers: Paticipant[]; 59 | displayDialog: boolean = false; 60 | 61 | showDialog() { 62 | this.displayDialog = true; 63 | } 64 | 65 | dialogClosed(){ 66 | this.displayDialog = false; 67 | } 68 | 69 | updateRoleAndUsers(event) { 70 | this.roleAndUsers = event; 71 | this.propagateChange(this.roleAndUsers); 72 | this.displayDialog = false; 73 | } 74 | 75 | constructor(private orgService: OrgDataService) { } 76 | 77 | ngOnInit() { 78 | this.roleAndUsers = []; 79 | } 80 | 81 | onReorder() { 82 | this.propagateChange(this.roleAndUsers); 83 | } 84 | 85 | // 以下为ControlValueAccessor的实现,具体阅读实现参考/|\ 86 | private propagateChange = (_: any) => { }; 87 | public writeValue(obj: any) { 88 | if (obj) { 89 | this.roleAndUsers = obj; 90 | } 91 | } 92 | get value(): any { 93 | return this.roleAndUsers; 94 | }; 95 | set value(v: any) { 96 | if (v !== this.roleAndUsers) { 97 | this.roleAndUsers = v; 98 | this.propagateChange(v); 99 | } 100 | } 101 | public registerOnChange(fn: any) { 102 | this.propagateChange = fn; 103 | } 104 | public registerOnTouched() { } 105 | 106 | // 以下为Validator的实现,具体阅读实现参考/|\ 107 | public validate(c: FormControl) { 108 | return !this.required ? null : { 109 | RoleUserSelectorError: { 110 | valid: false, 111 | }, 112 | }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/app/models/master-data-def.ts: -------------------------------------------------------------------------------- 1 | export interface DTO { 2 | guid: string; 3 | name: string; 4 | createTime: Date; 5 | } 6 | export interface OrgDTO extends DTO { 7 | orgId: number; 8 | shortName: string; 9 | displayName: string; 10 | code: string; 11 | indexNumber: string; 12 | url: string; 13 | DunsNumber: string; 14 | isDefault: boolean; 15 | isVisible: boolean; 16 | affintyLevel: number; 17 | firstOrgSchemaId: number; 18 | firstOrgSchemaName: string; 19 | } 20 | 21 | export interface OrgSchemaDTO extends DTO { 22 | orgSchemaId: number; 23 | shortName: string; 24 | displayName: string; 25 | code: string; 26 | indexNumber: string; 27 | isDefault: boolean; 28 | isVisible: boolean; 29 | rootBizEntities: BizEntityDTO[]; 30 | } 31 | 32 | export interface BizEntityDTO extends DTO { 33 | bizEntityId: number; 34 | shortName: string; 35 | displayName: string; 36 | englishName: string; 37 | code: string; 38 | indexNumber: string; 39 | url: string; 40 | DunsNumber: string; 41 | isVisible: boolean; 42 | firstbizEntitySchemaId: number; 43 | firstbizEntitySchemaName: string; 44 | } 45 | 46 | export interface BizEntitySchemaDTO extends DTO { 47 | bizEntitySchemaId: number; 48 | shortName: string; 49 | displayName: string; 50 | englishName: string; 51 | code: string; 52 | indexNumber: string; 53 | isVisible: boolean; 54 | rootDepartments: DepartmentDTO[]; 55 | } 56 | 57 | export interface DepartmentDTO extends DTO { 58 | departmentId: number; 59 | assistBizEntitySchemaId: number; 60 | shortName: string; 61 | displayName: string; 62 | englishName: string; 63 | code: string; 64 | indexNumber: string; 65 | isVisible: boolean; 66 | users: UserDTO[]; 67 | departments: DepartmentDTO[]; 68 | } 69 | 70 | export interface UserDTO extends DTO { 71 | userId: number; 72 | displayName: string; 73 | englishName: string; 74 | code: string; 75 | indexNumber: string; 76 | email: string; 77 | accountInNT: string; 78 | logonName: string; 79 | officeTel: string; 80 | personalTel: string; 81 | personalMobile: string; 82 | isVisible: boolean; 83 | validTimeFrom?: Date; 84 | validTimeTo?: Date; 85 | departmentNames: string[]; 86 | roleNames: string[]; 87 | } 88 | 89 | export enum UserPositionToDepartment { 90 | normal, // 普通人员 91 | manager, // 管理者 92 | temporary, // 临时人员 93 | other, // 其他 94 | } 95 | 96 | export interface DepartmentUserRelation { 97 | departmentUserRelationId: number; 98 | assistDepartmentId: number; 99 | assistUserId: number; 100 | userPosition: UserPositionToDepartment; 101 | createTime: Date; 102 | isValid: boolean; 103 | } 104 | 105 | export interface RoleDTO extends DTO { 106 | roleId: number; 107 | displayName: string; 108 | englishName: string; 109 | code: string; 110 | indexNumber: string; 111 | isVisible: boolean; 112 | users: UserDTO[]; 113 | } 114 | 115 | export interface RoleTypeDTO extends DTO { 116 | roleTypeId: number; 117 | roles: RoleDTO[]; 118 | } 119 | 120 | export interface FlowTemplateDTO extends DTO { 121 | flowTemplateId?: number; 122 | displayName: string; 123 | version: string; 124 | flowTemplateJson: string; 125 | code: string; 126 | indexNumber: string; 127 | isVisible?: boolean; 128 | isValidated?: boolean; 129 | isPublished?: boolean; 130 | } 131 | 132 | export interface FlowDynamicUserDTO extends DTO { 133 | flowDynamicUserId: number; 134 | displayName: string; 135 | memo: string; 136 | isVisible?: boolean; 137 | isValidated?: boolean; 138 | isPublished?: boolean; 139 | } 140 | -------------------------------------------------------------------------------- /src/assets/styles/notie.min.css: -------------------------------------------------------------------------------- 1 | .notie-transition{-webkit-transition:all .3s ease;transition:all .3s ease}.notie-background-success{background-color:#57bf57!important}.notie-background-warning{background-color:#d6a14d!important}.notie-background-error{background-color:#e1715b!important}.notie-background-info{background-color:#4d82d6!important}#notie-alert-outer,#notie-confirm-outer,#notie-date-outer,#notie-force-outer,#notie-input-outer,#notie-select-outer{position:fixed;top:0;left:0;z-index:2;height:auto;width:100%;display:none;text-align:center;cursor:pointer;font-size:24px;-o-box-shadow:0 0 10px 0 rgba(0,0,0,.5);-ms-box-shadow:0 0 10px 0 rgba(0,0,0,.5);box-shadow:0 0 10px 0 rgba(0,0,0,.5)}@media (max-width:600px){#notie-alert-outer,#notie-confirm-outer,#notie-date-outer,#notie-force-outer,#notie-input-outer,#notie-select-outer{font-size:18px}}#notie-alert-inner{padding:20px;display:table-cell}#notie-alert-content{max-width:900px;margin:0 auto}#notie-alert-text{color:#fff}#notie-confirm-outer{cursor:default}#notie-confirm-inner,#notie-force-inner,#notie-input-inner,#notie-select-inner{box-sizing:border-box;width:100%;padding:20px;display:block;cursor:default;background-color:#4d82d6}#notie-confirm-text,#notie-confirm-text-no,#notie-confirm-text-yes{color:#fff}#notie-confirm-no,#notie-confirm-yes,#notie-input-no,#notie-input-yes{float:left;height:50px;line-height:50px;width:50%;cursor:pointer;background-color:#57bf57}#notie-confirm-no,#notie-input-no{float:right;background-color:#e1715b}#notie-confirm-background,#notie-date-background,#notie-force-background,#notie-input-background,#notie-select-background{position:fixed;top:0;left:0;z-index:1;height:100%;width:100%;display:none;background-color:#fff;opacity:0}#notie-force-outer{cursor:default}#notie-force-text{color:#fff}#notie-force-button{height:50px;line-height:50px;width:100%;cursor:pointer;color:#fff}#notie-input-outer{cursor:default}#notie-input-field{display:block;box-sizing:border-box;height:55px;width:100%;text-align:center;outline:0;border:0;background-color:#fff;font-family:inherit;font-size:24px}@media (max-width:600px){#notie-input-field{font-size:18px}}#notie-input-text,#notie-input-text-no,#notie-input-text-yes{color:#fff}#notie-select-outer{top:auto;bottom:0;cursor:default}#notie-select-text{color:#fff}#notie-select-choices,.notie-select-choice{background-color:#57bf57}.notie-select-choice{height:50px;line-height:50px;color:#fff;cursor:pointer}#notie-select-cancel{height:60px;line-height:60px;color:#fff;cursor:pointer;background-color:#a0a0a0}.notie-select-choice-bottom-border{border-bottom:1px solid hsla(0,0%,100%,.2)}#notie-date-outer{background-color:#4d82d6;color:#fff;cursor:default}#notie-date-selector{margin:0 auto;max-width:900px;padding-left:10px;padding-right:10px}.notie-date-down,.notie-date-up{height:50px;float:left;width:33.333333%;cursor:pointer}.notie-date-arrow,.notie-date-arrow-down,.notie-date-arrow-up{height:40px;width:40px;background-size:40px 40px;margin:0 auto;margin-top:5px}.notie-date-arrow-up{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDQ4IDQ4IiB3aWR0aD0iNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE0IDI4bDEwLTEwIDEwIDEweiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNMCAwaDQ4djQ4aC00OHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4=)}.notie-date-arrow-down{background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDQ4IDQ4IiB3aWR0aD0iNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE0IDIwbDEwIDEwIDEwLTEweiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNMCAwaDQ4djQ4aC00OHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4=")}.notie-date-text{height:50px;line-height:50px;float:left;width:33.333333%}#notie-date-no,#notie-date-yes{float:left;width:50%;height:50px;line-height:50px;color:#fff;background-color:#57bf57;cursor:pointer}#notie-date-no{background-color:#e1715b} -------------------------------------------------------------------------------- /src/app/services/webtookit-base64.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base64 encode / decode, 解决window.atob()不支持中文等UTF8编码导致的乱码问题 3 | * http://www.webtoolkit.info/ 4 | * 从文章 https://blog.coding.net/blog/resolve-atob-decode-chinese-character-outputting-messy-code-problem-in-javascript 找到 5 | **/ 6 | import { Injectable } from '@angular/core'; 7 | 8 | @Injectable() 9 | export class MyBase64 { 10 | // private property 11 | private _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 12 | 13 | // public method for encoding 14 | encode(input) { 15 | var output = ""; 16 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 17 | var i = 0; 18 | input = this._utf8_encode(input); 19 | while (i < input.length) { 20 | chr1 = input.charCodeAt(i++); 21 | chr2 = input.charCodeAt(i++); 22 | chr3 = input.charCodeAt(i++); 23 | enc1 = chr1 >> 2; 24 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 25 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 26 | enc4 = chr3 & 63; 27 | if (isNaN(chr2)) { 28 | enc3 = enc4 = 64; 29 | } else if (isNaN(chr3)) { 30 | enc4 = 64; 31 | } 32 | output = output + 33 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + 34 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); 35 | } 36 | return output; 37 | } 38 | 39 | // public method for decoding 40 | decode(input) { 41 | var output = ""; 42 | var chr1, chr2, chr3; 43 | var enc1, enc2, enc3, enc4; 44 | var i = 0; 45 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 46 | while (i < input.length) { 47 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 48 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 49 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 50 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 51 | chr1 = (enc1 << 2) | (enc2 >> 4); 52 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 53 | chr3 = ((enc3 & 3) << 6) | enc4; 54 | output = output + String.fromCharCode(chr1); 55 | if (enc3 != 64) { 56 | output = output + String.fromCharCode(chr2); 57 | } 58 | if (enc4 != 64) { 59 | output = output + String.fromCharCode(chr3); 60 | } 61 | } 62 | output = this._utf8_decode(output); 63 | return output; 64 | } 65 | 66 | // private method for UTF-8 encoding 67 | private _utf8_encode(string) { 68 | string = string.replace(/\r\n/g, "\n"); 69 | var utftext = ""; 70 | for (var n = 0; n < string.length; n++) { 71 | var c = string.charCodeAt(n); 72 | if (c < 128) { 73 | utftext += String.fromCharCode(c); 74 | } 75 | else if ((c > 127) && (c < 2048)) { 76 | utftext += String.fromCharCode((c >> 6) | 192); 77 | utftext += String.fromCharCode((c & 63) | 128); 78 | } 79 | else { 80 | utftext += String.fromCharCode((c >> 12) | 224); 81 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 82 | utftext += String.fromCharCode((c & 63) | 128); 83 | } 84 | } 85 | return utftext; 86 | } 87 | 88 | // private method for UTF-8 decoding 89 | private _utf8_decode(utftext) { 90 | var string = ""; 91 | var i = 0; 92 | var c = 0; 93 | var c1 = 0; 94 | var c2 = 0; 95 | var c3 = 0; 96 | while (i < utftext.length) { 97 | c = utftext.charCodeAt(i); 98 | if (c < 128) { 99 | string += String.fromCharCode(c); 100 | i++; 101 | } 102 | else if ((c > 191) && (c < 224)) { 103 | c2 = utftext.charCodeAt(i + 1); 104 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 105 | i += 2; 106 | } 107 | else { 108 | c2 = utftext.charCodeAt(i + 1); 109 | c3 = utftext.charCodeAt(i + 2); 110 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 111 | i += 3; 112 | } 113 | } 114 | return string; 115 | } 116 | } -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, ModuleWithProviders } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | import { HttpModule } from '@angular/http'; 7 | import * as primeng from 'primeng/primeng'; 8 | import { 9 | createStore, 10 | Store, 11 | compose, 12 | StoreEnhancer 13 | } from 'redux'; 14 | 15 | // 服务 16 | import { FlowTemplateMetaService } from './services/flow-template-meta.service'; 17 | import { PropertyControlService } from './services/property-control.service'; 18 | import { OrgDataService } from './services/org-data.service'; 19 | import { FlowTemplateBackendService } from './services/flow-template-backend.service'; 20 | import { AppConfigService } from './services/app-config.service'; 21 | import {NewFlowTemplateSkeletonService} from './services/new-flow-template-skeleton.service'; 22 | import {LogonBackendService} from './services/logon-backend.service'; 23 | import { MyBase64 } from './services/webtookit-base64.service'; 24 | 25 | // 根组件 26 | import { AppComponent } from './app.component'; 27 | 28 | // 容器型组件 29 | import { HomeComponent } from './containers/home.component'; 30 | import { SlideToolBarComponent } from './containers/slide-tool-bar.component'; 31 | import { FlowFrameComponent } from './containers/flow-frame.component'; 32 | import { FlowTemplateMenuComponent } from './containers/flow-template-menu.component'; 33 | import { PropertyPanelComponent } from './containers/property-panel.component'; 34 | import { TabsetComponent } from './containers/tabset.component'; 35 | import { TabComponent } from './containers/tab.component'; 36 | import { MainMenuComponent } from './containers/main-menu.component'; 37 | 38 | 39 | // 功能型组件 40 | import { ActivityToolComponent } 41 | from './components/activity-tool.component'; 42 | import { FlowChartParentComponent } 43 | from './components/flow-chart-parent.component'; 44 | import { DynamicFlowPropertyControlComponent } from 45 | './components/dynamic-flow-property-control.component'; 46 | import { SimpleModalDialogComponent } from 47 | './components/simple-modal-dialog.component'; 48 | import { RoleUserSelectorComponent } 49 | from './components/role-user-selector.component'; 50 | import { RoleUserDialogComponent } 51 | from './components/role-user-dialog.component'; 52 | import { ConditionRuleManagerComponent } 53 | from './components/condition-rule-manager.component'; 54 | import { ConditionRuleEditorComponent } 55 | from './components/condition-rule-editor.component'; 56 | 57 | 58 | const appRoutes: Routes = [ 59 | { path: '', component: HomeComponent, pathMatch: 'full' } 60 | ]; 61 | 62 | export const appRoutingProviders: any[] = [ 63 | ]; 64 | 65 | export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); 66 | 67 | 68 | 69 | 70 | // 统一数据状态管理 71 | import { AppStore } from './app-store'; 72 | import { 73 | AppState, 74 | default as reducer 75 | } from './reducers'; 76 | 77 | // Chrome Redux调试工具兼容 78 | let devtools: StoreEnhancer = 79 | window['devToolsExtension'] ? 80 | window['devToolsExtension']() : f => f; 81 | 82 | let store: Store = createStore( 83 | reducer, 84 | compose(devtools) 85 | ); 86 | 87 | export function createStoreEx() { 88 | return store; 89 | } 90 | 91 | @NgModule({ 92 | declarations: [ 93 | AppComponent, 94 | SlideToolBarComponent, 95 | ActivityToolComponent, 96 | FlowFrameComponent, 97 | FlowTemplateMenuComponent, 98 | FlowChartParentComponent, 99 | PropertyPanelComponent, 100 | TabsetComponent, 101 | TabComponent, 102 | DynamicFlowPropertyControlComponent, 103 | SimpleModalDialogComponent, 104 | RoleUserSelectorComponent, 105 | ConditionRuleManagerComponent, 106 | RoleUserDialogComponent, 107 | HomeComponent, 108 | ConditionRuleEditorComponent, 109 | MainMenuComponent 110 | ], 111 | imports: [ 112 | primeng.SharedModule, 113 | primeng.DialogModule, 114 | primeng.TabViewModule, 115 | primeng.ButtonModule, 116 | primeng.OrderListModule, 117 | primeng.TabViewModule, 118 | primeng.DataTableModule, 119 | primeng.DataListModule, 120 | primeng.MenubarModule, 121 | primeng.MenuModule, 122 | primeng.ContextMenuModule, 123 | primeng.DropdownModule, 124 | primeng.CheckboxModule, 125 | BrowserModule, 126 | FormsModule, 127 | ReactiveFormsModule, 128 | HttpModule, 129 | CommonModule, 130 | routing 131 | ], 132 | providers: [ 133 | AppConfigService, 134 | NewFlowTemplateSkeletonService, 135 | OrgDataService, 136 | { provide: AppStore, useFactory: createStoreEx }, 137 | FlowTemplateMetaService, 138 | FlowTemplateBackendService, 139 | LogonBackendService, 140 | MyBase64, 141 | PropertyControlService, 142 | appRoutingProviders 143 | ], 144 | bootstrap: [AppComponent] 145 | }) 146 | export class AppModule { } 147 | -------------------------------------------------------------------------------- /src/app/services/flow-template-meta.service.ts: -------------------------------------------------------------------------------- 1 | import * as console from 'console'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { 5 | FlowPropertyBase, 6 | TextboxFlowProperty, 7 | TextareaFlowProperty, 8 | DropdownFlowProperty, 9 | RoleuserlistFlowProperty, 10 | ConditionRuleManagerFlowProperty, 11 | } from '../models/flow-data-def'; 12 | 13 | 14 | @Injectable() 15 | export class FlowTemplateMetaService { 16 | private readonly defs: Object = { 17 | 'Flow.basicInfo': [ 18 | new TextboxFlowProperty({ 19 | name: 'name', 20 | label: '流程名称', 21 | guid: '7493e28f-b34b-418f-b269-c0986d77d39b', 22 | required: true, 23 | readonly: false 24 | }), 25 | new TextboxFlowProperty({ 26 | name: 'code', 27 | label: '流程代码', 28 | guid: '6483e386-b15b-482f-a295-76286d77d39b', 29 | required: true, 30 | readonly: false 31 | }), 32 | new TextboxFlowProperty({ 33 | name: 'displayName', 34 | label: '流程显示名称', 35 | guid: '5493328f-132b-e98f-a9bc-26f86d77d5bf', 36 | required: true, 37 | readonly: false 38 | }), 39 | new TextboxFlowProperty({ 40 | name: 'version', 41 | label: '版本', 42 | guid: 'daf14a8a-6e4f-42de-ba01-23a5bfdca247', 43 | required: true, 44 | readonly: false 45 | }), 46 | new TextareaFlowProperty({ 47 | name: 'desc', 48 | label: '备注说明', 49 | guid: '8c0fdd7e-67af-43c2-8aa3-26f9a4cb8ac3', 50 | required: false, 51 | readonly: false, 52 | rows: 2, 53 | }), 54 | new TextboxFlowProperty({ 55 | name: 'createTime', 56 | label: '创建时间', 57 | guid: 'a58e1fea-378c-41ec-96bf-e71cf01d410a', 58 | required: false, 59 | readonly: true 60 | }), 61 | new DropdownFlowProperty({ 62 | name: 'isPopular', 63 | label: '是否为常用流程?', 64 | guid: '2034e82e-e495-4725-986b-fc29d33ff9eb', 65 | required: false, 66 | readonly: false, 67 | options: [{ text: '否', value: 0 }, { text: '是', value: 1 }] 68 | }), 69 | ], 70 | 'Flow.advancedInfo': [ 71 | new DropdownFlowProperty({ 72 | name: 'arbitraryJumpAllowed', 73 | label: '是否允许随意跳转?', 74 | guid: 'c0588821-8a5f-4747-a565-7f84d51ebb83', 75 | required: false, 76 | readonly: false, 77 | options: [{ text: '否', value: 0 }, { text: '是', value: 1 }] 78 | }), 79 | new TextareaFlowProperty({ 80 | name: 'managers', 81 | label: '流程负责人列表(依序)', 82 | guid: 'a66de058-ee10-41f6-9c69-c2f84d29f22c', 83 | required: false, 84 | readonly: false, 85 | rows: 2, 86 | }), 87 | ], 88 | 'Flow.customData': [], 89 | 'Activity.basicInfo': [ 90 | new TextboxFlowProperty({ 91 | name: 'name', 92 | label: '活动名称', 93 | guid: '07b6bd34-38c3-4cd6-a65f-1c844a8bd7d9', 94 | required: true, 95 | readonly: false 96 | }), 97 | new TextboxFlowProperty({ 98 | name: 'type', 99 | label: '活动类型', 100 | guid: 'cbb98715-dd92-460b-87b5-2b02a84e23ba', 101 | required: true, 102 | readonly: true 103 | }), 104 | new RoleuserlistFlowProperty({ 105 | name: 'roles', 106 | label: '参与人员/角色', 107 | guid: 'f63e59e6-024c-40fb-bffb-1c61d912cf48', 108 | required: false, 109 | readonly: false, 110 | rows: 3, 111 | }), 112 | ], 113 | 'Activity.advancedInfo': [ 114 | new ConditionRuleManagerFlowProperty({ 115 | name: 'autoRules', 116 | label: '自动规则集', 117 | guid: '00c15b73-df35-4691-974e-a5e7ea1cc56e', 118 | required: false, 119 | readonly: false, 120 | rows: 3, 121 | }), 122 | new TextboxFlowProperty({ 123 | name: 'linkForm', 124 | label: '处理表单', 125 | guid: '99d868ca-39f8-4b43-8684-d0169c983aae', 126 | required: false, 127 | readonly: false 128 | }), 129 | new TextareaFlowProperty({ 130 | name: 'beforeActions', 131 | label: '前置处理组件集', 132 | guid: 'f1869f77-f36d-4470-861c-56e559622c39', 133 | required: false, 134 | readonly: false 135 | }), 136 | new TextareaFlowProperty({ 137 | name: 'afterActions', 138 | label: '后置处理组件集', 139 | guid: '56198d9a-36e9-4833-bb65-355ddd76f22f', 140 | required: false, 141 | readonly: false 142 | }), 143 | ], 144 | 'Connection.basicInfo': [ 145 | new TextboxFlowProperty({ 146 | name: 'name', 147 | label: '连接名称', 148 | guid: '6ce1d57a-83b3-4946-9fd5-c4bb877b3a8a', 149 | required: true, 150 | readonly: false 151 | }), 152 | new DropdownFlowProperty({ 153 | name: 'showName', 154 | label: '是否在连接上显示名称?', 155 | guid: 'bb52f186-2dd5-499b-a844-37c4b0f86b10', 156 | required: false, 157 | readonly: false, 158 | options: [{ text: '否', value: 0 }, { text: '是', value: 1 }] 159 | }), 160 | ], 161 | 'Connection.advancedInfo': [ 162 | new TextboxFlowProperty({ 163 | name: 'beforeActions', 164 | label: '前置处理组件集', 165 | guid: 'ce13fdf1-4907-4aab-881a-09dbcfe6bda5', 166 | required: false, 167 | readonly: false 168 | }), 169 | new TextboxFlowProperty({ 170 | name: 'afterActions', 171 | label: '后置处理组件集', 172 | guid: '4b3bee66-1b4e-444f-8469-f64b4bee3d3d', 173 | required: false, 174 | readonly: false 175 | }), 176 | ], 177 | }; 178 | 179 | constructor() { 180 | 181 | } 182 | 183 | getPropertyDefinitions(defName: string) { 184 | if (this.defs.hasOwnProperty(defName)) { 185 | return this.defs[defName]; 186 | } else { 187 | throw new Error(`试图获取不存在的流程模板元数据定义集 '${defName}'`); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/app/services/org-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response, Headers } from '@angular/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AppConfigService } from './app-config.service'; 6 | import { 7 | OrgDTO, OrgSchemaDTO, 8 | BizEntityDTO, BizEntitySchemaDTO, 9 | DepartmentDTO, RoleDTO, UserDTO, RoleTypeDTO, 10 | DepartmentUserRelation, UserPositionToDepartment, 11 | FlowDynamicUserDTO 12 | } 13 | from '../models/master-data-def'; 14 | 15 | @Injectable() 16 | export class OrgDataService { 17 | private headers = new Headers({ 'Content-Type': 'application/json' }); 18 | 19 | constructor(private http: Http, 20 | private appConfig: AppConfigService) { } 21 | 22 | getOrgs(): Observable { 23 | return this.http 24 | .get(this.appConfig.serverAddress + 'api/OM_Org', 25 | { headers: this.headers }) 26 | .map((r: Response) => { 27 | this.outputMonitorLog(r.json()); 28 | return r.json() as OrgDTO[]; 29 | }) 30 | .catch(error => { 31 | console.error(error); 32 | return Observable.of([]); 33 | }); 34 | } 35 | 36 | getOrgSchmas(orgId: number): Observable { 37 | return this.http 38 | .get(this.appConfig.serverAddress + 'api/OM_Org/GetOrgSchemas/' + orgId, 39 | { headers: this.headers }) 40 | .map((r: Response) => { 41 | this.outputMonitorLog(r.json()); 42 | return r.json() as OrgSchemaDTO[]; 43 | }) 44 | .catch(error => { 45 | console.error(error); 46 | return Observable.of([]); 47 | }); 48 | } 49 | 50 | getBizEntity(bizEntityId: number): Observable { 51 | return this.http 52 | .get(this.appConfig.serverAddress + 53 | 'api/OM_BizEntity/' + bizEntityId, 54 | { headers: this.headers }) 55 | .map((r: Response) => { 56 | this.outputMonitorLog(r.json()); 57 | return r.json() as BizEntityDTO; 58 | }) 59 | .catch(error => { 60 | console.error(error); 61 | return Observable.of(null); 62 | }); 63 | } 64 | 65 | getBizEntityShema(bizEntitySchemaId: number): 66 | Observable { 67 | return this.http 68 | .get(this.appConfig.serverAddress + 69 | 'api/OM_BizEntitySchema/' + bizEntitySchemaId, 70 | { headers: this.headers }) 71 | .map((r: Response) => { 72 | this.outputMonitorLog(r.json()); 73 | return r.json() as BizEntitySchemaDTO; 74 | }) 75 | .catch(error => { 76 | console.error(error); 77 | return Observable.of(null); 78 | }); 79 | } 80 | 81 | getDepartment(departmentId: number): Observable { 82 | return this.http 83 | .get(this.appConfig.serverAddress + 84 | 'api/OM_Department/' + departmentId, 85 | { headers: this.headers }) 86 | .map((r: Response) => { 87 | this.outputMonitorLog(r.json()); 88 | return r.json() as DepartmentDTO; 89 | }) 90 | .catch(error => { 91 | console.error(error); 92 | return Observable.of(null); 93 | }); 94 | } 95 | 96 | getUser(userId: number): Observable { 97 | return this.http 98 | .get(this.appConfig.serverAddress + 99 | 'api/OM_User/' + userId, 100 | { headers: this.headers }) 101 | .map((r: Response) => { 102 | this.outputMonitorLog(r.json()); 103 | return r.json() as UserDTO; 104 | }) 105 | .catch(error => { 106 | console.error(error); 107 | return Observable.of(null); 108 | }); 109 | } 110 | 111 | getAllValidUsers(): Observable { 112 | return this.http 113 | .get(this.appConfig.serverAddress + 'api/OM_User/', 114 | { headers: this.headers }) 115 | .map((r: Response) => { 116 | this.outputMonitorLog(r.json()); 117 | return r.json() as UserDTO[]; 118 | }) 119 | .catch(error => { 120 | console.error(error); 121 | return Observable.of(null); 122 | }); 123 | } 124 | 125 | getUser_DepartmentUserRelations(userId: number): 126 | Observable { 127 | return this.http 128 | .get(this.appConfig.serverAddress + 129 | 'api/OM_User/GetDepartmentUserRelations/' + userId, 130 | { headers: this.headers }) 131 | .map((r: Response) => { 132 | this.outputMonitorLog(r.json()); 133 | return r.json() as DepartmentUserRelation[]; 134 | }) 135 | .catch(error => { 136 | console.error(error); 137 | return Observable.of(null); 138 | }); 139 | } 140 | 141 | getRoles(): Observable { 142 | return this.http 143 | .get(this.appConfig.serverAddress + 'api/OM_Role', 144 | { headers: this.headers }) 145 | .map((r: Response) => { 146 | this.outputMonitorLog(r.json()); 147 | return r.json() as RoleDTO[]; 148 | }) 149 | .catch(error => { 150 | console.error(error); 151 | return Observable.of([]); 152 | }); 153 | } 154 | 155 | getRole(roleId: number): Observable { 156 | return this.http 157 | .get(this.appConfig.serverAddress + 158 | 'api/OM_Role/' + roleId, 159 | { headers: this.headers }) 160 | .map((r: Response) => { 161 | this.outputMonitorLog(r.json()); 162 | return r.json() as RoleDTO; 163 | }) 164 | .catch(error => { 165 | console.error(error); 166 | return Observable.of(null); 167 | }); 168 | } 169 | 170 | getFlowDynamicUsers(): Observable { 171 | return this.http 172 | .get(this.appConfig.serverAddress + 'api/FlowDynamicUser', 173 | { headers: this.headers }) 174 | .map((r: Response) => { 175 | this.outputMonitorLog(r.json()); 176 | return r.json() as FlowDynamicUserDTO[]; 177 | }) 178 | .catch(error => { 179 | console.error(error); 180 | return Observable.of([]); 181 | }); 182 | } 183 | 184 | outputMonitorLog(data) { 185 | if (this.appConfig.logCommunication) { 186 | console.log(data); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/app/models/flow-data-def.ts: -------------------------------------------------------------------------------- 1 | import { genGuid } from './../utilites/gen-guid'; 2 | import { UserDTO, RoleDTO, FlowDynamicUserDTO } 3 | from './master-data-def'; 4 | 5 | export interface ActivityConnectionData { //节点间连线数据对象 6 | guid: string; 7 | fromGuid: string; //起始端节点数据对象的GUID 8 | toGuid: string; //结束端节点数据对象的GUID 9 | name?: string; 10 | } 11 | 12 | export class ActivityConnectionDataObj 13 | implements ActivityConnectionData { 14 | guid: string; 15 | fromGuid: string; //起始端节点数据对象的GUID 16 | toGuid: string; //结束端节点数据对象的GUID 17 | name?: string; 18 | 19 | constructor(fromGuid: string, toGuid: string, 20 | guid?: string, name?: string) { 21 | this.guid = guid || genGuid(); 22 | this.fromGuid = fromGuid; 23 | this.toGuid = toGuid; 24 | this.name = name || '未命名连接' + Date.now().toString(); 25 | } 26 | } 27 | 28 | export interface ActivityNodeData { //节点数据对象 29 | type: string; 30 | guid: string; 31 | name: string; 32 | size: number[]; 33 | position: number[]; 34 | initData?: Object; 35 | linkForm?: Object; 36 | beforeActions?: any[]; 37 | afterActions?: any[]; 38 | roles?: Paticipant[]; 39 | autoRules?: ConditionRule[]; 40 | } 41 | 42 | export class ActivityNodeDataObj implements ActivityNodeData { //节点数据对象 43 | type: string; 44 | guid: string; 45 | name: string; 46 | size: number[]; 47 | position: number[]; 48 | initData?: Object; 49 | linkForm?: Object; 50 | beforeActions?: any[]; 51 | afterActions?: any[]; 52 | roles?: Paticipant[]; 53 | autoRules?: ConditionRule[]; 54 | 55 | 56 | constructor(type: string, position: number[], size?: number[], 57 | guid?: string, name?: string, initData?: Object, 58 | linkForm?: Object, beforeActions?: any[], 59 | afterActions?: any[], roles?: any[], autoRules?: ConditionRule[]) { 60 | this.type = type; 61 | //TODO-需要加入对type的合法值判断 62 | this.guid = guid || genGuid(); 63 | this.position = position; 64 | this.name = name || '未命名 ' + type; 65 | this.size = size || [50, 50]; 66 | this.initData = initData; 67 | this.linkForm = linkForm; 68 | this.beforeActions = beforeActions; 69 | this.afterActions = afterActions; 70 | this.roles = roles; 71 | this.autoRules = autoRules; 72 | } 73 | } 74 | 75 | export interface PersonShortInfo { 76 | name: string; 77 | guid: string; 78 | } 79 | 80 | export interface FlowBasicInfoData { 81 | name: string; 82 | version: string; 83 | guid: string; 84 | displayName: string; 85 | code: string; 86 | desc: string; 87 | creator: PersonShortInfo; 88 | createTime: string; 89 | lastUpdateTime: string; 90 | } 91 | 92 | export interface FlowAdvancedInfoData { 93 | } 94 | 95 | // export interface FlowConfigInfoData { 96 | // } 97 | 98 | // export interface FlowPreferenceData { 99 | // } 100 | 101 | export interface FlowCustomInfoData { 102 | } 103 | 104 | export interface FlowData { 105 | basicInfo: FlowBasicInfoData; 106 | customData?: FlowCustomInfoData; 107 | advancedInfo?: FlowAdvancedInfoData; 108 | activityNodes: { nodes: ActivityNodeData[] }; 109 | activityConnections: { connections: ActivityConnectionData[] }; 110 | // configInfo?: FlowConfigInfoData; 111 | // preferences?: FlowPreferenceData; 112 | } 113 | 114 | export interface ActivityTool { 115 | isSelected: boolean; 116 | activityType: string; 117 | activityToolName: string; 118 | imagePath: string; 119 | tooltip?: string; 120 | } 121 | 122 | export interface ActivityToolSet { 123 | name: string; 124 | display: string; 125 | configData?: any; 126 | tools: ActivityTool[]; 127 | } 128 | 129 | export abstract class FlowPropertyBase{ 130 | name: string; 131 | value: T; 132 | guid: string; 133 | label: string; 134 | required: boolean; 135 | order: number; 136 | controlType: string; 137 | placeholder: string; 138 | readonly: boolean; 139 | 140 | constructor(options: { 141 | name?: string, 142 | value?: T, 143 | guid?: string, 144 | label?: string, 145 | required?: boolean, 146 | order?: number, 147 | controlType?: string, 148 | placeholder?: string, 149 | readonly?: boolean 150 | } = {}) { 151 | this.name = options.name; 152 | this.value = options.value; 153 | this.guid = options.guid || ''; 154 | this.label = options.label || ''; 155 | this.required = !!options.required; 156 | this.order = options.order === undefined ? 1 : options.order; 157 | this.controlType = options.controlType || ''; 158 | this.placeholder = options.placeholder || ''; 159 | this.readonly = !!options.readonly; 160 | } 161 | } 162 | 163 | export class TextboxFlowProperty extends FlowPropertyBase { 164 | controlType = 'text'; 165 | 166 | constructor(options: {} = { name: '未命名文本属性' }) { 167 | super(options); 168 | } 169 | } 170 | 171 | export class DropdownFlowProperty extends FlowPropertyBase { 172 | controlType = 'dropdown'; 173 | options: { text: string, value: string }[] = []; 174 | 175 | constructor(options: {} = { name: '未命名下拉框属性' }) { 176 | super(options); 177 | this.options = options['options'] || []; 178 | } 179 | } 180 | 181 | export class TextareaFlowProperty extends FlowPropertyBase { 182 | controlType = 'textarea'; 183 | rows: number; 184 | 185 | constructor(options: {} = { name: '未命名长文本属性' }) { 186 | super(options); 187 | this.rows = options['rows'] || 3; 188 | } 189 | } 190 | 191 | export interface Paticipant { 192 | PaticipantType: string; // 'role' or 'user' or 'dynamic' 193 | PaticipantObj: RoleDTO | UserDTO | FlowDynamicUserDTO; 194 | } 195 | 196 | export class RoleuserlistFlowProperty 197 | extends FlowPropertyBase { 198 | controlType = 'role-user-selector'; 199 | rows: number; 200 | 201 | constructor(options: {} = { name: '未命名角色用户列表属性' }) { 202 | super(options); 203 | this.rows = options['rows'] || 3; 204 | this.value = []; 205 | } 206 | } 207 | 208 | export interface ConditionRule { 209 | name: string; 210 | guid: string; 211 | isDefault: boolean; 212 | code: string; // C# code to evaluate 213 | connectionGuid: string; 214 | paticipants: Paticipant[]; 215 | } 216 | 217 | export class ConditionRuleManagerFlowProperty 218 | extends FlowPropertyBase { 219 | controlType = 'condition-rule-manager'; 220 | rows: number; 221 | 222 | constructor(options: {} = { name: '未命名自动规则列表属性' }) { 223 | super(options); 224 | this.rows = options['rows'] || 3; 225 | this.value = []; 226 | } 227 | } -------------------------------------------------------------------------------- /src/app/components/condition-rule-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, OnInit, Inject, Input, forwardRef, 3 | EventEmitter, Output, OnChanges, SimpleChange 4 | } 5 | from '@angular/core'; 6 | 7 | import { 8 | FormGroup, FormControl, FormsModule, 9 | ControlValueAccessor, NG_VALUE_ACCESSOR, 10 | Validator, NG_VALIDATORS, 11 | } from '@angular/forms'; 12 | 13 | import { Store } from 'redux'; 14 | 15 | import { 16 | DialogModule, DataTableModule, DataListModule, DropdownModule, 17 | SharedModule, CheckboxModule, SelectItem, Dropdown 18 | } 19 | from 'primeng/primeng'; 20 | import * as _ from 'lodash'; 21 | 22 | import { genGuid } from './../utilites/gen-guid'; 23 | import { UserDTO, RoleDTO } 24 | from '../models/master-data-def'; 25 | import { Paticipant, ConditionRule, ActivityNodeData } 26 | from '../models/flow-data-def'; 27 | 28 | import { FlowTemplateBackendService } 29 | from '../services/flow-template-backend.service'; 30 | import { MyBase64 } 31 | from '../services/webtookit-base64.service'; 32 | 33 | import { AppStore } from '../app-store'; 34 | 35 | import { 36 | AppState, 37 | } from '../reducers'; 38 | 39 | @Component({ 40 | selector: 'app-condition-rule-editor', 41 | template: ` 42 | 45 | 46 | 48 |

50 | 51 | 52 | 54 |

55 | 56 | 57 |
59 | 60 | 61 | 62 | 目标角色与用户列表 63 | 71 | 72 | 75 | 76 | 77 | 78 | 81 |
82 | 83 | 84 |
85 | 87 | 89 |
90 |
91 | ` 92 | }) 93 | export class ConditionRuleEditorComponent implements OnInit { 94 | 95 | @Input() initialRule: ConditionRule; 96 | @Output() private valueChanged = new EventEmitter(); 97 | @Output() private editorClosed = new EventEmitter(); 98 | 99 | rule: ConditionRule; 100 | ruleName: string; 101 | isDefault = false; 102 | code: string; // base64 encoded 103 | codeDecoded: string; // base64 decoded 104 | displayRoleUserDialog = false; 105 | roleAndUsers: Paticipant[]; 106 | displayDialog = true; 107 | connections: SelectItem[]; 108 | selectedConnection: any; 109 | allActivitys: ActivityNodeData[]; 110 | 111 | constructor( @Inject(AppStore) private store: Store, 112 | private base64Service: MyBase64) { 113 | const _state = this.store.getState(); 114 | this.allActivitys = _state.activityDataNodes.activityNodeDatas; 115 | } 116 | 117 | ngOnInit() { 118 | // 建立可选的当前活动节点出口连接列表 119 | this.initConnectionsDropDown(); 120 | 121 | if (this.initialRule) { 122 | this.selectedConnection = this.initialRule.connectionGuid; 123 | this.ruleName = this.initialRule.name; 124 | this.code = this.initialRule.code; 125 | this.codeDecoded = this.base64Service.decode(this.code); 126 | this.roleAndUsers = this.initialRule.paticipants; 127 | this.isDefault = this.initialRule.isDefault; 128 | } else { 129 | this.ruleName = '未命名规则' + Date.now().toString(); 130 | this.isDefault = false; 131 | this.codeDecoded = `//规则判断与执行脚本代码, 'return true;' 代表该规则将适用. 132 | var i = 0`; 133 | this.code = this.base64Service.encode(this.codeDecoded); 134 | } 135 | 136 | } 137 | 138 | confirmed() { 139 | if (this.initialRule) { 140 | this.rule = this.initialRule; 141 | } else { 142 | this.rule = { guid: genGuid() }; 143 | } 144 | const rule = this.rule; 145 | rule.name = this.ruleName; 146 | rule.isDefault = this.isDefault; 147 | rule.code = this.base64Service.encode(this.codeDecoded); 148 | rule.connectionGuid = this.selectedConnection; 149 | rule.paticipants = this.roleAndUsers; 150 | // console.info(rule); 151 | this.valueChanged.emit(rule); 152 | this.editorClosed.emit(); 153 | } 154 | 155 | canceled() { 156 | this.editorClosed.emit(); 157 | } 158 | 159 | // 以下为角色用户列表处理 160 | showRoleUserDialog() { 161 | this.displayRoleUserDialog = true; 162 | 163 | } 164 | updateRoleAndUsers(event) { 165 | this.roleAndUsers = event; 166 | this.displayRoleUserDialog = false; 167 | } 168 | roleuserDialogClosed() { 169 | this.displayRoleUserDialog = false; 170 | } 171 | 172 | // 以下为辅助函数 173 | validate(): boolean { 174 | return this.ruleName && this.codeDecoded && this.selectedConnection 175 | && this.roleAndUsers && this.roleAndUsers.length > 0; 176 | } 177 | private initConnectionsDropDown() { 178 | const _state = this.store.getState(); 179 | const currentNodeGuid = 180 | _state.activityDataNodes.currentNodeGuid; 181 | const _allConns = 182 | _state.activityConnections.activityConnectionDatas; 183 | const _potentialConnections = _.filter(_allConns, 184 | (_conn) => { return _conn.fromGuid === currentNodeGuid; }); 185 | this.connections = []; 186 | this.connections.push({ label: '选择连接', value: null }); 187 | this.connections.push(..._.map(_potentialConnections, (_conn) => { 188 | return { 189 | label: _conn.name + ' -> ' + 190 | this.getActivityNodeNameFromGuid(_conn.toGuid), 191 | value: _conn.guid 192 | }; 193 | })); 194 | } 195 | private getActivityNodeNameFromGuid(guid: string): string { 196 | return _.find(this.allActivitys, node => { 197 | return node.guid === guid; 198 | }).name; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/app/components/condition-rule-manager.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Inject, forwardRef } from '@angular/core'; 2 | import { 3 | FormGroup, FormControl, 4 | ControlValueAccessor, NG_VALUE_ACCESSOR, 5 | Validator, NG_VALIDATORS, 6 | } from '@angular/forms'; 7 | import { 8 | OrderListModule, DialogModule, TabViewModule, DataTableModule, 9 | ContextMenuModule, MenuItem 10 | } from 'primeng/primeng'; 11 | import * as _ from 'lodash'; 12 | import { Store } from 'redux'; 13 | 14 | import { UserDTO, RoleDTO } 15 | from '../models/master-data-def'; 16 | 17 | import { Paticipant, ConditionRule, ActivityConnectionData } from '../models/flow-data-def'; 18 | 19 | import { OrgDataService } from '../services/org-data.service'; 20 | import { FlowTemplateBackendService } 21 | from '../services/flow-template-backend.service'; 22 | 23 | import { AppStore } from '../app-store'; 24 | 25 | import { 26 | AppState, 27 | } from '../reducers'; 28 | 29 | @Component({ 30 | selector: 'app-condition-rule-manager', 31 | template: ` 32 | 34 | 42 | 43 | 47 | 48 | 51 | 52 | 55 | 56 | 57 | 60 | 61 | 62 | 65 | 66 | 67 | 70 | 71 | 72 | 73 |
74 | 76 | 79 | 81 | 83 | 84 |
85 |
86 | 87 |
88 | 89 | 90 |
91 | 92 | 96 | 97 | `, 98 | providers: [ 99 | { 100 | provide: NG_VALUE_ACCESSOR, 101 | useExisting: forwardRef(() => ConditionRuleManagerComponent), 102 | multi: true, 103 | }, 104 | ] 105 | }) 106 | export class ConditionRuleManagerComponent 107 | implements OnInit, ControlValueAccessor { 108 | 109 | @Input() required: boolean = false; 110 | // @Input() 111 | 112 | // 缓存数组用于获取 113 | allActivityConnections: ActivityConnectionData[]; 114 | rules: ConditionRule[] = []; 115 | rulesToChange: ConditionRule[] = []; 116 | displayRulesDialog: boolean = false; 117 | displaySingleRuleDialog: boolean = false; 118 | menuItems: MenuItem[]; 119 | selectedRule: ConditionRule; 120 | ruleToEdit: ConditionRule; 121 | 122 | showDialog() { 123 | this.displayRulesDialog = true; 124 | } 125 | 126 | showChildDialog() { 127 | this.displaySingleRuleDialog = true; 128 | } 129 | 130 | onReorder() { 131 | this.propagateChange(this.rules); 132 | } 133 | 134 | showDialogToAdd() { 135 | this.ruleToEdit = null; 136 | this.displaySingleRuleDialog = true; 137 | } 138 | 139 | confirm() { 140 | this.rulesToChange = this.rules; 141 | this.displayRulesDialog = false; 142 | } 143 | 144 | cancel() { 145 | this.displayRulesDialog = false; 146 | } 147 | 148 | newOrUpdateRule(rule: ConditionRule) { 149 | let oldRuleIndex = _.findIndex(this.rules, _rule => { 150 | return _rule.guid == rule.guid; 151 | }); 152 | if (oldRuleIndex >= 0) { // Replace 153 | this.rules.splice(oldRuleIndex, 1, rule); 154 | } else { // Push to the tail 155 | this.rules.push(rule); 156 | } 157 | this.propagateChange(this.rules); 158 | } 159 | 160 | editorClosed() { 161 | this.displaySingleRuleDialog = false; 162 | } 163 | 164 | checkCodes() { 165 | notie.alert(3, "尚未实现!", 3); 166 | } 167 | 168 | constructor( @Inject(AppStore) private store: Store, ) { } 169 | 170 | ngOnInit() { 171 | this.menuItems = [ 172 | { 173 | label: '编辑', icon: 'fa-edit', command: (event) => { 174 | this.ruleToEdit = this.selectedRule; 175 | this.displaySingleRuleDialog = true; 176 | } 177 | }, 178 | { 179 | label: '删除', icon: 'fa-close', command: (event) => { 180 | this.rules.splice(this.rules.findIndex(rule => { 181 | return rule.guid == this.selectedRule.guid; 182 | }), 1); 183 | this.propagateChange(this.rules); 184 | } 185 | }, 186 | { 187 | label: '上移', icon: 'fa-level-up', command: (event) => { 188 | let idx = this.rules.findIndex(rule => { 189 | return rule.guid == this.selectedRule.guid; 190 | }); 191 | if (idx != 0) { 192 | let rule = this.rules.splice(idx, 1)[0]; 193 | this.rules.splice(idx - 1, 0, rule); 194 | this.propagateChange(this.rules); 195 | } 196 | } 197 | }, 198 | { 199 | label: '下移', icon: 'fa-level-down', command: (event) => { 200 | let idx = this.rules.findIndex(rule => { 201 | return rule.guid == this.selectedRule.guid; 202 | }); 203 | if (idx != this.rules.length - 1) { 204 | let rule = this.rules.splice(idx, 1)[0]; 205 | this.rules.splice(idx + 1, 0, rule); 206 | this.propagateChange(this.rules); 207 | } 208 | } 209 | } 210 | ]; 211 | 212 | const _state = this.store.getState(); 213 | this.allActivityConnections = _state.activityConnections. 214 | activityConnectionDatas; 215 | } 216 | 217 | // 以下为ControlValueAccessor的实现,具体阅读实现参考/|\ 218 | private propagateChange = (_: any) => { }; 219 | public writeValue(obj: any) { 220 | if (obj) { 221 | this.rulesToChange = obj; 222 | this.rules = _.cloneDeep(this.rulesToChange); 223 | } 224 | } 225 | get value(): any { 226 | return this.rulesToChange; 227 | }; 228 | set value(v: any) { 229 | if (v !== this.rulesToChange) { 230 | this.rulesToChange = v; 231 | this.rules = _.cloneDeep(this.rulesToChange); 232 | this.propagateChange(v); 233 | } 234 | } 235 | public registerOnChange(fn: any) { 236 | this.propagateChange = fn; 237 | } 238 | public registerOnTouched() { } 239 | 240 | // 以下为辅助函数 241 | private getConnectionNameFromGuid(guid: string): string { 242 | let _conn = _.find(this.allActivityConnections, conn => { 243 | return conn.guid === guid; 244 | }); 245 | return (_conn && _conn.name) || ''; 246 | } 247 | 248 | private getPaticipantsInfo(paticipants: Paticipant[]): string { 249 | return paticipants.map(p => p.PaticipantObj.name).reduce( 250 | (prev, current) => { return prev + current + "; " }, "" 251 | ); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/app/containers/flow-template-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject, ElementRef } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | import { Store } from 'redux'; 4 | 5 | import { OrderListModule, DialogModule, TabViewModule, ButtonModule } 6 | from 'primeng/primeng'; 7 | import * as $ from 'jquery'; 8 | import * as _ from 'lodash'; 9 | import * as moment from 'moment'; 10 | 11 | import { genGuid } from '../utilites/gen-guid'; 12 | import { formatJson } from '../utilites/format-json'; 13 | 14 | import { 15 | FlowData, 16 | ActivityConnectionData, 17 | ActivityNodeData, 18 | ActivityNodeDataObj, 19 | FlowPropertyBase 20 | } from '../models/flow-data-def'; 21 | 22 | import { FlowTemplateDTO } from '../models/master-data-def'; 23 | 24 | import { FlowTemplateMetaService } from 25 | '../services/flow-template-meta.service'; 26 | import { PropertyControlService } from 27 | '../services/property-control.service'; 28 | import { NewFlowTemplateSkeletonService } from 29 | '../services/new-flow-template-skeleton.service'; 30 | import { FlowTemplateBackendService } from 31 | '../services/flow-template-backend.service'; 32 | import { MyBase64 } from 33 | '../services/webtookit-base64.service'; 34 | 35 | import { SimpleModalDialogComponent } from '../components/simple-modal-dialog.component'; 36 | 37 | import { AppStore } from '../app-store'; 38 | 39 | import { 40 | ActivityNodeDataActions, 41 | ActivityConnectionDataActions, 42 | FlowDataActions 43 | } from '../actions'; 44 | 45 | import { 46 | AppState, 47 | } from '../reducers'; 48 | 49 | enum CurrentOperation { 50 | import = 1, 51 | export, 52 | } 53 | 54 | @Component({ 55 | selector: 'app-flow-template-menu', 56 | template: ` 57 | 100 | ` 101 | }) 102 | export class FlowTemplateMenuComponent implements OnInit { 103 | private currentOperation: CurrentOperation; 104 | private formattedFlowData: string; 105 | public taValue: string; 106 | public showDlg = false; 107 | public showChooseTemplatesDlg = false; 108 | public flowTemplates: FlowTemplateDTO[]; 109 | public selectedTemplate: FlowTemplateDTO; 110 | 111 | constructor( 112 | @Inject(AppStore) private store: Store, 113 | @Inject(NewFlowTemplateSkeletonService) 114 | private newFlowTemplateSkeletonService: 115 | NewFlowTemplateSkeletonService, 116 | private flowTemplateBackendService: 117 | FlowTemplateBackendService) {} 118 | 119 | ngOnInit() { 120 | } 121 | 122 | create() { 123 | let _flowData = this.newFlowTemplateSkeletonService. 124 | getNewFlowTemplateSkeletonObj(); 125 | this.updateFlowDataToStore(_flowData); 126 | } 127 | 128 | export() { 129 | const _state = this.store.getState(); 130 | let _flowData = this.cloneFlowData(); 131 | this.taValue = formatJson(JSON.stringify(_flowData)); 132 | this.showDlg = true; 133 | this.currentOperation = CurrentOperation.export; 134 | } 135 | 136 | import() { 137 | this.taValue = ''; 138 | this.showDlg = true; 139 | this.currentOperation = CurrentOperation.import; 140 | } 141 | 142 | canceled() { 143 | this.showDlg = false; 144 | } 145 | 146 | confirmed() { 147 | if (this.currentOperation === CurrentOperation.import && 148 | this.taValue) { 149 | // 确认导入流程模板数据 150 | let _flowData; 151 | try { 152 | _flowData = JSON.parse(this.taValue); 153 | } catch (e) { 154 | notie.alert(3, `导入流程模板数据失败:${e}`, 10); 155 | return; 156 | } 157 | 158 | this.updateFlowDataToStore(_flowData); 159 | 160 | this.showDlg = false; 161 | } 162 | } 163 | 164 | open() { 165 | this.flowTemplateBackendService.getFlowTemplates().toPromise() 166 | .then(value => { 167 | this.flowTemplates = value; 168 | this.showChooseTemplatesDlg = true; 169 | }); 170 | } 171 | 172 | createInBackend() { 173 | let _flowData = this.cloneFlowData(); 174 | _flowData.basicInfo.guid = genGuid(); 175 | this.flowTemplateBackendService.newFlowTemplate({ 176 | guid: _flowData.basicInfo.guid, 177 | name: _flowData.basicInfo.name, 178 | createTime: new Date(Date.now()), 179 | displayName: _flowData.basicInfo.displayName, 180 | version: _flowData.basicInfo.version, 181 | flowTemplateJson: JSON.stringify(_flowData), 182 | code: _flowData.basicInfo.code, 183 | indexNumber: '0', 184 | }).toPromise() 185 | .then(value => { 186 | // console.info(value); 187 | if (value.flowTemplateId) { // 成功创建 188 | 189 | } else { 190 | notie.alert(3, `保存流程模板数据失败:${value['_body']}`, 20); 191 | } 192 | }); 193 | } 194 | 195 | updateInBackend() { 196 | let _flowData = this.cloneFlowData(); 197 | this.flowTemplateBackendService.updateFlowTemplate({ 198 | guid: _flowData.basicInfo.guid, 199 | name: _flowData.basicInfo.name, 200 | createTime: new Date(Date.now()), 201 | displayName: _flowData.basicInfo.displayName, 202 | version: _flowData.basicInfo.version, 203 | flowTemplateJson: JSON.stringify(_flowData), 204 | code: _flowData.basicInfo.code, 205 | indexNumber: '0', 206 | }).toPromise() 207 | .then(value => { 208 | // console.info(value); 209 | }); 210 | } 211 | 212 | confirmOpenTemplate() { 213 | let _flowData; 214 | let _flowTemplateJson: string; 215 | try { 216 | _flowTemplateJson = this.selectedTemplate.flowTemplateJson; 217 | _flowData = JSON.parse(_flowTemplateJson); 218 | this.updateFlowDataToStore(_flowData); 219 | this.showChooseTemplatesDlg = false; 220 | } catch (e) { 221 | // console.info(_flowTemplateJson); 222 | notie.alert(3, `导入流程模板数据失败:${e}`, 10); 223 | return; 224 | } 225 | this.selectedTemplate = null; 226 | } 227 | 228 | cancelOpenTemplate() { 229 | this.selectedTemplate = null; 230 | this.showChooseTemplatesDlg = false; 231 | } 232 | 233 | private updateFlowDataToStore(flowData: FlowData) { 234 | this.store.dispatch( 235 | FlowDataActions.populateFlowData(flowData)); 236 | 237 | this.store.dispatch( 238 | ActivityNodeDataActions.populateNodesData( 239 | flowData.activityNodes.nodes 240 | ) 241 | ); 242 | 243 | this.store.dispatch( 244 | ActivityConnectionDataActions.populateConnectionsData( 245 | flowData.activityConnections.connections 246 | ) 247 | ); 248 | } 249 | 250 | private cloneFlowData(): FlowData { 251 | const _state = this.store.getState(); 252 | let _flowData = {}; 253 | _flowData['basicInfo'] = _state.flowData.flowData.basicInfo; 254 | _flowData['advancedInfo'] = _state.flowData.flowData.advancedInfo; 255 | _flowData['customData'] = {}; 256 | _flowData['activityNodes'] = {}; 257 | _flowData['activityNodes']['nodes'] = _state.activityDataNodes.activityNodeDatas; 258 | _flowData['activityConnections'] = {}; 259 | _flowData['activityConnections']['connections'] = 260 | _state.activityConnections.activityConnectionDatas; 261 | 262 | return _flowData; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/app/components/role-user-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, OnInit, Input, forwardRef, 3 | EventEmitter, Output, OnChanges, SimpleChange 4 | } 5 | from '@angular/core'; 6 | import { 7 | FormGroup, FormControl, 8 | ControlValueAccessor, NG_VALUE_ACCESSOR, 9 | Validator, NG_VALIDATORS, 10 | } from '@angular/forms'; 11 | import { OrderListModule, DialogModule, TabViewModule } from 'primeng/primeng'; 12 | import * as _ from 'lodash'; 13 | 14 | import { UserDTO, RoleDTO, FlowDynamicUserDTO } 15 | from '../models/master-data-def'; 16 | 17 | import { Paticipant } from '../models/flow-data-def'; 18 | 19 | import { OrgDataService } from '../services/org-data.service'; 20 | 21 | @Component({ 22 | selector: 'app-role-user-dialog', 23 | template: ` 24 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 |
姓名英文名 代码用户列表
{{role.name}}{{role.englishName}}{{role.code}}{{getRoleUsers(role)}}
47 |
48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 |
姓名英文名 工号部门角色列表
{{user.name}}{{user.englishName}}{{user.code}}{{user.departmentNames}}{{user.roleNames}}
69 |
70 |
71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 85 | 86 | 87 |
名称说明
{{flowDynamicUser.name}}{{flowDynamicUser.memo}}
88 |
89 |
90 |
91 | 92 |
93 | 95 | 97 |
98 |
99 | `, 100 | providers: [ 101 | { 102 | provide: NG_VALUE_ACCESSOR, 103 | useExisting: forwardRef(() => RoleUserDialogComponent), 104 | multi: true, 105 | }, 106 | { 107 | provide: NG_VALIDATORS, 108 | useExisting: forwardRef(() => RoleUserDialogComponent), 109 | multi: true, 110 | } 111 | ] 112 | }) 113 | export class RoleUserDialogComponent 114 | implements OnInit, ControlValueAccessor, OnChanges { 115 | 116 | @Input() initial; 117 | @Input() showDialog = true; 118 | @Output() private valueChanged = new EventEmitter(); 119 | @Output() private dialogClosed = new EventEmitter(); 120 | 121 | roleAndUsers: Paticipant[]; 122 | displayDialog = true; 123 | 124 | private allUsers: UserDTO[] = []; 125 | private selectedUsers: UserDTO[] = []; 126 | 127 | private allRoles: RoleDTO[] = []; 128 | private selectedRoles: RoleDTO[] = []; 129 | 130 | private allFlowDynamicUsers: FlowDynamicUserDTO[] = []; 131 | private selectedFlowDynamicUsers: FlowDynamicUserDTO[] = []; 132 | 133 | constructor(private orgService: OrgDataService) { } 134 | 135 | ngOnInit() { 136 | this.orgService.getRoles().toPromise() 137 | .then(value => { 138 | this.allRoles = value; 139 | }); 140 | 141 | this.orgService.getAllValidUsers().toPromise() 142 | .then(value => { 143 | this.allUsers = value; 144 | }); 145 | 146 | this.orgService.getFlowDynamicUsers().toPromise() 147 | .then(value => { 148 | this.allFlowDynamicUsers = value; 149 | console.log(value); 150 | }); 151 | } 152 | 153 | ngOnChanges(changes: { [propName: string]: SimpleChange }): void { 154 | this.roleAndUsers = this.initial; 155 | this.selectedUsers = _.filter(this.roleAndUsers, p => { 156 | return p.PaticipantType === 'user'; 157 | }).map(p => { return p.PaticipantObj; }); 158 | 159 | this.selectedRoles = _.filter(this.roleAndUsers, p => { 160 | return p.PaticipantType === 'role'; 161 | }).map(p => { return p.PaticipantObj; }); 162 | 163 | this.selectedFlowDynamicUsers = _.filter(this.roleAndUsers, p => { 164 | return p.PaticipantType === 'dynamic'; 165 | }).map(p => { return p.PaticipantObj; }); 166 | 167 | if (changes['showDialog'].currentValue) { 168 | this.displayDialog = true; 169 | } 170 | } 171 | 172 | toggle(user: UserDTO) { 173 | if (_.find(this.selectedUsers, { userId: user.userId })) { 174 | _.remove(this.selectedUsers, { userId: user.userId }); 175 | } else { 176 | this.selectedUsers.push(user); 177 | } 178 | } 179 | 180 | isUserSelected(user: UserDTO) { 181 | return _.find(this.selectedUsers, { userId: user.userId }); 182 | } 183 | 184 | toggleRole(role: RoleDTO) { 185 | if (_.find(this.selectedRoles, { roleId: role.roleId })) { 186 | _.remove(this.selectedRoles, { roleId: role.roleId }); 187 | } else { 188 | this.selectedRoles.push(role); 189 | } 190 | } 191 | 192 | isRoleSelected(role: RoleDTO) { 193 | return _.find(this.selectedRoles, { roleId: role.roleId }); 194 | } 195 | 196 | getRoleUsers(role: RoleDTO) { 197 | return _.map(role.users, 'name'); 198 | } 199 | 200 | toggleFlowDynamicUser(flowDynamicUser: FlowDynamicUserDTO) { 201 | if (_.find(this.selectedFlowDynamicUsers, 202 | { flowDynamicUserId: flowDynamicUser.flowDynamicUserId })) { 203 | _.remove(this.selectedFlowDynamicUsers, 204 | { flowDynamicUserId: flowDynamicUser.flowDynamicUserId }); 205 | } else { 206 | this.selectedFlowDynamicUsers.push(flowDynamicUser); 207 | } 208 | } 209 | 210 | isFlowDynamicUserSelected(flowDynamicUser: FlowDynamicUserDTO) { 211 | return _.find(this.selectedFlowDynamicUsers, 212 | { flowDynamicUserId: flowDynamicUser.flowDynamicUserId }); 213 | } 214 | 215 | confirmed() { 216 | this.roleAndUsers = [ 217 | ...(_.map(this.selectedRoles, 218 | role => { 219 | return { 220 | PaticipantType: 'role', 221 | PaticipantObj: { 222 | name: role.name, 223 | guid: role.guid, 224 | roleId: role.roleId 225 | } 226 | }; 227 | })), 228 | ...(_.map(this.selectedUsers, 229 | user => { 230 | return { 231 | PaticipantType: 'user', 232 | PaticipantObj: { 233 | name: user.name, 234 | guid: user.guid, 235 | userId: user.userId 236 | } 237 | }; 238 | })), 239 | ...(_.map(this.selectedFlowDynamicUsers, 240 | flowDynamicUser => { 241 | return { 242 | PaticipantType: 'dynamic', 243 | PaticipantObj: { 244 | name: flowDynamicUser.name, 245 | guid: flowDynamicUser.guid, 246 | flowDynamicUserId: flowDynamicUser.flowDynamicUserId 247 | } 248 | }; 249 | })), 250 | ]; 251 | this.propagateChange(this.roleAndUsers); 252 | this.valueChanged.emit(this.roleAndUsers); 253 | this.dialogClosed.emit(); 254 | } 255 | 256 | canceled() { 257 | this.dialogClosed.emit(); 258 | } 259 | 260 | afterHideDialog() { 261 | this.dialogClosed.emit(); 262 | } 263 | 264 | // 以下为ControlValueAccessor的实现 265 | private propagateChange = (_: any) => { }; 266 | public writeValue(obj: any) { 267 | if (obj) { 268 | this.roleAndUsers = obj; 269 | } 270 | } 271 | get value(): any { 272 | return this.roleAndUsers; 273 | }; 274 | set value(v: any) { 275 | if (v !== this.roleAndUsers) { 276 | this.roleAndUsers = v; 277 | this.propagateChange(v); 278 | } 279 | } 280 | public registerOnChange(fn: any) { 281 | this.propagateChange = fn; 282 | } 283 | public registerOnTouched() { } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /src/app/services/mock-data.ts: -------------------------------------------------------------------------------- 1 | import { genGuid } from '../utilites/gen-guid'; 2 | 3 | export function mockFlowDataObj(): Object { 4 | return { 5 | 'basicInfo': { 6 | 'name': '流程模板样例', 7 | 'code': 'XX', 8 | 'version': '1', 9 | 'guid': genGuid(), 10 | 'desc': 'Test Flow', 11 | 'creator': { 12 | 'name': 'Qin,Chao', 13 | 'guid': '4d0abaa9-4825-4809-8f8f-631a7595439c' 14 | }, 15 | 'createTime': '2016-12-20 8:00AM', 16 | 'lastUpdateTime': '', 17 | 'displayName': '测试流程1', 18 | 'isPopular': '' 19 | }, 20 | 'advancedInfo': { 21 | 'arbitraryJumpAllowed': '', 22 | 'managers': '' 23 | }, 24 | 'customData': { 25 | }, 26 | 'activityNodes': { 27 | 'nodes': [ 28 | { 29 | 'type': 'st-start', 30 | 'guid': 'f6152039-1745-4f4c-8f8e-3ed37770fa0d', 31 | 'size': [ 32 | 0, 33 | 0 34 | ], 35 | 'position': [ 36 | 120, 37 | 22 38 | ], 39 | 'name': '开始', 40 | 'customData': { 41 | }, 42 | 'linkForm': '', 43 | 'beforeActions': '', 44 | 'afterActions': '', 45 | 'roles': [ 46 | { 47 | 'PaticipantType': 'role', 48 | 'PaticipantObj': { 49 | 'name': '角色-系统管理员', 50 | 'guid': 'ae25f603-87ff-4671-900f-ee69e770891a', 51 | 'roleId': 1 52 | } 53 | } 54 | ] 55 | }, 56 | { 57 | 'type': 'st-singleHumanActivity', 58 | 'guid': '2dde0ed2-c10b-4c98-b263-1016dcfa951d', 59 | 'size': [ 60 | 0, 61 | 0 62 | ], 63 | 'position': [ 64 | 100, 65 | 100 66 | ], 67 | 'name': '财务经理预审', 68 | 'customData': { 69 | }, 70 | 'linkForm': '', 71 | 'beforeActions': '', 72 | 'afterActions': '', 73 | 'roles': [ 74 | { 75 | 'PaticipantType': 'user', 76 | 'PaticipantObj': { 77 | 'name': '李四', 78 | 'guid': '27bcd361-12c7-4376-8dd8-ce68ad964431', 79 | 'userId': 2 80 | } 81 | } 82 | ] 83 | }, 84 | { 85 | 'type': 'st-multiHumanActivity', 86 | 'guid': 'ab0f92f6-35cd-44b9-a7bd-5a17e544aa9c', 87 | 'size': [ 88 | 0, 89 | 0 90 | ], 91 | 'position': [ 92 | 100, 93 | 200 94 | ], 95 | 'name': '总监批准', 96 | 'customData': { 97 | }, 98 | 'linkForm': '', 99 | 'beforeActions': '', 100 | 'afterActions': '', 101 | 'roles': [ 102 | { 103 | 'PaticipantType': 'role', 104 | 'PaticipantObj': { 105 | 'name': 'Directors', 106 | 'guid': '0c57f01f-f576-4a97-9dcb-f4eea0959c03', 107 | 'roleId': 11 108 | } 109 | }, 110 | { 111 | 'PaticipantType': 'role', 112 | 'PaticipantObj': { 113 | 'name': '中方员工', 114 | 'guid': '31a306a3-900d-46f6-ba95-66c37aaa9e84', 115 | 'roleId': 12 116 | } 117 | } 118 | ] 119 | }, 120 | { 121 | 'type': 'st-end', 122 | 'guid': 'c09fcfbe-92a9-48cd-bd14-975996e063a7', 123 | 'size': [ 124 | 0, 125 | 0 126 | ], 127 | 'position': [ 128 | 492, 129 | 373 130 | ], 131 | 'name': '结束', 132 | 'customData': { 133 | }, 134 | 'linkForm': '', 135 | 'beforeActions': '', 136 | 'afterActions': '', 137 | 'roles': '' 138 | }, 139 | { 140 | 'type': 'st-singleHumanActivity', 141 | 'guid': '5ee19a5b-df8b-4c11-a0d4-082848b5f216', 142 | 'position': [ 143 | 287, 144 | 441 145 | ], 146 | 'name': '总经理终审', 147 | 'size': [ 148 | 50, 149 | 50 150 | ], 151 | 'linkForm': '', 152 | 'beforeActions': '', 153 | 'afterActions': '', 154 | 'roles': [ 155 | { 156 | 'PaticipantType': 'user', 157 | 'PaticipantObj': { 158 | 'name': '川普', 159 | 'guid': 'c4961686-41a6-469a-afa9-df05e42ba9f8', 160 | 'userId': 16 161 | } 162 | } 163 | ], 164 | 'autoRules': [ 165 | { 166 | 'guid': '6146f486-0b86-4580-9faa-69c2e814bf59', 167 | 'name': '默认规则1', 168 | 'isDefault': true, 169 | 'code': 'return true;', 170 | 'connectionGuid': '0dade9b8-acc9-4223-8dab-9d55480722a8', 171 | 'paticipants': [ 172 | { 173 | 'PaticipantType': 'role', 174 | 'PaticipantObj': { 175 | 'name': '中方员工', 176 | 'guid': '31a306a3-900d-46f6-ba95-66c37aaa9e84', 177 | 'roleId': 12 178 | } 179 | }, 180 | { 181 | 'PaticipantType': 'role', 182 | 'PaticipantObj': { 183 | 'name': 'Directors', 184 | 'guid': '0c57f01f-f576-4a97-9dcb-f4eea0959c03', 185 | 'roleId': 11 186 | } 187 | } 188 | ] 189 | } 190 | ] 191 | }, 192 | { 193 | 'type': 'st-autoActivity', 194 | 'guid': '9d4a6006-1099-46ad-b037-24e84476ab50', 195 | 'position': [ 196 | 253, 197 | 261 198 | ], 199 | 'name': 'CEO批准?', 200 | 'size': [ 201 | 50, 202 | 50 203 | ], 204 | 'linkForm': '', 205 | 'beforeActions': '', 206 | 'afterActions': '', 207 | 'roles': '', 208 | 'autoRules': [ 209 | { 210 | 'guid': 'f00ea40e-d9ef-429b-b4bb-d18e0a4f5b05', 211 | 'name': '大于5万', 212 | 'isDefault': false, 213 | 'code': 'IC8v6KeE5YiZ5Yik5pat5LiO5omn6KGM6ISa5pys5Luj56CBLCAicmV0dXJuIHRydWU7IiDku6Pooajor6Xop4TliJnlsIbpgILnlKguCiAgICAgICAgICAgIGlmKChkb3VibGUpQml6RGF0YS5BbW91bnRUb3RhbD41MDAwMCkgewogICAgICAgICAgICAgIHJldHVybiB0cnVlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIA==', 214 | 'connectionGuid': 'd95490ae-a806-42ed-9327-272cdc7a73e8', 215 | 'paticipants': [ 216 | { 217 | 'PaticipantType': 'user', 218 | 'PaticipantObj': { 219 | 'name': '川普', 220 | 'guid': 'c4961686-41a6-469a-afa9-df05e42ba9f8', 221 | 'userId': 16 222 | } 223 | } 224 | ] 225 | }, 226 | { 227 | 'guid': 'e0f3b9ef-2e97-4f6d-ad86-5554fe3e899f', 228 | 'name': '默认规则', 229 | 'isDefault': true, 230 | 'code': 'Ly/op4TliJnliKTmlq3kuI7miafooYzohJrmnKzku6PnoIEsICdyZXR1cm4gdHJ1ZTsnIOS7o+ihqOivpeinhOWImeWwhumAgueUqC4KICAgICAgICAgICAgcmV0dXJuIHRydWU7', 231 | 'connectionGuid': '04c313cf-e702-485a-ab7e-6e0603f73366', 232 | 'paticipants': [ 233 | { 234 | 'PaticipantType': 'role', 235 | 'PaticipantObj': { 236 | 'name': '角色-系统管理员', 237 | 'guid': 'ae25f603-87ff-4671-900f-ee69e770891a', 238 | 'roleId': 1 239 | } 240 | } 241 | ] 242 | } 243 | ] 244 | } 245 | ] 246 | }, 247 | 'activityConnections': { 248 | 'connections': [ 249 | { 250 | 'guid': '476d2d52-fd31-4589-9da8-8c1e7f5d0766', 251 | 'name': '提交预审', 252 | 'fromGuid': 'f6152039-1745-4f4c-8f8e-3ed37770fa0d', 253 | 'toGuid': '2dde0ed2-c10b-4c98-b263-1016dcfa951d', 254 | 'showName': '', 255 | 'beforeActions': '', 256 | 'afterActions': '' 257 | }, 258 | { 259 | 'guid': '55d72a3e-051a-46a0-9aae-0be1c62b2e24', 260 | 'name': '完成审批', 261 | 'fromGuid': '2dde0ed2-c10b-4c98-b263-1016dcfa951d', 262 | 'toGuid': 'ab0f92f6-35cd-44b9-a7bd-5a17e544aa9c', 263 | 'showName': '', 264 | 'beforeActions': '', 265 | 'afterActions': '' 266 | }, 267 | { 268 | 'guid': '0dade9b8-acc9-4223-8dab-9d55480722a8', 269 | 'name': 'CEO批准结束', 270 | 'fromGuid': '5ee19a5b-df8b-4c11-a0d4-082848b5f216', 271 | 'toGuid': 'c09fcfbe-92a9-48cd-bd14-975996e063a7', 272 | 'showName': '', 273 | 'beforeActions': '', 274 | 'afterActions': '' 275 | }, 276 | { 277 | 'guid': 'ec0ac0d6-9ddb-4919-8a77-965891e9cfe7', 278 | 'name': '完成审批', 279 | 'fromGuid': 'ab0f92f6-35cd-44b9-a7bd-5a17e544aa9c', 280 | 'toGuid': '9d4a6006-1099-46ad-b037-24e84476ab50', 281 | 'showName': '', 282 | 'beforeActions': '', 283 | 'afterActions': '' 284 | }, 285 | { 286 | 'guid': 'd95490ae-a806-42ed-9327-272cdc7a73e8', 287 | 'name': '送CEO批准', 288 | 'fromGuid': '9d4a6006-1099-46ad-b037-24e84476ab50', 289 | 'toGuid': '5ee19a5b-df8b-4c11-a0d4-082848b5f216', 290 | 'showName': '', 291 | 'beforeActions': '', 292 | 'afterActions': '' 293 | }, 294 | { 295 | 'guid': '04c313cf-e702-485a-ab7e-6e0603f73366', 296 | 'name': '直接完成批准生效', 297 | 'fromGuid': '9d4a6006-1099-46ad-b037-24e84476ab50', 298 | 'toGuid': 'c09fcfbe-92a9-48cd-bd14-975996e063a7', 299 | 'showName': '', 300 | 'beforeActions': '', 301 | 'afterActions': '' 302 | }, 303 | { 304 | 'guid': '9d985ac5-a1da-4c63-8aea-83e9974ffccc', 305 | 'name': '直接提交', 306 | 'fromGuid': 'f6152039-1745-4f4c-8f8e-3ed37770fa0d', 307 | 'toGuid': '9d4a6006-1099-46ad-b037-24e84476ab50', 308 | 'showName': '', 309 | 'beforeActions': '', 310 | 'afterActions': '' 311 | } 312 | ] 313 | } 314 | }; 315 | } -------------------------------------------------------------------------------- /src/app/containers/property-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject, ElementRef } from '@angular/core'; 2 | import { FormGroup } from '@angular/forms'; 3 | import { Store } from 'redux'; 4 | 5 | import * as $ from 'jquery'; 6 | import * as _ from 'lodash'; 7 | import * as moment from 'moment'; 8 | 9 | import { genGuid } from '../utilites/gen-guid'; 10 | 11 | import { 12 | ActivityConnectionData, 13 | ActivityNodeData, 14 | ActivityNodeDataObj, 15 | FlowPropertyBase 16 | } from '../models/flow-data-def'; 17 | 18 | import { FlowTemplateMetaService } from '../services/flow-template-meta.service'; 19 | import { PropertyControlService } from '../services/property-control.service'; 20 | 21 | import { AppStore } from '../app-store'; 22 | 23 | import { 24 | ActivityNodeDataActions, 25 | ActivityConnectionDataActions, 26 | FlowDataActions 27 | } from '../actions'; 28 | 29 | import { 30 | AppState, 31 | } from '../reducers'; 32 | 33 | enum WorkingMode { 34 | Workflow = 1, // 缺省模式 35 | Activity, 36 | Connection, 37 | } 38 | 39 | @Component({ 40 | selector: 'app-property-panel', 41 | template: ` 42 |
44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | TODO: 暂未实现 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 71 | 72 | 73 | 74 | TODO: 暂未实现 75 | 76 | 77 | 78 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | TODO: 暂未实现 92 | 93 | 94 | 95 |
96 | 98 |
99 |
100 | ` 101 | }) 102 | export class PropertyPanelComponent implements OnInit 103 | { 104 | private currentWorkingMode: WorkingMode = WorkingMode.Workflow; 105 | private currentWorkflowTplGuid: string; 106 | private currentNodeGuid: string; 107 | private currentConnectionGuid: string; 108 | private nodes: ActivityNodeData[]; 109 | private connections: ActivityConnectionData[]; 110 | private formGroup: FormGroup; 111 | private basicControlInfo: FlowPropertyBase[]; 112 | private advancedControlInfo: FlowPropertyBase[]; 113 | 114 | constructor( 115 | @Inject(AppStore) private store: Store, 116 | @Inject(FlowTemplateMetaService) 117 | private flowTemplateMetaService: FlowTemplateMetaService, 118 | @Inject(PropertyControlService) 119 | private propertyControlService: PropertyControlService) 120 | { 121 | this.formGroup = this.propertyControlService.toFormGroup([]); 122 | store.subscribe(() => this.updateStoreState()); 123 | this.updateStoreState(); 124 | } 125 | 126 | updateStoreState(){ 127 | const _state = this.store.getState(); 128 | this.nodes = _state.activityDataNodes.activityNodeDatas; 129 | this.connections = _state.activityConnections.activityConnectionDatas; 130 | 131 | if(_state.activityConnections.currentConnectionGuid){// 当前有连接被选中 132 | if(this.currentWorkingMode == WorkingMode.Connection && 133 | this.currentConnectionGuid == 134 | _state.activityConnections.currentConnectionGuid){ 135 | // 继续编辑当前的连接属性, 136 | return; 137 | } else { // workingMode need be changed or different connection 138 | this.currentWorkingMode = WorkingMode.Connection; 139 | this.currentConnectionGuid = 140 | _state.activityConnections.currentConnectionGuid 141 | } 142 | }else if(_state.activityDataNodes.currentNodeGuid){// 当前有活动被选中 143 | if(this.currentWorkingMode == WorkingMode.Activity && 144 | this.currentNodeGuid == _state.activityDataNodes.currentNodeGuid){ 145 | // 继续编辑当前的活动属性 146 | return; 147 | } else { // workingMode need be changed or different activity 148 | this.currentWorkingMode = WorkingMode.Activity; 149 | this.currentNodeGuid = _state.activityDataNodes.currentNodeGuid; 150 | } 151 | 152 | }else{// 当前无选中的连接和活动,编辑当前的流程属性 153 | if(this.currentWorkingMode == WorkingMode.Workflow && 154 | this.currentWorkflowTplGuid == _state.flowData.currentFlowGuid){ 155 | // 继续编辑当前的流程属性 156 | return; 157 | } else { // workingMode need be changed or different flow template 158 | this.currentWorkingMode = WorkingMode.Workflow; 159 | this.currentWorkflowTplGuid = _state.flowData.currentFlowGuid; 160 | } 161 | } 162 | 163 | this.render(); 164 | } 165 | 166 | generateControlInfo(infoInstanceSection: any,PropertyDefinitionSection: string){ 167 | let info = infoInstanceSection; 168 | let InfoDefs = 169 | this.flowTemplateMetaService.getPropertyDefinitions(PropertyDefinitionSection); 170 | 171 | let _basicControlInfo = []>(_.map(InfoDefs, 172 | (def)=>{ 173 | return Object.assign({},def,{value: info[def["name"]]}); 174 | })); 175 | 176 | return _basicControlInfo; 177 | } 178 | 179 | render(){ 180 | let _basicInfo, _basicInfoDefs, _basicControlInfo, 181 | _advancedInfo, _advancedInfoDefs, _advancedControlInfo, 182 | _formGroup; 183 | const _state = this.store.getState(); 184 | 185 | switch(this.currentWorkingMode){ 186 | case WorkingMode.Workflow: // 显示与编辑流程属性 187 | this.basicControlInfo = this.generateControlInfo( 188 | _state.flowData.flowData.basicInfo, 189 | "Flow.basicInfo"); 190 | this.advancedControlInfo = this.generateControlInfo( 191 | _state.flowData.flowData.advancedInfo, 192 | "Flow.advancedInfo"); 193 | 194 | _formGroup = this.propertyControlService.toFormGroup( 195 | [...this.basicControlInfo, ...this.advancedControlInfo]); 196 | this.formGroup = _formGroup; 197 | break; 198 | 199 | case WorkingMode.Activity: // 显示与编辑活动属性 200 | this.basicControlInfo = this.generateControlInfo( 201 | _.find(_state.activityDataNodes.activityNodeDatas, 202 | {guid: this.currentNodeGuid}), 203 | "Activity.basicInfo"); 204 | this.advancedControlInfo = this.generateControlInfo( 205 | _.find(_state.activityDataNodes.activityNodeDatas, 206 | {guid: this.currentNodeGuid}), 207 | "Activity.advancedInfo"); 208 | 209 | _formGroup = this.propertyControlService.toFormGroup( 210 | [...this.basicControlInfo, ...this.advancedControlInfo]); 211 | this.formGroup = _formGroup; 212 | break; 213 | 214 | case WorkingMode.Connection: // 显示与编辑连接属性 215 | this.basicControlInfo = this.generateControlInfo( 216 | _.find(_state.activityConnections.activityConnectionDatas, 217 | {guid: this.currentConnectionGuid}), 218 | "Connection.basicInfo"); 219 | this.advancedControlInfo = this.generateControlInfo( 220 | _.find(_state.activityConnections.activityConnectionDatas, 221 | {guid: this.currentConnectionGuid}), 222 | "Connection.advancedInfo"); 223 | 224 | _formGroup = this.propertyControlService.toFormGroup( 225 | [...this.basicControlInfo, ...this.advancedControlInfo]); 226 | this.formGroup = _formGroup; 227 | break; 228 | 229 | default: 230 | 231 | } 232 | } 233 | 234 | fillSubmitSection(objToSubmitSection: any, PropertyDefinitionSection: string){ 235 | let _basicInfoDefs; 236 | _basicInfoDefs = this.flowTemplateMetaService. 237 | getPropertyDefinitions(PropertyDefinitionSection); 238 | _.each(_basicInfoDefs,(def)=>{ 239 | objToSubmitSection[def["name"]]=this.formGroup.value[def["guid"]]; 240 | }); 241 | } 242 | 243 | onSubmit(){ 244 | // console.log(this.formGroup.value); 245 | let objSubmitted={}; 246 | const _state = this.store.getState(); 247 | 248 | switch(this.currentWorkingMode){ 249 | case WorkingMode.Workflow: // 当前正在显示与编辑流程模板属性,需要提交流程模板的更新 250 | objSubmitted["basicInfo"] = 251 | Object.assign({},_state.flowData.flowData.basicInfo); 252 | this.fillSubmitSection(objSubmitted["basicInfo"],"Flow.basicInfo"); 253 | 254 | objSubmitted["advancedInfo"]= 255 | Object.assign({},_state.flowData.flowData.advancedInfo); 256 | this.fillSubmitSection(objSubmitted["advancedInfo"],"Flow.advancedInfo"); 257 | 258 | this.store.dispatch(FlowDataActions.UpdateFlowData(objSubmitted)); 259 | break; 260 | 261 | case WorkingMode.Activity: // 当前正在显示与编辑活动属性,需要提交流程活动的更新 262 | objSubmitted = 263 | Object.assign({}, _.find( 264 | _state.activityDataNodes.activityNodeDatas, 265 | {guid: this.currentNodeGuid})); 266 | this.fillSubmitSection(objSubmitted,"Activity.basicInfo"); 267 | this.fillSubmitSection(objSubmitted,"Activity.advancedInfo"); 268 | this.store.dispatch( 269 | ActivityNodeDataActions.UpdateNodeData(objSubmitted)); 270 | break; 271 | 272 | case WorkingMode.Connection: // 当前正在显示与编辑连接属性,需要提交流程连接的更新 273 | objSubmitted = 274 | Object.assign({}, _.find( 275 | _state.activityConnections.activityConnectionDatas, 276 | {guid: this.currentConnectionGuid})); 277 | this.fillSubmitSection(objSubmitted,"Connection.basicInfo"); 278 | this.fillSubmitSection(objSubmitted,"Connection.advancedInfo"); 279 | this.store.dispatch( 280 | ActivityConnectionDataActions.UpdateConnectionData(objSubmitted)); 281 | break; 282 | 283 | default: 284 | 285 | } 286 | return false; 287 | } 288 | 289 | ngOnInit() { 290 | 291 | } 292 | 293 | } 294 | -------------------------------------------------------------------------------- /src/assets/js/notie@3.9.5.js: -------------------------------------------------------------------------------- 1 | var notie=function(){function e(e){for(var t in e)N[t]=e[t]}function t(e,t,i){N.colorText.length>0&&(Y.style.color=N.colorText),L(),O++,setTimeout(function(){O--},N.animationDelay+10),1===O&&(J?n(function(){o(e,t,i)}):o(e,t,i))}function o(e,t,o){J=!0;var i=0;switch(i="undefined"==typeof o||0===o?864e5:o>0&&o<1?1e3:1e3*o,M(S,"notie-background-success"),M(S,"notie-background-warning"),M(S,"notie-background-error"),M(S,"notie-background-info"),e){case 1:case"success":N.colorSuccess.length>0?S.style.backgroundColor=N.colorSuccess:H(S,"notie-background-success");break;case 2:case"warning":N.colorWarning.length>0?S.style.backgroundColor=N.colorWarning:H(S,"notie-background-warning");break;case 3:case"error":N.colorError.length>0?S.style.backgroundColor=N.colorError:H(S,"notie-background-error");break;case 4:case"info":N.colorInfo.length>0?S.style.backgroundColor=N.colorInfo:H(S,"notie-background-info")}Y.innerHTML=t,S.style.top="-10000px",S.style.display="table",S.style.top="-"+S.offsetHeight-5+"px",z=setTimeout(function(){H(S,"notie-transition"),S.style.top=0,j=setTimeout(function(){n(function(){})},i)},20)}function n(e){clearTimeout(z),clearTimeout(j),S.style.top="-"+S.offsetHeight-5+"px",setTimeout(function(){M(S,"notie-transition"),S.style.top="-10000px",J=!1,e&&e()},N.animationDelay+10)}function i(e,t,o,i){N.colorText.length>0&&(B.style.color=N.colorText,K.style.color=N.colorText),L(),J?n(function(){c(e,t,o,i)}):c(e,t,o,i)}function c(e,t,o,n){function i(){B.innerHTML=t,K.innerHTML=o,R.style.top="-10000px",R.style.display="table",R.style.top="-"+R.offsetHeight-5+"px",V.style.display="block",setTimeout(function(){H(R,"notie-transition"),R.style.top=0,V.style.opacity="0.75",setTimeout(function(){$=!0},N.animationDelay+10)},20)}switch(A(),K.onclick=function(){l(),n&&setTimeout(function(){n()},N.animationDelay+10)},M(R,"notie-background-success"),M(R,"notie-background-warning"),M(R,"notie-background-error"),M(R,"notie-background-info"),e){case 1:case"success":N.colorSuccess.length>0?R.style.backgroundColor=N.colorSuccess:H(R,"notie-background-success");break;case 2:case"warning":N.colorWarning.length>0?R.style.backgroundColor=N.colorWarning:H(R,"notie-background-warning");break;case 3:case"error":N.colorError.length>0?R.style.backgroundColor=N.colorError:H(R,"notie-background-error");break;case 4:case"info":N.colorInfo.length>0?R.style.backgroundColor=N.colorInfo:H(R,"notie-background-info")}$?(l(),setTimeout(function(){i()},N.animationDelay+10)):i()}function l(){R.style.top="-"+R.offsetHeight-5+"px",V.style.opacity="0",setTimeout(function(){M(R,"notie-transition"),R.style.top="-10000px",V.style.display="none",I(),$=!1},N.animationDelay+10)}function a(e,t,o,i,c){N.colorInfo.length>0&&(G.style.backgroundColor=N.colorInfo),N.colorSuccess.length>0&&(Q.style.backgroundColor=N.colorSuccess),N.colorError.length>0&&(U.style.backgroundColor=N.colorError),N.colorText.length>0&&(P.style.color=N.colorText,X.style.color=N.colorText,_.style.color=N.colorText),L(),J?n(function(){r(e,t,o,i,c)}):r(e,t,o,i,c)}function r(e,t,o,n,i){function c(){P.innerHTML=e,X.innerHTML=t,_.innerHTML=o,q.style.top="-10000px",q.style.display="table",q.style.top="-"+q.offsetHeight-5+"px",ee.style.display="block",setTimeout(function(){H(q,"notie-transition"),q.style.top=0,ee.style.opacity="0.75",setTimeout(function(){te=!0},N.animationDelay+10)},20)}A(),Q.onclick=function(){d(),n&&setTimeout(function(){n()},N.animationDelay+10)},U.onclick=function(){d(),i&&setTimeout(function(){i()},N.animationDelay+10)},te?(d(),setTimeout(function(){c()},N.animationDelay+10)):c()}function d(){q.style.top="-"+q.offsetHeight-5+"px",ee.style.opacity="0",setTimeout(function(){M(q,"notie-transition"),q.style.top="-10000px",ee.style.display="none",I(),te=!1},N.animationDelay+10)}function s(e,t,o,i,c,l){N.colorInfo.length>0&&(ie.style.backgroundColor=N.colorInfo),N.colorSuccess.length>0&&(le.style.backgroundColor=N.colorSuccess),N.colorError.length>0&&(ae.style.backgroundColor=N.colorError),N.colorText.length>0&&(re.style.color=N.colorText,de.style.color=N.colorText,se.style.color=N.colorText),L(),ce.setAttribute("autocapitalize",e.autocapitalize||"none"),ce.setAttribute("autocomplete",e.autocomplete||"off"),ce.setAttribute("autocorrect",e.autocorrect||"off"),ce.setAttribute("autofocus",e.autofocus||"true"),ce.setAttribute("inputmode",e.inputmode||"verbatim"),ce.setAttribute("max",e.max||""),ce.setAttribute("maxlength",e.maxlength||""),ce.setAttribute("min",e.min||""),ce.setAttribute("minlength",e.minlength||""),ce.setAttribute("placeholder",e.placeholder||""),ce.setAttribute("spellcheck",e.spellcheck||"default"),ce.setAttribute("step",e.step||"any"),ce.setAttribute("type",e.type||"text"),ce.value=e.prefilledValue||"",e.allowed?ce.oninput=function(){if(Array.isArray(e.allowed)){for(var t="",o=e.allowed,n=0;n0&&(pe.style.backgroundColor=N.colorInfo),N.colorNeutral.length>0&&(he.style.backgroundColor=N.colorNeutral),N.colorText.length>0&&(ye.style.color=N.colorText,he.style.color=N.colorText),L(),J?n(function(){y(e,t,o)}):y(e,t,o)}function y(e,t,o){function n(e){ye.innerHTML=e,me.style.bottom="-10000px",me.style.display="table",me.style.bottom="-"+me.offsetHeight-5+"px",fe.style.display="block",setTimeout(function(){H(me,"notie-transition"),me.style.bottom=0,fe.style.opacity="0.75",setTimeout(function(){be=!0},N.animationDelay+10)},20)}A(),document.getElementById("notie-select-choices").innerHTML="",he.innerHTML=t;for(var i,c=0;c0&&(l.style.color=N.colorText),o[c].type)switch(o[c].type){case 1:N.colorSuccess.length>0?l.style.backgroundColor=N.colorSuccess:H(l,"notie-background-success");break;case 2:N.colorWarning.length>0?l.style.backgroundColor=N.colorWarning:H(l,"notie-background-warning");break;case 3:N.colorError.length>0?l.style.backgroundColor=N.colorError:H(l,"notie-background-error");break;case 4:N.colorInfo.length>0?l.style.backgroundColor=N.colorInfo:H(l,"notie-background-info")}else o[c].color&&(l.style.backgroundColor=o[c].color);if(l.style.backgroundColor=window.getComputedStyle(l).backgroundColor,c>0&&l.style.backgroundColor===i.style.backgroundColor&&H(i,"notie-select-choice-bottom-border"),!o[c].handler)throw new Error('notie.select choice "'+l.title+'" must have a handler');l.onclick=function(e){return function(){f(),setTimeout(function(){o[e].handler()},N.animationDelay+10)}}(c),i=l}be?(f(),setTimeout(function(){n(e)},N.animationDelay+10)):n(e)}function f(){me.style.bottom="-"+me.offsetHeight-5+"px",fe.style.opacity="0",setTimeout(function(){M(me,"notie-transition"),me.style.bottom="-10000px",fe.style.display="none",I(),be=!1},N.animationDelay+10)}function g(){return J||te||ue||be||Je}function h(e){N.colorInfo.length>0&&(Ce.style.backgroundColor=N.colorInfo),N.colorSuccess.length>0&&(Se.style.backgroundColor=N.colorSuccess),N.colorError.length>0&&(We.style.backgroundColor=N.colorError),N.colorText.length>0&&(Ce.style.color=N.colorText),L(),J?n(function(){b(e)}):b(e)}function b(e){function t(){Ye=e.initial||new Date,v(Ye),Se.innerHTML=e.yesText||"OK",We.innerHTML=e.noText||"Cancel",ve.style.top="-10000px",ve.style.display="table",ve.style.top="-"+ve.offsetHeight-5+"px",Fe.style.display="block",setTimeout(function(){H(ve,"notie-transition"),ve.style.top=0,Fe.style.opacity="0.75",setTimeout(function(){Je=!0},N.animationDelay+10)},20)}A(),Se.onclick=function(){D(),e.yesCallback&&setTimeout(function(){e.yesCallback(Ye)},N.animationDelay+10)},We.onclick=function(){D(),e.noCallback&&setTimeout(function(){e.noCallback(Ye)},N.animationDelay+10)},Je?(D(),setTimeout(function(){t()},N.animationDelay+10)):t()}function v(e){He.innerHTML=N.dateMonths[e.getMonth()],Me.innerHTML=e.getDate(),Le.innerHTML=e.getFullYear()}function k(){Ye.setMonth(Ye.getMonth()-1),v(Ye)}function C(){Ye.setMonth(Ye.getMonth()+1),v(Ye)}function T(){Ye.setDate(Ye.getDate()-1),v(Ye)}function x(){Ye.setDate(Ye.getDate()+1),v(Ye)}function E(){Ye.setFullYear(Ye.getFullYear()-1),v(Ye)}function w(){Ye.setFullYear(Ye.getFullYear()+1),v(Ye)}function D(){ve.style.top="-"+ve.offsetHeight-5+"px",Fe.style.opacity="0",setTimeout(function(){M(ve,"notie-transition"),ve.style.top="-10000px",Fe.style.display="none",I(),Je=!1},N.animationDelay+10)}function H(e,t){e.classList?e.classList.add(t):e.className+=" "+t}function M(e,t){e.classList?e.classList.remove(t):e.className=e.className.replace(new RegExp("(^|\\b)"+t.split(" ").join("|")+"(\\b|$)","gi")," ")}function L(){document.activeElement&&document.activeElement.blur()}function A(){ze=document.body.style.height,je=document.body.style.overflow,document.body.style.height="100%",document.body.style.overflow="hidden"}function I(){document.body.style.height=ze,document.body.style.overflow=je}var N={colorSuccess:"",colorWarning:"",colorError:"",colorInfo:"",colorNeutral:"",colorText:"",dateMonths:["January","February","March","April","May","June","July","August","September","October","November","December"],animationDelay:300,backgroundClickDismiss:!0},S=document.createElement("div");S.id="notie-alert-outer",S.onclick=function(){n()},document.body.appendChild(S);var W=document.createElement("div");W.id="notie-alert-inner",S.appendChild(W);var F=document.createElement("div");F.id="notie-alert-content",W.appendChild(F);var Y=document.createElement("span");Y.id="notie-alert-text",F.appendChild(Y);var z,j,J=!1,O=0,R=document.createElement("div");R.id="notie-force-outer";var Z=document.createElement("div");Z.id="notie-force-inner",R.appendChild(Z);var B=document.createElement("span");B.id="notie-force-text",Z.appendChild(B);var K=document.createElement("div");K.id="notie-force-button",R.appendChild(K);var V=document.createElement("div");V.id="notie-force-background",H(V,"notie-transition"),document.body.appendChild(R),document.body.appendChild(V);var $=!1,q=document.createElement("div");q.id="notie-confirm-outer";var G=document.createElement("div");G.id="notie-confirm-inner",q.appendChild(G);var P=document.createElement("span");P.id="notie-confirm-text",G.appendChild(P);var Q=document.createElement("div");Q.id="notie-confirm-yes",q.appendChild(Q);var U=document.createElement("div");U.id="notie-confirm-no",q.appendChild(U);var X=document.createElement("span");X.id="notie-confirm-text-yes",Q.appendChild(X);var _=document.createElement("span");_.id="notie-confirm-text-no",U.appendChild(_);var ee=document.createElement("div");ee.id="notie-confirm-background",H(ee,"notie-transition"),ee.onclick=function(){N.backgroundClickDismiss&&d()},document.body.appendChild(q),document.body.appendChild(ee);var te=!1,oe=document.createElement("div");oe.id="notie-input-outer";var ne=document.createElement("div");ne.id="notie-input-background",H(ne,"notie-transition");var ie=document.createElement("div");ie.id="notie-input-inner",oe.appendChild(ie);var ce=document.createElement("input");ce.id="notie-input-field",oe.appendChild(ce);var le=document.createElement("div");le.id="notie-input-yes",oe.appendChild(le);var ae=document.createElement("div");ae.id="notie-input-no",oe.appendChild(ae);var re=document.createElement("span");re.id="notie-input-text",ie.appendChild(re);var de=document.createElement("span");de.id="notie-input-text-yes",le.appendChild(de);var se=document.createElement("span");se.id="notie-input-text-no",ae.appendChild(se),document.body.appendChild(oe),document.body.appendChild(ne),ne.onclick=function(){N.backgroundClickDismiss&&m()};var ue=!1,me=document.createElement("div");me.id="notie-select-outer";var pe=document.createElement("div");pe.id="notie-select-inner",me.appendChild(pe);var ye=document.createElement("span");ye.id="notie-select-text",pe.appendChild(ye);var fe=document.createElement("div");fe.id="notie-select-background",H(fe,"notie-transition");var ge=document.createElement("div");ge.id="notie-select-choices",me.appendChild(ge);var he=document.createElement("div");he.id="notie-select-cancel",me.appendChild(he),document.body.appendChild(me),document.body.appendChild(fe),fe.onclick=function(){N.backgroundClickDismiss&&f()},he.onclick=function(){f()};var be=!1,ve=document.createElement("div");ve.id="notie-date-outer";var ke=document.createElement("div");ke.id="notie-date-selector",ve.appendChild(ke);var Ce=document.createElement("div");Ce.id="notie-date-inner",ve.appendChild(Ce);var Te='
',xe='
',Ee=document.createElement("div");Ee.className="notie-date-up",Ee.innerHTML=Te,ke.appendChild(Ee),Ee.onclick=k;var we=document.createElement("div");we.className="notie-date-up",we.innerHTML=Te,ke.appendChild(we),we.onclick=T;var De=document.createElement("div");De.className="notie-date-up",De.innerHTML=Te,ke.appendChild(De),De.onclick=E;var He=document.createElement("div");He.className="notie-date-text",ke.appendChild(He);var Me=document.createElement("div");Me.className="notie-date-text",ke.appendChild(Me);var Le=document.createElement("div");Le.className="notie-date-text",ke.appendChild(Le);var Ae=document.createElement("div");Ae.className="notie-date-down",Ae.innerHTML=xe,ke.appendChild(Ae),Ae.onclick=C;var Ie=document.createElement("div");Ie.className="notie-date-down",Ie.innerHTML=xe,ke.appendChild(Ie),Ie.onclick=x;var Ne=document.createElement("div");Ne.className="notie-date-down",Ne.innerHTML=xe,ke.appendChild(Ne),Ne.onclick=w;var Se=document.createElement("div");Se.id="notie-date-yes",Ce.appendChild(Se);var We=document.createElement("div");We.id="notie-date-no",Ce.appendChild(We);var Fe=document.createElement("div");Fe.id="notie-date-background",H(Fe,"notie-transition"),Fe.onclick=function(){N.backgroundClickDismiss&&D()},document.body.appendChild(ve),document.body.appendChild(Fe);var Ye,ze,je,Je=!1;return window.addEventListener("keydown",function(e){var t=13===e.which||13===e.keyCode,o=27===e.which||27===e.keyCode;J?(t||o)&&n():te?t?Q.click():o&&d():ue?t?le.click():o&&m():be?o&&f():Je&&(t?Se.click():o&&D())}),{setOptions:e,alert:t,alertHide:n,force:i,confirm:a,input:s,select:p,date:h,isShowing:g}};"undefined"!=typeof window&&window&&("object"==typeof module&&module.exports?module.exports=notie():window.notie=notie()); -------------------------------------------------------------------------------- /src/app/components/flow-chart-parent.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject, ElementRef } from '@angular/core'; 2 | import { Store } from 'redux'; 3 | 4 | // import * as $ from 'jquery'; // Webpack complains and output error 5 | declare var $; 6 | 7 | import * as _ from 'lodash'; 8 | import * as moment from 'moment'; 9 | import * as Raphael from 'raphael'; 10 | 11 | import { genGuid } from '../utilites/gen-guid'; 12 | 13 | import { 14 | ActivityConnectionData, ActivityNodeData, ActivityNodeDataObj, 15 | } from '../models/flow-data-def'; 16 | 17 | import { AppStore } from '../app-store'; 18 | 19 | import { 20 | ActivityNodeDataActions, 21 | ActivityConnectionDataActions 22 | } from '../actions'; 23 | 24 | import { 25 | AppState, 26 | } from '../reducers'; 27 | 28 | class ActivityConnectionDraw { // 节点间连线绘图对象 29 | guid?: string; 30 | name?: string; 31 | element?: RaphaelElement; 32 | obj1: any; // 为DragProcessor.drag_end作此妥协 33 | obj2: RaphaelElement; 34 | 35 | // 绘制(并创建)节点间连线绘图对象 36 | static drawConnection(obj: ActivityConnectionDraw, drawingPaper: DrawingPaper) { 37 | // when draw the first time, need validate it 为简单起见两个节点间的同一方向连线不能超过两条 38 | if (!obj.element && _.filter(drawingPaper.connections, (c) => { 39 | return c.obj1 === obj.obj1 && c.obj2 === obj.obj2; 40 | }).length >= 2) { 41 | notie.alert(3, '两个节点间的同一方向连线不能超过两条!', 3); 42 | return null; 43 | } 44 | 45 | let _connectionsBefore = _.take(drawingPaper.connections, 46 | obj.element ? _.findIndex(drawingPaper.connections, { guid: obj.guid }) : 47 | drawingPaper.connections.length); 48 | let _conIndex = _.filter(_connectionsBefore, (c) => { 49 | return ( // c.guid != obj.guid && 50 | ((c.obj1 === obj.obj1 && c.obj2 === obj.obj2) 51 | || (c.obj1 === obj.obj2 && c.obj2 === obj.obj1))); 52 | }).length; 53 | let pointsPair = ActivityConnectionDraw.getStartEnd(obj.obj1, obj.obj2, _conIndex); 54 | let _path = ActivityConnectionDraw.getConnection(pointsPair.start.x, pointsPair.start.y, 55 | pointsPair.end.x, pointsPair.end.y); 56 | if (obj.element) { // redraw 57 | obj.element.attr({ path: _path }); 58 | } else { // draw the first time, need initialize it 59 | obj.guid = obj.guid || genGuid(); 60 | obj.name = obj.name || '未命名连接' + Date.now().toString(); 61 | obj.element = drawingPaper.paper.path(_path); 62 | obj.element.click(drawingPaper.clickProcessor.connection_click); 63 | obj.element.hover( 64 | function (e) { 65 | e.currentTarget.style.cursor = 'hand'; 66 | }, 67 | function (e) { 68 | e.currentTarget.style.cursor = 'pointer'; 69 | } 70 | ); 71 | drawingPaper.connections.push(obj); 72 | 73 | } 74 | obj.element.attr({ 'stroke-dasharray': '', stroke: 'blue', 'stroke-width': 2 }); 75 | obj.element.data('guid', obj.guid); 76 | obj.element.toBack(); 77 | return obj; 78 | }; 79 | 80 | // 获取连接起始与结束对象的连线的起始Point与结束Point 81 | static getStartEnd(obj1: RaphaelElement, obj2: RaphaelElement, index = 0): Point2Point { 82 | let bb1 = obj1.getBBox(), 83 | bb2 = obj2.getBBox(); 84 | let p = [ 85 | { x: bb1.x + bb1.width / 2, y: bb1.y - 1 }, // top 86 | { x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + 1 }, // bottom 87 | { x: bb1.x - 1, y: bb1.y + bb1.height / 2 }, // left 88 | { x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height / 2 }, // right 89 | { x: bb1.x - 1, y: bb1.y - 1 }, // top-left 90 | { x: bb1.x + bb1.width + 1, y: bb1.y - 1 }, // top-right 91 | { x: bb1.x - 1, y: bb1.y + bb1.height + 1 }, // bottom-left 92 | { x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height + 1 }, // bottom-right 93 | 94 | { x: bb2.x + bb2.width / 2, y: bb2.y - 1 }, // top 95 | { x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + 1 }, // bottom 96 | { x: bb2.x - 1, y: bb2.y + bb2.height / 2 }, // left 97 | { x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height / 2 }, // right 98 | { x: bb2.x - 1, y: bb2.y - 1 }, // top-left 99 | { x: bb2.x + bb2.width + 1, y: bb2.y - 1 }, // top-right 100 | { x: bb2.x - 1, y: bb2.y + bb2.height + 1 }, // bottom-left 101 | { x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height + 1 }, // bottom-right 102 | ]; 103 | 104 | let disArray = []; 105 | for (let i = 0; i < 8; i++) { 106 | for (let j = 8; j < 16; j++) { 107 | disArray.push({ 108 | distance: Math.abs(p[i].x - p[j].x) + Math.abs(p[i].y - p[j].y), 109 | start: p[i], end: p[j] 110 | }); 111 | } 112 | } 113 | let result: Point2Point = _.sortBy(disArray, 'distance')[index]; 114 | return result; 115 | } 116 | 117 | // 获取组成箭头的三条线段的路径数组 118 | static getConnection(x1: number, y1: number, x2: number, y2: number, size = 10): any[] { 119 | let closeAngle = 20; 120 | let angle = Raphael.angle(x1, y1, x2, y2); // 得到两点之间的角度 121 | let aClose1 = Raphael.rad(angle - closeAngle); // 角度转换成弧度 122 | let aClose2 = Raphael.rad(angle + closeAngle); 123 | let x2a = x2 + Math.cos(aClose1) * size; 124 | let y2a = y2 + Math.sin(aClose1) * size; 125 | let x2b = x2 + Math.cos(aClose2) * size; 126 | let y2b = y2 + Math.sin(aClose2) * size; 127 | let result = ['M', x1, y1, 'L', x2, y2, 'L', x2a, y2a, 'M', x2, y2, 'L', x2b, y2b]; 128 | return result; 129 | } 130 | } 131 | 132 | class DragProcessor { // 拖动事件处理器,用于处理 1.拖动节点; 2.拖动生成形成节点间的连线 133 | static drawingPaper: DrawingPaper; 134 | // 拖动节点开始时的事件 135 | drag_start(x, y, e): Object { 136 | if (DragProcessor.drawingPaper.drawActivityType === '_connection') {// 连线模式 137 | this.ox = e.offsetX; 138 | this.oy = e.offsetY; 139 | this.attr({ 'stroke-width': 1, 'stroke-dasharray': '.' }); 140 | } else {// 拖动节点模式 141 | this.ox = this.attr('x'); 142 | this.oy = this.attr('y'); 143 | this.attr({ 'stroke-width': 3, 'stroke-dasharray': '-' }); 144 | } 145 | return this; 146 | }; 147 | 148 | // 拖动事件 149 | drag_move(dx, dy, x, y, e): Object { 150 | if (DragProcessor.drawingPaper.drawActivityType === '_connection') {// 连线模式 151 | DragProcessor.drawingPaper.removeDrawingTempPath(); 152 | DragProcessor.drawingPaper.drawingTempPath = 153 | DragProcessor.drawingPaper.paper.path(['M', this.ox, this.oy, 'L', e.offsetX, e.offsetY]); 154 | DragProcessor.drawingPaper.drawingTempPath.attr({ 'stroke-width': 2, 'stroke-dasharray': '-' }); 155 | } else {// 拖动节点模式 156 | let att = { x: this.ox + dx, y: this.oy + dy }; 157 | this.attr(att); // 即时根据拖动的位移改动节点位置 158 | if (!this.attr('src')) { // 非图片型的节点需要同时移动其文本位置 159 | const txtX = att.x + 32, txtY = att.y + 20; 160 | DragProcessor.drawingPaper.findNodeDrawByGuid(this.data('guid')). 161 | textElement.attr({ x: txtX, y: txtY }); 162 | } 163 | 164 | // 筛选出需要重绘的节点间连接并执行重绘 165 | let _connectionsNeedRedraw = _.filter(DragProcessor.drawingPaper.connections, (con) => { 166 | return (con.obj2.data('guid') === this.data('guid') || con.obj1.data('guid') === this.data('guid')); 167 | }); 168 | _connectionsNeedRedraw.forEach((con) => { 169 | ActivityConnectionDraw.drawConnection(con, DragProcessor.drawingPaper); 170 | }); 171 | } 172 | return this; 173 | }; 174 | 175 | // 拖动结束后的事件 176 | drag_end(e): Object { 177 | if (DragProcessor.drawingPaper.drawActivityType === '_connection') {// 连线模式 178 | this.attr({ 'stroke-width': 1, 'stroke-dasharray': '' }); 179 | let _shape = _.find(DragProcessor.drawingPaper.nodeDrawElements, (_shapeEle) => { 180 | let _bbox = _shapeEle.element.getBBox(); 181 | return (_bbox.x <= e.offsetX && e.offsetX <= _bbox.x2 && 182 | _bbox.y <= e.offsetY && e.offsetY <= _bbox.y2); 183 | }); 184 | 185 | if (_shape && _shape.element.id !== this.id) { // 需要创建节点间连线 186 | let drawObj = ActivityConnectionDraw.drawConnection({ 187 | obj1: this, 188 | obj2: _shape.element 189 | }, DragProcessor.drawingPaper); 190 | 191 | if (drawObj) { //连线能够成功创建才更新 192 | DragProcessor.drawingPaper.store.dispatch( 193 | ActivityConnectionDataActions.CreateConnectionData( 194 | DragProcessor.drawingPaper.addConnection(drawObj))); 195 | } 196 | } 197 | DragProcessor.drawingPaper.removeDrawingTempPath(); 198 | 199 | } else { // 拖动节点模式 200 | this.attr({ 'stroke-width': 1, 'stroke-dasharray': '' }); 201 | const nodeData = DragProcessor.drawingPaper.findNodeDataByGuid(this.data('guid')); 202 | DragProcessor.drawingPaper.store.dispatch(ActivityNodeDataActions.UpdateNodeData( 203 | Object.assign({}, nodeData, { position: [this.attr('x'), this.attr('y')] }) 204 | )); 205 | DragProcessor.drawingPaper.unselectConnection(); 206 | } 207 | 208 | // 将画板获取焦点,否则DEL键响应失效. 209 | $('#FlowChart').trigger('focus'); 210 | 211 | return this; 212 | }; 213 | 214 | /// 以下的实例属性与方法用于欺骗TypeScript编译检查 215 | ox: number; 216 | oy: number; 217 | id: string; 218 | data(name: string): any { } 219 | attr(attr: any): any { } 220 | /// 欺骗内容结束 221 | } 222 | 223 | class ClickProcessor { // 点击事件处理器 224 | static drawingPaper: DrawingPaper; 225 | 226 | // Warning: these event handlers will not be triggered for 227 | // the conflict with DragProcessor 228 | node_click(e) { 229 | ClickProcessor.drawingPaper.selectNode(this.data('guid')); 230 | } 231 | 232 | connection_click = function (e) { 233 | ClickProcessor.drawingPaper.selectConnection(this.data('guid')); 234 | } 235 | 236 | /// 以下的实例属性与方法用于欺骗TypeScript编译检查 237 | data(attr: any): any { 238 | } 239 | /// 欺骗内容结束 240 | } 241 | 242 | class DblClickProcessor { // 双击事件处理器 243 | static drawingPaper: DrawingPaper; 244 | currentEditingNodeGuid: string = null; 245 | 246 | text_dbl_click(e) { 247 | /// 在事件处理器里this并不是类实例本身,而是触发事件的源对象 248 | DblClickProcessor.drawingPaper.startNodeTextEditing( 249 | this); 250 | } 251 | } 252 | 253 | class ActivityNodeDraw { // 节点绘图对象 254 | static getNodeNameShortVersion(name: string) { 255 | return name.length > 6 ? name.substr(0, 4) + '...' : name; 256 | } 257 | // 绘制节点 258 | static drawFromNodeData(nodeData: ActivityNodeData, drawingPaper: DrawingPaper): ActivityNodeDraw { 259 | let result: ActivityNodeDraw, ele: RaphaelElement, txtElement: RaphaelElement; 260 | const x = nodeData.position[0], y = nodeData.position[1]; 261 | const txtX = x + 32, txtY = y + 20; 262 | const imgHeight = 40, imgWidth = 40; 263 | const rectHeight = 40, rectWidth = 80, rectRadius = 5; 264 | const nodeNameDisplayed = ActivityNodeDraw.getNodeNameShortVersion(nodeData.name); 265 | switch (nodeData.type) { 266 | case 'st-start': 267 | ele = drawingPaper.paper.image('assets/images/flow_start.png', x, y, imgWidth, imgHeight); 268 | break; 269 | case 'st-end': 270 | ele = drawingPaper.paper.image('assets/images/flow_end.png', x, y, imgWidth, imgHeight); 271 | break; 272 | case 'st-singleHumanActivity': 273 | ele = drawingPaper.paper.rect(x, y, rectWidth, rectHeight, rectRadius); 274 | ele.attr({ fill: '#43C8F7', title: nodeData.name }); 275 | txtElement = drawingPaper.paper.text(txtX, txtY, nodeNameDisplayed); 276 | break; 277 | case 'st-multiHumanActivity': 278 | ele = drawingPaper.paper.rect(x, y, rectWidth, rectHeight, rectRadius); 279 | ele.attr({ fill: '#F1C8F7', title: nodeData.name }); 280 | txtElement = drawingPaper.paper.text(txtX, txtY, nodeNameDisplayed); 281 | break; 282 | case 'st-autoActivity': 283 | ele = drawingPaper.paper.rect(x, y, rectWidth, rectHeight, rectRadius); 284 | ele.attr({ fill: '#F1F10D', title: nodeData.name }); 285 | txtElement = drawingPaper.paper.text(txtX, txtY, nodeNameDisplayed); 286 | break; 287 | default: 288 | console.info(`No drawing handler for type'` + nodeData.type + '"'); 289 | return null; 290 | } 291 | 292 | ele.data('guid', nodeData.guid); 293 | result = new ActivityNodeDraw(nodeData.guid, ele, drawingPaper); 294 | if (txtElement) { 295 | txtElement.data('guid', nodeData.guid); 296 | txtElement.dblclick(drawingPaper.dblClickProcessor.text_dbl_click); 297 | result.textElement = txtElement; 298 | } 299 | result.element.drag(drawingPaper.dragProcessor.drag_move, drawingPaper.dragProcessor.drag_start, drawingPaper.dragProcessor.drag_end); 300 | result.element.click(drawingPaper.clickProcessor.node_click); 301 | result.element.hover(function (e) { e.currentTarget.style.cursor = 'hand'; }, function (e) { e.currentTarget.style.cursor = 'pointer'; }); 302 | drawingPaper.nodeDrawElements.push(result); 303 | // 以下为根据节点的位置自动扩张流程图绘图区域 304 | if (drawingPaper.paper.width - 50 < x + 80) { 305 | drawingPaper.paper.setSize( 306 | drawingPaper.paper.width + 200, 307 | drawingPaper.paper.height ); 308 | } 309 | if (drawingPaper.paper.height - 50 < y + 40) { 310 | drawingPaper.paper.setSize( 311 | drawingPaper.paper.width, 312 | drawingPaper.paper.height + 200); 313 | } 314 | return result; 315 | } 316 | 317 | constructor(guid: string, element: RaphaelElement, drawingPaper: DrawingPaper) { 318 | this.guid = guid; 319 | this.element = element; 320 | this.drawingPaper = drawingPaper; 321 | } 322 | 323 | guid: string; 324 | element: RaphaelElement; 325 | textElement?: RaphaelElement; // 开始结束节点无文本显示 326 | drawingPaper: DrawingPaper; 327 | 328 | } 329 | 330 | interface Point { 331 | x: number; 332 | y: number; 333 | } 334 | interface Point2Point { 335 | start: Point; 336 | end: Point; 337 | } 338 | 339 | class DrawingPaper { 340 | constructor(paper: RaphaelPaper) { 341 | this.paper = paper; 342 | } 343 | paper: RaphaelPaper; 344 | store: Store; 345 | 346 | dragProcessor: DragProcessor = null; 347 | clickProcessor: ClickProcessor = null; 348 | dblClickProcessor: DblClickProcessor = null; 349 | 350 | drawActivityType: string = '_'; 351 | isNodeTextEditing: boolean = false; // 是否正在编辑节点名称的文本 352 | drawingTempPath: RaphaelElement = null;// 正在画的临时连接Path 353 | //selectedConnection :ActivityConnectionDraw = null; // 正在被选中的连接 354 | selectedConnectionGuid: string = ''; // 正在被选中的连接GUID 355 | selectedNodeGuid: string = ''; // 正在被选中的节点GUID 356 | 357 | connectionsData: ActivityConnectionData[] = []; // 节点间连线数据对象数组 358 | connections: ActivityConnectionDraw[] = []; // 节点间连线绘图对象数组 359 | nodesData: ActivityNodeData[] = []; // 节点数据对象数组 360 | nodeDrawElements: ActivityNodeDraw[] = [] // 节点绘图对象数组 361 | 362 | workflowTemplateName: string = ''; 363 | 364 | // 根据节点数据对象数组和节点间连线数据对象数组绘制流程图 365 | render(needRedrawNodes = true, needRedrawConnections = true) { 366 | if (needRedrawNodes) { // 绘制节点 367 | this.clearNodes(); 368 | _.each(this.nodesData, (n) => { 369 | ActivityNodeDraw.drawFromNodeData(n, this); 370 | }); 371 | } 372 | 373 | if (needRedrawConnections) { // 绘制节点间连接线与箭头 374 | this.clearConnections(); 375 | _.each(this.connectionsData, (con) => { 376 | if (this.findNodeDrawByGuid(con.fromGuid) && 377 | this.findNodeDrawByGuid(con.toGuid)) { 378 | ActivityConnectionDraw.drawConnection({ 379 | obj1: this.findNodeDrawByGuid(con.fromGuid).element, 380 | obj2: this.findNodeDrawByGuid(con.toGuid).element, 381 | guid: con.guid, 382 | }, this); 383 | } 384 | }); 385 | } 386 | 387 | 388 | this.decorateSelectedNode(this.selectedNodeGuid); 389 | this.decorateSelectedConnection(this.selectedConnectionGuid); 390 | } 391 | 392 | removeDrawingTempPath() { 393 | if (this.drawingTempPath) { 394 | this.drawingTempPath.remove(); 395 | this.drawingTempPath = null; 396 | } 397 | } 398 | 399 | selectConnection(guid: string) { 400 | this.finishNodeTextEditing(false); 401 | if (guid) { this.unselectNode(); } 402 | this.store.dispatch(ActivityConnectionDataActions.SetCurrentConnectionData(guid)); 403 | } 404 | 405 | decorateSelectedConnection(guid: string) { 406 | if (!guid) { return null; } 407 | 408 | let _con = _.find(this.connections, { guid: guid }); 409 | if (_con) { 410 | _con.element.attr({ 'stroke-dasharray': '-', stroke: 'red' }); 411 | } else { 412 | console.info('Cannot find connection(maybe deleted?) :' + guid); 413 | } 414 | return _con; 415 | } 416 | 417 | unselectConnection() { 418 | this.selectConnection(null); 419 | } 420 | 421 | selectNode(guid: string) { 422 | if (guid) {this.unselectConnection();} 423 | this.store.dispatch(ActivityNodeDataActions.SetCurrentNodeData(guid)); 424 | } 425 | 426 | decorateSelectedNode(guid: string): ActivityNodeDraw { 427 | if (!guid) {return null;} 428 | 429 | let _node = _.find(this.nodeDrawElements, { guid: guid }); 430 | if (_node) { 431 | // this.selectedNodeElement = _node; 432 | _node.element.attr({ 'stroke-dasharray': '-' }); 433 | let imgSrc = _node.element.attr('src'); 434 | if (imgSrc && imgSrc.indexOf('selected') < 0) { 435 | _node.element.attr({ src: imgSrc.replace('.png', '_selected.png') }); 436 | } 437 | } else { 438 | console.error('Cannot find node ' + guid); 439 | } 440 | return _node; 441 | } 442 | 443 | unselectNode() { 444 | this.selectNode(null); 445 | } 446 | 447 | addNode(x: number, y: number): ActivityNodeData { 448 | if (this.drawActivityType && this.drawActivityType !== '_') { 449 | let activityNodeData = new ActivityNodeDataObj(this.drawActivityType, [x, y]); 450 | let activityNodeDraw = ActivityNodeDraw.drawFromNodeData(activityNodeData, this); 451 | return activityNodeData; 452 | } 453 | return null; 454 | } 455 | 456 | removeNode(guid: string) { 457 | // 需要先调用removeConnection删除进出该节点的所有连接 458 | let _connectionsData = _.filter(this.connectionsData, (c) => { 459 | return (c.fromGuid === guid || c.toGuid === guid); 460 | }); 461 | _.each(_connectionsData, (c) => { this.removeConnection(c.guid); }); 462 | 463 | let _node = _.find(this.nodeDrawElements, { guid: guid }); 464 | if (_node) { 465 | this.store.dispatch(ActivityNodeDataActions.DeleteNodeData(guid)); 466 | } 467 | } 468 | 469 | clearNodes() { 470 | _.each(this.nodeDrawElements, (node) => { 471 | node.element.remove(); 472 | node.textElement && node.textElement.remove(); 473 | }); 474 | this.nodeDrawElements = []; 475 | } 476 | 477 | addConnection(connection: ActivityConnectionDraw): ActivityConnectionData { 478 | return 479 | { 480 | guid: connection.guid, 481 | name: connection.name, 482 | fromGuid: connection.obj1.data('guid'), 483 | toGuid: connection.obj2.data('guid') 484 | }; 485 | } 486 | 487 | removeConnection(guid: string) { 488 | let _c = _.find(this.connections, { guid: guid }); 489 | if (_c) { 490 | this.store.dispatch(ActivityConnectionDataActions.DeleteConnectionData(guid)); 491 | } 492 | } 493 | 494 | clearConnections() { 495 | _.each(this.connections, (con) => { con.element.remove(); }); 496 | this.connections = []; 497 | } 498 | 499 | findNodeDrawByGuid(guid: string): ActivityNodeDraw { 500 | return _.find(this.nodeDrawElements, (n) => { return n.guid == guid; }); 501 | } 502 | 503 | findNodeDataByGuid(guid: string): ActivityNodeData { 504 | return _.find(this.nodesData, (n) => { return n.guid == guid; }); 505 | } 506 | 507 | startNodeTextEditing(textElement: RaphaelElement) { 508 | const guid: string = textElement.data('guid'); 509 | $('#InputActivityNodeName').css( 510 | { left: textElement.attr('x') - 30, top: textElement.attr('y') - 10, display: 'block' }); 511 | $('#InputActivityNodeName').val(DblClickProcessor.drawingPaper.findNodeDataByGuid(guid).name); 512 | const _nodeDraw: ActivityNodeDraw = DblClickProcessor.drawingPaper.findNodeDrawByGuid(guid); 513 | 514 | DblClickProcessor.drawingPaper.dblClickProcessor.currentEditingNodeGuid = guid; 515 | DblClickProcessor.drawingPaper.isNodeTextEditing = true; 516 | $('#InputActivityNodeName').keypress((e) => {// 按下回车键则确认修改,并退出编辑模式 517 | if (e.keyCode === 13) { 518 | DblClickProcessor.drawingPaper.finishNodeTextEditing(false); 519 | } 520 | }); 521 | DblClickProcessor.drawingPaper.unselectNode(); 522 | DblClickProcessor.drawingPaper.unselectConnection(); 523 | } 524 | 525 | finishNodeTextEditing(cancel = true) { 526 | if (this.isNodeTextEditing && !cancel) { // 非取消编辑,需要确认修改,并退出编辑模式 527 | const _newName = $('#InputActivityNodeName').val(); 528 | const _guid = this.dblClickProcessor.currentEditingNodeGuid; 529 | const _nodeData = Object.assign({}, this.findNodeDataByGuid(_guid), { name: _newName }); 530 | this.store.dispatch(ActivityNodeDataActions.UpdateNodeData(_nodeData)); 531 | } 532 | $('#InputActivityNodeName').css({ display: 'none' }); 533 | this.isNodeTextEditing = false; 534 | } 535 | } 536 | 537 | @Component({ 538 | selector: 'app-flow-chart-parent', 539 | template: ` 540 |
541 | {{drawingPaper.workflowTemplateName}} 542 |
543 | 544 | ` 545 | }) 546 | export class FlowChartParentComponent implements OnInit { 547 | public drawingPaper: DrawingPaper; 548 | 549 | constructor( @Inject(AppStore) private store: Store) { 550 | store.subscribe(() => this.updateStoreState()); 551 | } 552 | 553 | updateStoreState() { 554 | const state = this.store.getState(); 555 | let needRedrawNodes = false, needRedrawConnections = false; 556 | this.drawingPaper.drawActivityType = state.activityTools.currentActivityTool; 557 | if (this.drawingPaper.nodesData !== state.activityDataNodes.activityNodeDatas) { 558 | this.drawingPaper.nodesData = state.activityDataNodes.activityNodeDatas; 559 | needRedrawNodes = true; 560 | } 561 | if (this.drawingPaper.connectionsData !== state.activityConnections.activityConnectionDatas) { 562 | this.drawingPaper.connectionsData = state.activityConnections.activityConnectionDatas; 563 | needRedrawConnections = true; 564 | } 565 | 566 | this.drawingPaper.selectedNodeGuid = state.activityDataNodes.currentNodeGuid; 567 | this.drawingPaper.selectedConnectionGuid = state.activityConnections.currentConnectionGuid; 568 | this.drawingPaper.workflowTemplateName = state.flowData.flowData.basicInfo.name; 569 | 570 | this.drawingPaper.render(); 571 | } 572 | 573 | ngOnInit() { 574 | // 创建绘图板对象 575 | let raphael = Raphael('FlowChart', 576 | $('FlowChart').width(), $('FlowChart').height()); 577 | this.drawingPaper = DragProcessor.drawingPaper 578 | = ClickProcessor.drawingPaper 579 | = DblClickProcessor.drawingPaper 580 | = new DrawingPaper(raphael); 581 | // 初始化↑并创建拖动处理器对象; 582 | let dragProcessor = new DragProcessor(); 583 | this.drawingPaper.dragProcessor = dragProcessor; 584 | // 初始化↑并创建点击事件处理器; 585 | let clickProcessor = new ClickProcessor(); 586 | this.drawingPaper.clickProcessor = clickProcessor; 587 | // 初始化↑并创建双击事件处理器; 588 | let dblClickProcessor = new DblClickProcessor(); 589 | this.drawingPaper.dblClickProcessor = dblClickProcessor; 590 | 591 | // 绘图板对象可以访问用来发送数据state变更通知 592 | this.drawingPaper.store = this.store; 593 | 594 | // 绘图板对象进行流程图绘制(通过store) 595 | this.updateStoreState(); 596 | 597 | // DEL,ESC键的事件处理设置 598 | $('#FlowChart').keydown((e) => { 599 | if (this.drawingPaper.selectedConnectionGuid) {// 目前有被选中的连接, 600 | switch (e.keyCode) { 601 | case 46: // 删除键 602 | this.drawingPaper.removeConnection(this.drawingPaper.selectedConnectionGuid) 603 | break; 604 | case 27: // ESC 605 | this.drawingPaper.unselectConnection(); 606 | break; 607 | default: 608 | } 609 | } 610 | if (this.drawingPaper.selectedNodeGuid) {// 目前有被选中的节点 611 | switch (e.keyCode) { 612 | case 46: // 删除键 613 | this.drawingPaper.removeNode(this.drawingPaper.selectedNodeGuid); 614 | this.drawingPaper.selectedNodeGuid = null; 615 | break; 616 | case 27: // ESC 617 | this.drawingPaper.unselectNode(); 618 | break; 619 | default: 620 | } 621 | } 622 | if (this.drawingPaper.isNodeTextEditing) {// 目前有正在编辑的节点文本输入框 623 | if (e.keyCode === 27) {// ESC则取消编辑 624 | this.drawingPaper.finishNodeTextEditing(true); 625 | } 626 | } 627 | }); 628 | 629 | // 在设计区点击后增加新节点或连接的操作设置 630 | $('#FlowChartParent').click((e) => { 631 | if (e.target.nodeName === 'svg') { 632 | if (this.drawingPaper.drawActivityType && this.drawingPaper.drawActivityType !== '_') { 633 | if (this.drawingPaper.drawActivityType === '_connection') {// 增加节点间连接操作 634 | // notie.alert(2,'请从处理节点开始点击',3); 635 | } else {// 增加节点操作 636 | const _nodeData = this.drawingPaper.addNode(e.offsetX, e.offsetY); 637 | if (_nodeData) { 638 | this.store.dispatch(ActivityNodeDataActions.CreateNodeData( 639 | this.drawingPaper.addNode(e.offsetX, e.offsetY))); 640 | } else { // 不能创建节点,可能是点击空白处,则取消当前选中的活动节点和连接 641 | this.store.dispatch(ActivityNodeDataActions.SetCurrentNodeData('')); 642 | } 643 | } 644 | } else { 645 | this.drawingPaper.unselectConnection(); 646 | this.drawingPaper.unselectNode(); 647 | } 648 | if (this.drawingPaper.isNodeTextEditing) { 649 | this.drawingPaper.finishNodeTextEditing(false); 650 | } 651 | } else { 652 | } 653 | }); 654 | } 655 | } 656 | --------------------------------------------------------------------------------