初始化
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
.vite-ssg-temp
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# lock
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
*.log
|
||||
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
<div align="center">
|
||||
<h1>lowflow-design 低代码流程设计器</h1>
|
||||
</div>
|
||||
|
||||
## 介绍
|
||||
lowflow-design是一个基于`Vue3`,`Vite`,`TypeScript`,`Element-Plus`等技术栈开发的,适用于低代码或无代码开发平台的流程设计器。
|
||||
让普通人也能通过简单配置快速搭建流程。
|
||||
|
||||
## 示例图
|
||||

|
||||

|
||||
## 一键转BPMN(暂不开源)
|
||||

|
||||

|
||||
## 加微信拉入群聊
|
||||

|
||||
18
index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>lowflow-design</title>
|
||||
<!-- element css cdn, if you use custom theme, remove it. -->
|
||||
<!-- <link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css"
|
||||
/> -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "lowflow-design",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"generate": "vite-ssg build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-plus": "^2.3.12",
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ep": "^1.1.12",
|
||||
"@types/node": "^20.6.0",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"sass": "^1.66.1",
|
||||
"typescript": "^5.2.2",
|
||||
"unocss": "^0.55.7",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vite": "^4.4.9",
|
||||
"vite-ssg": "^0.23.1",
|
||||
"vue-tsc": "^1.8.11"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
1
public/CNAME
Normal file
@ -0,0 +1 @@
|
||||
vite-starter.element-plus.org
|
||||
BIN
public/bpmn.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/dark.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
1
public/element-plus-logo-small.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{fill:#409eff;fill-rule:evenodd;}</style></defs><title>element plus-logo-small 副本</title><path id="element_plus-logo-small" data-name="element plus-logo-small" class="cls-1" d="M37.41,32.37c0,1.57-.83,1.93-.83,1.93L21.51,43A1.69,1.69,0,0,1,20,43S5.2,34.4,4.66,34a1.29,1.29,0,0,1-.55-1V15.24c0-.78,1-1.33,1-1.33L19.86,5.36a2,2,0,0,1,1.79,0l14.46,8.41a2.06,2.06,0,0,1,1.25,2.06V32.37Zm-5.9-17L21.35,9.5a1.59,1.59,0,0,0-1.41,0L8.33,16.15s-.77.46-.76,1.08,0,13.92,0,13.92A1,1,0,0,0,8,31.9c.43.3,12,7,12,7a1.31,1.31,0,0,0,1.19,0C21.91,38.5,33,32.11,33,32.11s.65-.28.65-1.51V27.13l-13,7.9V32a3.05,3.05,0,0,1,1-2.07L33.2,23a2.44,2.44,0,0,0,.55-1.46V18.43L20.64,26.35v-3.2a2.22,2.22,0,0,1,.83-1.79ZM41.07,4.22a.39.39,0,0,0-.37-.42H38V1.06c0-.16-.26-.22-.53-.22L36,1.08c-.18,0-.31.12-.31.23V3.8H33a.4.4,0,0,0-.36.37v2h3V9c0,.16.26.27.54.23l1.51-.25c.18,0,.29-.13.29-.23V6.14h3Z"/></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
1
public/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{fill:#409eff;fill-rule:evenodd;}</style></defs><title>element plus-logo-small 副本</title><path id="element_plus-logo-small" data-name="element plus-logo-small" class="cls-1" d="M37.41,32.37c0,1.57-.83,1.93-.83,1.93L21.51,43A1.69,1.69,0,0,1,20,43S5.2,34.4,4.66,34a1.29,1.29,0,0,1-.55-1V15.24c0-.78,1-1.33,1-1.33L19.86,5.36a2,2,0,0,1,1.79,0l14.46,8.41a2.06,2.06,0,0,1,1.25,2.06V32.37Zm-5.9-17L21.35,9.5a1.59,1.59,0,0,0-1.41,0L8.33,16.15s-.77.46-.76,1.08,0,13.92,0,13.92A1,1,0,0,0,8,31.9c.43.3,12,7,12,7a1.31,1.31,0,0,0,1.19,0C21.91,38.5,33,32.11,33,32.11s.65-.28.65-1.51V27.13l-13,7.9V32a3.05,3.05,0,0,1,1-2.07L33.2,23a2.44,2.44,0,0,0,.55-1.46V18.43L20.64,26.35v-3.2a2.22,2.22,0,0,1,.83-1.79ZM41.07,4.22a.39.39,0,0,0-.37-.42H38V1.06c0-.16-.26-.22-.53-.22L36,1.08c-.18,0-.31.12-.31.23V3.8H33a.4.4,0,0,0-.36.37v2h3V9c0,.16.26.27.54.23l1.51-.25c.18,0,.29-.13.29-.23V6.14h3Z"/></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
BIN
public/flow.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/penal.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/wx.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
21
src/App.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<el-config-provider namespace="el" :locale="zhCn">
|
||||
<div class="main-container">
|
||||
<FlowDesign/>
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import FlowDesign from '~/views/flowDesign/index.vue'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
|
||||
</script>
|
||||
<style>
|
||||
#app {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
height: calc(100vh);
|
||||
}
|
||||
</style>
|
||||
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 497 B |
31
src/components.d.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
Segmented: typeof import('./components/Segmented/index.ts')['default']
|
||||
}
|
||||
}
|
||||
5
src/components/Segmented/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Segmented from './src/index.vue'
|
||||
|
||||
export default Segmented
|
||||
|
||||
export * from './src/index.vue'
|
||||
103
src/components/Segmented/src/index.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useVModel} from '@vueuse/core'
|
||||
|
||||
interface SegmentedProps {
|
||||
block?: boolean,
|
||||
modelValue?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SegmentedProps>(), {
|
||||
block: true
|
||||
})
|
||||
const emits = defineEmits<
|
||||
(e: 'update:modelValue', value: string) => void
|
||||
>()
|
||||
const data = useVModel(props, 'modelValue', emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tabs v-model="data" :class="['el-segmented',{'is-block': block}]">
|
||||
<slot/>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-segmented {
|
||||
:deep {
|
||||
&.is-block {
|
||||
.el-tabs__header {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
background: var(--el-segmented-bg);
|
||||
border-radius: var(--el-segmented-radius);
|
||||
padding: var(--el-segmented-padding);
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll, .el-tabs__nav-wrap {
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav {
|
||||
float: none;
|
||||
|
||||
&:not(:has(.is-active)) {
|
||||
.el-tabs__active-bar {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 var(--el-segmented-item-padding);
|
||||
color: var(--el-segmented-color);
|
||||
height: var(--el-segmented-height);
|
||||
line-height: var(--el-segmented-height);
|
||||
font-size: var(--el-segmented-font-size);
|
||||
border-radius: var(--el-segmented-radius);
|
||||
transition: color .2s, background-color .2s;
|
||||
background: none;
|
||||
z-index: 2;
|
||||
|
||||
&:not(.is-disabled) {
|
||||
&.is-active {
|
||||
color: var(--el-segmented-active-color) !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-segmented-active-color);
|
||||
background: var(--el-segmented-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 活动栏属性 */
|
||||
.el-tabs__active-bar {
|
||||
padding: 0 var(--el-segmented-item-padding);
|
||||
margin-left: calc(0px - var(--el-segmented-item-padding));
|
||||
background: var(--el-segmented-active-bg);
|
||||
border-radius: var(--el-segmented-radius);
|
||||
box-shadow: var(--el-segmented-active-shadow);
|
||||
transform: translate(var(--el-segmented-item-padding));
|
||||
box-sizing: content-box;
|
||||
height: auto;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
4
src/composables/dark.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { useDark, useToggle } from "@vueuse/core";
|
||||
|
||||
export const isDark = useDark();
|
||||
export const toggleDark = useToggle(isDark);
|
||||
1
src/composables/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./dark";
|
||||
7
src/env.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
20
src/main.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
// import "~/styles/element/index.scss";
|
||||
|
||||
// import ElementPlus from "element-plus";
|
||||
// import all element css, uncommented next line
|
||||
// import "element-plus/dist/index.css";
|
||||
|
||||
// or use cdn, uncomment cdn link in `index.html`
|
||||
|
||||
import "~/styles/index.scss";
|
||||
import "uno.css";
|
||||
|
||||
// If you want to use ElMessage, import it.
|
||||
import "element-plus/theme-chalk/src/message.scss";
|
||||
|
||||
const app = createApp(App);
|
||||
// app.use(ElementPlus);
|
||||
app.mount("#app");
|
||||
11
src/styles/element/dark.scss
Normal file
@ -0,0 +1,11 @@
|
||||
// only scss variables
|
||||
|
||||
$--colors: (
|
||||
"primary": (
|
||||
"base": #589ef8,
|
||||
),
|
||||
);
|
||||
|
||||
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
|
||||
$colors: $--colors
|
||||
);
|
||||
42
src/styles/element/index.scss
Normal file
@ -0,0 +1,42 @@
|
||||
$--colors: (
|
||||
"primary": (
|
||||
"base": #589ef8,
|
||||
),
|
||||
"success": (
|
||||
"base": #21ba45,
|
||||
),
|
||||
"warning": (
|
||||
"base": #f2711c,
|
||||
),
|
||||
"danger": (
|
||||
"base": #db2828,
|
||||
),
|
||||
"error": (
|
||||
"base": #db2828,
|
||||
),
|
||||
"info": (
|
||||
"base": #42b8dd,
|
||||
),
|
||||
);
|
||||
|
||||
// we can add this to custom namespace, default is 'el'
|
||||
@forward "element-plus/theme-chalk/src/mixins/config.scss" with (
|
||||
$namespace: "el"
|
||||
);
|
||||
|
||||
// You should use them in scss, because we calculate it by sass.
|
||||
// comment next lines to use default color
|
||||
@forward "element-plus/theme-chalk/src/common/var.scss" with (
|
||||
// do not use same name, it will override.
|
||||
$colors: $--colors,
|
||||
$button-padding-horizontal: ("default": 50px)
|
||||
);
|
||||
|
||||
// if you want to import all
|
||||
// @use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
|
||||
// You can comment it to hide debug info.
|
||||
// @debug $--colors;
|
||||
|
||||
// custom dark variables
|
||||
@use "./dark.scss";
|
||||
59
src/styles/index.scss
Normal file
@ -0,0 +1,59 @@
|
||||
// import dark theme
|
||||
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
|
||||
|
||||
:root {
|
||||
.el-segmented {
|
||||
--el-segmented-radius: var(--el-border-radius-base);
|
||||
--el-segmented-padding: 3px;
|
||||
--el-segmented-bg: var(--el-fill-color-light);
|
||||
--el-segmented-height: 28px;
|
||||
--el-segmented-font-size: 14px;
|
||||
--el-segmented-item-padding: 12px;
|
||||
--el-segmented-color: var(--el-text-color-secondary);
|
||||
--el-segmented-active-color: var(--el-text-color-primary);
|
||||
--el-segmented-active-bg: var(--el-bg-color-overlay);
|
||||
--el-segmented-active-shadow: 0 1px 3px 0 rgba(0, 0, 0, .08);
|
||||
--el-segmented-hover-bg: rgba(0, 0, 0, .04);
|
||||
--el-segmented-disabled-color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义抽屉样式
|
||||
.el-drawer {
|
||||
// 抽屉头部
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
padding: calc(var(--el-drawer-padding-primary) - 5px) var(--el-drawer-padding-primary) calc(var(--el-drawer-padding-primary) - 6px);
|
||||
border-bottom: 1px var(--el-border-style) var(--el-border-color);
|
||||
justify-content: space-between;
|
||||
// 抽屉标题
|
||||
.el-drawer__title {
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-drawer__footer {
|
||||
border-top: var(--el-border);
|
||||
padding: calc(var(--el-drawer-padding-primary) - 5px)
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, system-ui, Avenir, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
159
src/views/flowDesign/hooks/useNode.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import {FlowNode} from '../nodes/Node/index'
|
||||
import {ExclusiveNode} from '../nodes/Exclusive/index'
|
||||
import {BranchNode} from '../nodes/Branch/index'
|
||||
import {ConditionNode} from '../nodes/Condition/index'
|
||||
import {ApprovalNode} from '../nodes/Approval/index'
|
||||
import {CcNode} from '../nodes/Cc/index'
|
||||
|
||||
const useNode = (node: FlowNode) => {
|
||||
/**
|
||||
* 生成节点id
|
||||
*/
|
||||
const generateId = (): string => {
|
||||
return `field-${Math.random().toString(36).substr(5)}`
|
||||
}
|
||||
/**
|
||||
* 添加条件
|
||||
* @param currentNode
|
||||
*/
|
||||
const addConnection = (currentNode: FlowNode) => {
|
||||
const exclusive = currentNode as ExclusiveNode
|
||||
exclusive.children.unshift({
|
||||
id: generateId(),
|
||||
pid: currentNode.id,
|
||||
type: 'condition',
|
||||
def: false,
|
||||
name: `条件${exclusive.children.length + 1}`,
|
||||
child: null
|
||||
} as ConditionNode)
|
||||
}
|
||||
/**
|
||||
* 添加审批
|
||||
* @param currentNode
|
||||
*/
|
||||
const addApproval = (currentNode: FlowNode) => {
|
||||
const child = currentNode.child
|
||||
const id = generateId()
|
||||
currentNode.child = {
|
||||
id: id,
|
||||
pid: currentNode.id,
|
||||
type: 'approval',
|
||||
name: '审批人',
|
||||
child: child,
|
||||
// 属性
|
||||
assigneeType: 'user',
|
||||
users: [],
|
||||
roles: [],
|
||||
leader: 1,
|
||||
self: false,
|
||||
nobody: 'reject'
|
||||
} as ApprovalNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 添加抄送
|
||||
* @param currentNode
|
||||
*/
|
||||
const addCc = (currentNode: FlowNode) => {
|
||||
const child = currentNode.child
|
||||
const id = generateId()
|
||||
currentNode.child = {
|
||||
id: id,
|
||||
pid: currentNode.id,
|
||||
type: 'cc',
|
||||
name: '抄送人',
|
||||
child: child,
|
||||
users: []
|
||||
} as CcNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 添加网关
|
||||
* @param currentNode
|
||||
*/
|
||||
const addExclusive = (currentNode: FlowNode) => {
|
||||
const child = currentNode.child
|
||||
const id = generateId()
|
||||
const exclusiveNode = {
|
||||
id: id,
|
||||
pid: currentNode.id,
|
||||
type: 'exclusive',
|
||||
name: '独占网关',
|
||||
child: child,
|
||||
children: []
|
||||
} as ExclusiveNode
|
||||
currentNode.child = exclusiveNode
|
||||
if (child) {
|
||||
child.pid = id
|
||||
}
|
||||
addConnection(currentNode.child)
|
||||
addConnection(currentNode.child)
|
||||
if (exclusiveNode.children.length > 0) {
|
||||
const condition = exclusiveNode.children[exclusiveNode.children.length - 1] as ConditionNode
|
||||
condition.def = true
|
||||
condition.name = '默认条件'
|
||||
}
|
||||
}
|
||||
|
||||
const addNodes: Record<string, (currentNode: FlowNode) => void> = {
|
||||
condition: addConnection,
|
||||
approval: addApproval,
|
||||
cc: addCc,
|
||||
exclusive: addExclusive
|
||||
}
|
||||
|
||||
const addNode = (type: string, currentNode: FlowNode) => {
|
||||
const fun = addNodes[type]
|
||||
if (fun) {
|
||||
fun(currentNode)
|
||||
}
|
||||
}
|
||||
|
||||
const delNode = (del: FlowNode) => {
|
||||
delNodeNext(node, del)
|
||||
}
|
||||
const delNodeNext = (next: FlowNode, del: FlowNode) => {
|
||||
if (next.id === del.pid) {
|
||||
if ('children' in next && next.child?.id !== del.id) {
|
||||
const branchNode = next as BranchNode
|
||||
const index = branchNode.children.findIndex(item => item.id === del.id)
|
||||
if (index !== -1) {
|
||||
if (branchNode.children.length <= 2) {
|
||||
delNode(branchNode)
|
||||
} else {
|
||||
branchNode.children.splice(index, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (del.child && del.child.pid) {
|
||||
del.child.pid = next.id
|
||||
}
|
||||
next.child = del.child
|
||||
}
|
||||
} else {
|
||||
if (next.child) {
|
||||
delNodeNext(next.child, del)
|
||||
}
|
||||
if ('children' in next) {
|
||||
const nextBranch = next as BranchNode
|
||||
if (nextBranch.children && nextBranch.children.length > 0) {
|
||||
nextBranch.children.forEach(item => {
|
||||
delNodeNext(item, del)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addNode,
|
||||
delNode
|
||||
}
|
||||
}
|
||||
|
||||
export default useNode
|
||||
112
src/views/flowDesign/index.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<script setup lang="ts" name="flowDesign">
|
||||
import NodeTree from './nodes/index.vue'
|
||||
import NodePenal from './penal/index.vue'
|
||||
import {FlowNode} from './nodes/Node/index'
|
||||
import useNode from './hooks/useNode'
|
||||
import {computed, onUnmounted, provide, reactive, ref} from "vue";
|
||||
import {Plus,Minus} from "@element-plus/icons-vue";
|
||||
|
||||
const nodePenalRef = ref<InstanceType<typeof NodePenal>>()
|
||||
const nodeTreeObj = reactive<FlowNode>({
|
||||
id: 'root',
|
||||
pid: null,
|
||||
type: 'start',
|
||||
name: '发起人',
|
||||
child: {
|
||||
id: 'end',
|
||||
pid: 'root',
|
||||
type: 'end',
|
||||
name: '结束',
|
||||
child: null
|
||||
}
|
||||
})
|
||||
const zoom = ref(100)
|
||||
const getScale = computed(() => zoom.value / 100)
|
||||
const openPenal = (node: FlowNode) => {
|
||||
nodePenalRef.value?.open(node)
|
||||
}
|
||||
const {addNode, delNode} = useNode(nodeTreeObj)
|
||||
provide('nodeHooks', {
|
||||
readOnly: false,
|
||||
addNode,
|
||||
delNode,
|
||||
openPenal
|
||||
})
|
||||
const handleZoom = (e: WheelEvent) => {
|
||||
if (e.shiftKey) {
|
||||
if (e.deltaY > 0) {
|
||||
if (zoom.value > 50) {
|
||||
zoom.value -= 10
|
||||
}
|
||||
} else {
|
||||
if (zoom.value < 170) {
|
||||
zoom.value += 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const convert = () => {
|
||||
console.log(nodeTreeObj)
|
||||
const process = {
|
||||
id: 'dawdawdw',
|
||||
name: '测试流程',
|
||||
process: nodeTreeObj
|
||||
}
|
||||
}
|
||||
// 按住shift键滚动鼠标滚轮,可以放大/缩小
|
||||
window.addEventListener('wheel', handleZoom)
|
||||
// 离开页面时,销毁事件监听
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('wheel', handleZoom)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="designer-container">
|
||||
<!--放大/缩小-->
|
||||
<div class="zoom">
|
||||
<el-button :icon="Plus" @click="zoom += 10" :disabled="zoom >= 170" circle></el-button>
|
||||
<span>{{ zoom }}%</span>
|
||||
<el-button :icon="Minus" @click="zoom -= 10" circle :disabled="zoom <= 50"></el-button>
|
||||
</div>
|
||||
<!--流程树-->
|
||||
<div class="node-container">
|
||||
<NodeTree :node="nodeTreeObj" />
|
||||
</div>
|
||||
|
||||
<!--属性面板-->
|
||||
<NodePenal ref="nodePenalRef"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.designer-container {
|
||||
background-color: var(--el-bg-color);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
// overflow: scroll;
|
||||
|
||||
.zoom {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 40px;
|
||||
right: 30px;
|
||||
|
||||
span {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.node-container {
|
||||
margin: 0 auto;
|
||||
transform: scale(v-bind(getScale));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/views/flowDesign/nodes/Add/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { FlowNode } from '../Node/index'
|
||||
export interface AddNode extends FlowNode {
|
||||
|
||||
}
|
||||
117
src/views/flowDesign/nodes/Add/index.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import {type ElPopover} from 'element-plus'
|
||||
import {ref} from "vue";
|
||||
import {Stamp,Promotion,Share,Plus} from "@element-plus/icons-vue";
|
||||
const nodePopoverRef = ref<InstanceType<typeof ElPopover>>()
|
||||
const $emits = defineEmits<{
|
||||
(e: 'addNode', type: string): void
|
||||
}>()
|
||||
const addApprovalNode = () => {
|
||||
$emits('addNode', 'approval')
|
||||
nodePopoverRef.value?.hide()
|
||||
}
|
||||
const addCcNode = () => {
|
||||
$emits('addNode', 'cc')
|
||||
nodePopoverRef.value?.hide()
|
||||
}
|
||||
const addExclusiveNode = () => {
|
||||
$emits('addNode', 'exclusive')
|
||||
nodePopoverRef.value?.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="add-but">
|
||||
<el-popover placement="bottom-start" ref="nodePopoverRef" trigger="click" title="添加节点" :width="235">
|
||||
<div class="node-select">
|
||||
<div @click="addApprovalNode">
|
||||
<el-icon color="#ff943e">
|
||||
<Stamp/>
|
||||
</el-icon>
|
||||
<span>审批人</span>
|
||||
</div>
|
||||
<div @click="addCcNode">
|
||||
<el-icon color="#3296fa">
|
||||
<Promotion/>
|
||||
</el-icon>
|
||||
<span>抄送人</span>
|
||||
</div>
|
||||
<div @click="addExclusiveNode">
|
||||
<el-icon>
|
||||
<Share/>
|
||||
</el-icon>
|
||||
<span>互斥分支</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button
|
||||
:icon="Plus"
|
||||
type="primary"
|
||||
style="z-index: 1"
|
||||
circle></el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-select {
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
margin: 5px 5px;
|
||||
cursor: pointer;
|
||||
padding: 10px 15px;
|
||||
border: var(--el-border);
|
||||
border-radius: 10px;
|
||||
width: 170px;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
left: 65px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 25px;
|
||||
padding: 5px;
|
||||
border: var(--el-border);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-but {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 20px 0 32px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
// background-color: var(--el-border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/views/flowDesign/nodes/Approval/content.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import {ApprovalNode} from './index'
|
||||
import {ref, watchEffect} from "vue";
|
||||
|
||||
export interface ApprovalContentProps {
|
||||
node: ApprovalNode
|
||||
}
|
||||
|
||||
const $props = withDefaults(defineProps<ApprovalContentProps>(), {})
|
||||
const content = ref<string>('')
|
||||
watchEffect(() => {
|
||||
const props = $props.node
|
||||
if (props.assigneeType === 'self_select') {
|
||||
content.value = '发起人自选'
|
||||
} else if (props.assigneeType === 'self') {
|
||||
content.value = '发起人自己'
|
||||
} else if (props.assigneeType === 'leader') {
|
||||
content.value = props.leader === 1 ? '直属上级' : `${props.leader}级上级`
|
||||
} else if (props.assigneeType === 'form_user') {
|
||||
content.value = '表单内人员'
|
||||
} else if (props.assigneeType === 'user') {
|
||||
if (props.users.length > 0) {
|
||||
// 可以从异步后端查询出用户名,显示在content中
|
||||
} else {
|
||||
content.value = '未指定人员'
|
||||
}
|
||||
} else if (props.assigneeType === 'role') {
|
||||
if (props.roles.length > 0) {
|
||||
// 可以从异步后端查询出角色名,显示在content中
|
||||
} else {
|
||||
content.value = '未指定角色'
|
||||
}
|
||||
} else {
|
||||
content.value = $props.node.name
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-text>
|
||||
{{ content || node.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
16
src/views/flowDesign/nodes/Approval/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {FlowNode} from '../Node/index'
|
||||
|
||||
export interface ApprovalNode extends FlowNode {
|
||||
// 审批方式
|
||||
assigneeType: 'user' | 'role' | 'self_select' | 'self' | 'leader' | 'form_user'
|
||||
// 审批人
|
||||
users: string[]
|
||||
// 审批角色
|
||||
roles: string[]
|
||||
// 主管
|
||||
leader: number
|
||||
// 自选:true-多选,false-单选
|
||||
self: boolean
|
||||
// 审批人为空时处理方式:reject-驳回,admin-管理员,pass-通过
|
||||
nobody: 'reject' | 'pass'
|
||||
}
|
||||
23
src/views/flowDesign/nodes/Approval/index.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import Node from '../Node/index.vue'
|
||||
import Content from './content.vue'
|
||||
import {ApprovalNode} from './index'
|
||||
|
||||
export interface ApprovalProps {
|
||||
node: ApprovalNode
|
||||
}
|
||||
|
||||
withDefaults(defineProps<ApprovalProps>(), {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Node icon="Stamp"
|
||||
color="linear-gradient(89.96deg, #FA6F32 .05%, #FB9337 79.83%)"
|
||||
:node="node">
|
||||
<Content :node="node"/>
|
||||
</Node>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
5
src/views/flowDesign/nodes/Branch/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {FlowNode} from '../Node/index'
|
||||
|
||||
export interface BranchNode extends FlowNode{
|
||||
children: FlowNode[];
|
||||
}
|
||||
128
src/views/flowDesign/nodes/Branch/index.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import NodeTree from '../index.vue'
|
||||
import AddBut from '../Add/index.vue'
|
||||
import {BranchNode} from './index'
|
||||
import {FlowNode} from '../Node/index'
|
||||
import {inject} from "vue";
|
||||
|
||||
export interface BranchProps {
|
||||
node: BranchNode
|
||||
}
|
||||
|
||||
withDefaults(defineProps<BranchProps>(), {})
|
||||
const {addNode} = inject<{
|
||||
addNode: (type: string, node: FlowNode) => void
|
||||
}>('nodeHooks')!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="branch-node">
|
||||
<!--添加新分支按钮-->
|
||||
<div class="add-branch">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<!--分支节点-->
|
||||
<div v-for="(item,index) in node.children" :key="item.id" class="col-box">
|
||||
<template v-if="node.children.length===(index+1)">
|
||||
<div :class="`top-right-border`"></div>
|
||||
<div :class="`bottom-right-border`"/>
|
||||
</template>
|
||||
<template v-if="index===0">
|
||||
<div :class="`top-left-border`"></div>
|
||||
<div :class="`bottom-left-border`"/>
|
||||
</template>
|
||||
<node-tree :node="item"/>
|
||||
</div>
|
||||
</div>
|
||||
<!--添加节点-->
|
||||
<add-but @add-node="(type:string)=>addNode(type,node)" class="branch-but"/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.branch-node {
|
||||
display: flex;
|
||||
border-top: var(--el-border);
|
||||
border-bottom: var(--el-border);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
.add-branch {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -15px;
|
||||
z-index: 2;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.col-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--el-bg-color);
|
||||
&:before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.top-left-border {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -2px;
|
||||
height: 3px;
|
||||
width: 50%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.bottom-left-border {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -2px;
|
||||
height: 3px;
|
||||
width: 50%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.top-right-border {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -2px;
|
||||
height: 3px;
|
||||
width: 50%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.bottom-right-border {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -2px;
|
||||
height: 3px;
|
||||
width: 50%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.branch-but {
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
30
src/views/flowDesign/nodes/Cc/content.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import {CcNode} from './index'
|
||||
import {ref, watchEffect} from "vue";
|
||||
|
||||
export interface CcContentProps {
|
||||
node: CcNode
|
||||
}
|
||||
|
||||
const $props = withDefaults(defineProps<CcContentProps>(), {})
|
||||
const content = ref<string>('')
|
||||
watchEffect(() => {
|
||||
const props = $props.node
|
||||
if (props.users.length > 0) {
|
||||
// 可以从异步后端查询出用户名,显示在content中
|
||||
} else {
|
||||
content.value = '未指定人员'
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-text>
|
||||
{{ content || node.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
4
src/views/flowDesign/nodes/Cc/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { FlowNode } from '../Node/index'
|
||||
export interface CcNode extends FlowNode {
|
||||
users: string[]
|
||||
}
|
||||
24
src/views/flowDesign/nodes/Cc/index.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import Node from '../Node/index.vue'
|
||||
import {CcNode} from '../Cc/index'
|
||||
import Content from './content.vue'
|
||||
|
||||
export interface ApprovalProps {
|
||||
node: CcNode
|
||||
}
|
||||
|
||||
withDefaults(defineProps<ApprovalProps>(), {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Node icon="Promotion"
|
||||
v-bind="$attrs"
|
||||
color="rgb(50, 150, 250)"
|
||||
:node="node">
|
||||
<Content :node="node"/>
|
||||
</Node>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
29
src/views/flowDesign/nodes/Condition/content.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import {ConditionNode} from './index'
|
||||
import {ref, watchEffect} from "vue";
|
||||
|
||||
export interface ConditionContentProps {
|
||||
node: ConditionNode
|
||||
}
|
||||
|
||||
const $props = withDefaults(defineProps<ConditionContentProps>(), {})
|
||||
const content = ref<string>('')
|
||||
watchEffect(() => {
|
||||
if ($props.node.def) {
|
||||
content.value = '不满足条件时,进入默认条件'
|
||||
} else {
|
||||
content.value = '未设置条件'
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-text>
|
||||
{{ content || node.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
4
src/views/flowDesign/nodes/Condition/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { FlowNode } from '../Node/index'
|
||||
export interface ConditionNode extends FlowNode {
|
||||
def: boolean
|
||||
}
|
||||
30
src/views/flowDesign/nodes/Condition/index.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import Node from '../Node/index.vue'
|
||||
import {ConditionNode} from '../Condition/index'
|
||||
import Content from './content.vue'
|
||||
|
||||
export interface ApprovalProps {
|
||||
node: ConditionNode
|
||||
}
|
||||
|
||||
withDefaults(defineProps<ApprovalProps>(), {})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="condition-box">
|
||||
<Node icon="Share"
|
||||
:readOnly="node.def"
|
||||
:node="node">
|
||||
<Content :node="node"/>
|
||||
</Node>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.condition-box{
|
||||
:deep(.node-box){
|
||||
padding: 30px 50px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/views/flowDesign/nodes/End/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { FlowNode } from '../Node/index'
|
||||
export interface EndNode extends FlowNode {
|
||||
|
||||
}
|
||||
35
src/views/flowDesign/nodes/End/index.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-box">
|
||||
<el-text class="end-node">
|
||||
结束
|
||||
</el-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-box {
|
||||
padding: 0 50px 50px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
.end-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
font-size: 17px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
background-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px 0 var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/views/flowDesign/nodes/Exclusive/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import {BranchNode} from '../Branch/index'
|
||||
export interface ExclusiveNode extends BranchNode {
|
||||
|
||||
}
|
||||
25
src/views/flowDesign/nodes/Exclusive/index.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import Branch from '../Branch/index.vue'
|
||||
import {ExclusiveNode} from '../Exclusive/index'
|
||||
import {FlowNode} from '../Node/index'
|
||||
import {inject} from "vue";
|
||||
|
||||
export interface ExclusiveProps {
|
||||
node: ExclusiveNode
|
||||
}
|
||||
|
||||
withDefaults(defineProps<ExclusiveProps>(), {})
|
||||
const {addNode} = inject<{
|
||||
addNode: (type: string, node: FlowNode) => void
|
||||
}>('nodeHooks')!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Branch :node="node">
|
||||
<el-button type="primary" @click="addNode('condition',node)" plain round>添加条件</el-button>
|
||||
</Branch>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
8
src/views/flowDesign/nodes/Node/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface FlowNode {
|
||||
id: string;
|
||||
pid: string | null;
|
||||
name: string;
|
||||
type: string;
|
||||
child: FlowNode | null;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
160
src/views/flowDesign/nodes/Node/index.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import AddBut from '../Add/index.vue'
|
||||
import {useVModels} from '@vueuse/core'
|
||||
import {ClickOutside as vClickOutside, componentSizeMap, ElInput, useFormSize} from 'element-plus'
|
||||
import {FlowNode} from './index'
|
||||
import {computed, inject, nextTick, ref} from "vue";
|
||||
import {List,Stamp,Promotion,EditPen,CircleClose} from "@element-plus/icons-vue";
|
||||
|
||||
export interface NodeProps {
|
||||
icon?: string
|
||||
node: FlowNode
|
||||
color?: string
|
||||
readOnly?: boolean
|
||||
close?: boolean
|
||||
}
|
||||
|
||||
const $props = withDefaults(defineProps<NodeProps>(), {
|
||||
readOnly: false,
|
||||
close: true
|
||||
})
|
||||
const $emits = defineEmits<{
|
||||
(e: 'update:node', title: string): void
|
||||
}>()
|
||||
const {node} = useVModels($props, $emits)
|
||||
const showInput = ref(false)
|
||||
const inputRef = ref<InstanceType<typeof ElInput>>()
|
||||
const formSize = useFormSize()
|
||||
const getComponentWidth = computed<string>(() => {
|
||||
return componentSizeMap[formSize.value || 'default'] + 205 + 'px'
|
||||
})
|
||||
const getComponentHeight = computed<string>(() => {
|
||||
return componentSizeMap[formSize.value || 'default'] + 60 + 'px'
|
||||
})
|
||||
const {addNode, delNode, openPenal} = inject<{
|
||||
addNode: (type: string, currentNode: FlowNode) => void
|
||||
delNode: (node: FlowNode) => void
|
||||
openPenal: (node: FlowNode) => void
|
||||
}>('nodeHooks')!
|
||||
const onOpenPenal = () => {
|
||||
if ($props.readOnly) return
|
||||
openPenal(node.value)
|
||||
}
|
||||
const onShowInput = () => {
|
||||
if ($props.readOnly) return
|
||||
showInput.value = true
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
const onClickOutside = () => {
|
||||
if (showInput.value) {
|
||||
showInput.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-box">
|
||||
<el-card shadow="always" @click="onOpenPenal()" class="node">
|
||||
<template #header>
|
||||
<div class="head">
|
||||
<el-input ref="inputRef"
|
||||
v-click-outside="onClickOutside"
|
||||
@blur="onClickOutside"
|
||||
maxlength="30"
|
||||
v-model="node.name"
|
||||
v-if="showInput"/>
|
||||
<el-text tag="b" truncated v-else @click.stop="onShowInput">
|
||||
{{ node.name }}
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-text>
|
||||
<span v-if="icon">
|
||||
<el-icon :size="30" color="node-icon">
|
||||
<component :is="icon"/>
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<span @click.stop>
|
||||
<el-popconfirm title="您确定要删除该节点吗?"
|
||||
width="200"
|
||||
:hide-after="0"
|
||||
placement="right-start"
|
||||
@confirm="delNode(node)">
|
||||
<template #reference>
|
||||
<el-button class='node-close'
|
||||
v-show="close && !readOnly"
|
||||
plain circle
|
||||
:icon="CircleClose"
|
||||
size="small"
|
||||
type="danger"/>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</span>
|
||||
<slot></slot>
|
||||
</el-card>
|
||||
<add-but @add-node="(type:string)=>addNode(type,node)"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-box {
|
||||
position: relative;
|
||||
&:before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--el-border-color);
|
||||
}
|
||||
.node {
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
min-height: v-bind(getComponentHeight);
|
||||
width: v-bind(getComponentWidth);
|
||||
|
||||
.node-close {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 5px 0 var(--el-color-primary);
|
||||
|
||||
.node-close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: calc(var(--el-card-padding) - 18px) calc(var(--el-card-padding) - 13px);
|
||||
background: v-bind(color);
|
||||
border-radius: 7px 7px 0 0;
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: var(--el-card-bg-color);
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/views/flowDesign/nodes/Start/content.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import {StartNode} from './index'
|
||||
import {ref, watchEffect} from "vue";
|
||||
|
||||
export interface StartContentProps {
|
||||
node: StartNode
|
||||
}
|
||||
|
||||
const $props = withDefaults(defineProps<StartContentProps>(), {})
|
||||
const content = ref<string>('')
|
||||
watchEffect(() => {
|
||||
content.value = $props.node.name
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-text>
|
||||
{{ content || node.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
4
src/views/flowDesign/nodes/Start/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { FlowNode } from '../Node/index'
|
||||
export interface StartNode extends FlowNode {
|
||||
|
||||
}
|
||||
28
src/views/flowDesign/nodes/Start/index.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import Node from '../Node/index.vue'
|
||||
import {FlowNode} from '../Node/index'
|
||||
import Content from './content.vue'
|
||||
|
||||
export interface StartProps {
|
||||
node: FlowNode
|
||||
}
|
||||
|
||||
withDefaults(defineProps<StartProps>(), {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="start-box">
|
||||
<Node icon="List"
|
||||
:close="false"
|
||||
color="linear-gradient(89.96deg, #7B68EE .05%, #7B68EE 79.83%)"
|
||||
:node="node">
|
||||
<Content :node="node"/>
|
||||
</Node>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.start-box {
|
||||
padding: 50px 0 0;
|
||||
}
|
||||
</style>
|
||||
36
src/views/flowDesign/nodes/index.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<script setup lang="ts" name="NodeTree">
|
||||
import StartNode from './Start/index.vue'
|
||||
import ApprovalNode from './Approval/index.vue'
|
||||
import CcNode from './Cc/index.vue'
|
||||
import ConditionNode from './Condition/index.vue'
|
||||
import ExclusiveNode from './Exclusive/index.vue'
|
||||
import EndNode from './End/index.vue'
|
||||
import {type Component} from 'vue'
|
||||
import {FlowNode} from './Node/index'
|
||||
|
||||
defineProps<{
|
||||
node: FlowNode
|
||||
}>()
|
||||
const nodes: Record<string, Component> = {
|
||||
start: StartNode,
|
||||
approval: ApprovalNode,
|
||||
cc: CcNode,
|
||||
condition: ConditionNode,
|
||||
exclusive: ExclusiveNode,
|
||||
end: EndNode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot/>
|
||||
<component
|
||||
:node="node"
|
||||
:is="nodes[node.type]"/>
|
||||
<NodeTree v-if="node.child" :node="node.child"/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.node-container {
|
||||
|
||||
}
|
||||
</style>
|
||||
87
src/views/flowDesign/penal/ApprovalAttr.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import Segmented from '~/components/Segmented'
|
||||
import {useVModels} from '@vueuse/core'
|
||||
import {ApprovalNode} from '../nodes/Approval/index'
|
||||
import {ref} from "vue";
|
||||
const activeName = ref('properties')
|
||||
export interface ApprovalAttr {
|
||||
node: ApprovalNode
|
||||
}
|
||||
const $props = defineProps<ApprovalAttr>()
|
||||
const $emits = defineEmits<{
|
||||
(e: 'update:node', modelValue: ApprovalNode): void
|
||||
}>()
|
||||
const {node} = useVModels($props, $emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<segmented v-model="activeName" stretch :block="false">
|
||||
<el-tab-pane label="设置审批人" name="properties">
|
||||
<el-form label-position="top" label-width="90px">
|
||||
<el-form-item prop="assigneeType" label="审批对象">
|
||||
<el-radio-group v-model="node.assigneeType">
|
||||
<el-radio label="user">指定人员</el-radio>
|
||||
<el-radio label="role">指定角色</el-radio>
|
||||
<el-radio label="self_select">发起人自选</el-radio>
|
||||
<el-radio label="self">发起人自己</el-radio>
|
||||
<el-radio label="leader">多级上级</el-radio>
|
||||
<el-radio label="form_user">表单内人员</el-radio>
|
||||
<el-radio label="form_role">表单内角色</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="assigneeType" label="指定人员" v-if="node.assigneeType === 'user'">
|
||||
待添加...
|
||||
</el-form-item>
|
||||
<el-form-item prop="selfSelect" label="发起人自选择" v-if="node.assigneeType === 'self_select'">
|
||||
<el-radio-group v-model="node.self">
|
||||
<el-radio-button :label="false">单选</el-radio-button>
|
||||
<el-radio-button :label="true">多选</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="self" label="发起人自己" v-if="node.assigneeType === 'self'">
|
||||
<el-text>
|
||||
发起人自己作为审批人进行审批
|
||||
</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item prop="leader" label="直属上级" v-if="node.assigneeType === 'leader'">
|
||||
<el-select v-model="node.leader" placeholder="请选择直属上级">
|
||||
<el-option label="直属上级" :value="1"></el-option>
|
||||
<el-option label="二级上级" :value="2"></el-option>
|
||||
<el-option label="三级上级" :value="3"></el-option>
|
||||
<el-option label="四级上级" :value="4"></el-option>
|
||||
<el-option label="五级上级" :value="5"></el-option>
|
||||
<el-option label="六级上级" :value="6"></el-option>
|
||||
<el-option label="七级上级" :value="7"></el-option>
|
||||
<el-option label="八级上级" :value="8"></el-option>
|
||||
<el-option label="九级上级" :value="9"></el-option>
|
||||
<el-option label="十级上级" :value="10"></el-option>
|
||||
<el-option label="十一级上级" :value="11"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="role" label="系统角色" v-if="node.assigneeType === 'role'">
|
||||
待添加...
|
||||
</el-form-item>
|
||||
<el-form-item prop="assignedUser" label="表单内人员" v-if="node.assigneeType === 'form_user'">
|
||||
待添加...
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="handler" label="审批人为空">
|
||||
<el-radio-group v-model="node.nobody">
|
||||
<el-radio label="pass">自动通过</el-radio>
|
||||
<el-radio label="reject">自动驳回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="表单权限" name="formPermissions">
|
||||
表单权限设置
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="操作权限" name="operationPermissions">
|
||||
操作权限
|
||||
</el-tab-pane>
|
||||
</segmented>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
34
src/views/flowDesign/penal/CcAttr.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import Segmented from '~/components/Segmented'
|
||||
import {useVModels} from '@vueuse/core'
|
||||
import {CcNode} from '../nodes/Cc/index'
|
||||
import {ref} from "vue";
|
||||
const activeName = ref('properties')
|
||||
export interface ApprovalAttr {
|
||||
node: CcNode
|
||||
}
|
||||
const $props = defineProps<ApprovalAttr>()
|
||||
const $emits = defineEmits<{
|
||||
(e: 'update:node', modelValue: CcNode): void
|
||||
}>()
|
||||
const {node} = useVModels($props, $emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<segmented v-model="activeName" stretch :block="false">
|
||||
<el-tab-pane label="设置从抄送人" name="properties">
|
||||
<el-form label-position="top" label-width="90px">
|
||||
<el-form-item prop="assigneeType" label="抄送人">
|
||||
待添加...
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="表单权限" name="formPermissions">
|
||||
表单权限设置
|
||||
</el-tab-pane>
|
||||
</segmented>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
30
src/views/flowDesign/penal/StartAttr.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import Segmented from '~/components/Segmented'
|
||||
import {useVModels} from '@vueuse/core'
|
||||
import {StartNode} from '../nodes/Start/index'
|
||||
import {ref} from "vue";
|
||||
const activeName = ref('formPermissions')
|
||||
export interface ApprovalAttr {
|
||||
node: StartNode
|
||||
}
|
||||
const $props = defineProps<ApprovalAttr>()
|
||||
const $emits = defineEmits<{
|
||||
(e: 'update:node', modelValue: StartNode): void
|
||||
}>()
|
||||
const {node} = useVModels($props, $emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<segmented v-model="activeName" stretch :block="false">
|
||||
<el-tab-pane label="表单权限" name="formPermissions">
|
||||
表单权限设置
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="操作权限" name="operationPermissions">
|
||||
操作权限设置
|
||||
</el-tab-pane>
|
||||
</segmented>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
56
src/views/flowDesign/penal/index.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import {ClickOutside as vClickOutside} from 'element-plus'
|
||||
import {FlowNode} from '../nodes/Node/index'
|
||||
import {type Component, ref} from 'vue'
|
||||
import ApprovalAttr from './ApprovalAttr.vue'
|
||||
import CcAttr from './CcAttr.vue'
|
||||
import StartAttr from './StartAttr.vue'
|
||||
|
||||
const nodeProps: Record<string, Component> = {
|
||||
start: StartAttr,
|
||||
approval: ApprovalAttr,
|
||||
cc: CcAttr
|
||||
}
|
||||
|
||||
let flowNode = ref<FlowNode | undefined>()
|
||||
const visible = ref(false)
|
||||
const showInput = ref(false)
|
||||
const onClickOutside = () => {
|
||||
if (showInput.value) {
|
||||
showInput.value = false
|
||||
}
|
||||
}
|
||||
const open = (node: FlowNode) => {
|
||||
flowNode.value = node
|
||||
visible.value = true
|
||||
}
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-drawer v-model="visible" v-if="visible" size="600px">
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<span :id="titleId" :class="titleClass">
|
||||
<el-input v-click-outside="onClickOutside" @blur="onClickOutside" maxlength="30" v-model="flowNode!.name"
|
||||
v-show="showInput"
|
||||
></el-input>
|
||||
<el-link icon="EditPen" v-show="!showInput" @click="showInput = true">
|
||||
{{ flowNode!.name }}
|
||||
</el-link>
|
||||
</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<component
|
||||
:node="flowNode"
|
||||
:is="nodeProps[flowNode!.type]"/>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-tabs__content) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
},
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 3
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
67
vite.config.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
|
||||
|
||||
import Unocss from 'unocss/vite'
|
||||
import {
|
||||
presetAttributify,
|
||||
presetIcons,
|
||||
presetUno,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss'
|
||||
|
||||
const pathSrc = path.resolve(__dirname, 'src')
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${pathSrc}/`,
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "~/styles/element/index.scss" as *;`,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
VueSetupExtend(),
|
||||
Components({
|
||||
// allow auto load markdown components under `./src/components/`
|
||||
extensions: ['ts'],
|
||||
// allow auto import and register components used in markdown
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.[tj]sx?$/],
|
||||
resolvers: [
|
||||
ElementPlusResolver({
|
||||
importStyle: 'sass',
|
||||
}),
|
||||
],
|
||||
dts: 'src/components.d.ts',
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unocss
|
||||
// see unocss.config.ts for config
|
||||
Unocss({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetAttributify(),
|
||||
presetIcons({
|
||||
scale: 1.2,
|
||||
warn: true,
|
||||
}),
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives(),
|
||||
transformerVariantGroup(),
|
||||
]
|
||||
}),
|
||||
],
|
||||
})
|
||||