初始化

This commit is contained in:
caixiaofeng 2023-09-26 10:29:20 +08:00
parent 7f92c1f5e7
commit d808a6cafe
58 changed files with 1752 additions and 0 deletions

14
.gitignore vendored Normal file
View 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

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["Vue.volar"]
}

16
README.md Normal file
View File

@ -0,0 +1,16 @@
<div align="center">
<h1>lowflow-design 低代码流程设计器</h1>
</div>
## 介绍
lowflow-design是一个基于`Vue3``Vite``TypeScript``Element-Plus`等技术栈开发的,适用于低代码或无代码开发平台的流程设计器。
让普通人也能通过简单配置快速搭建流程。
## 示例图
![flow.png](public%2Fflow.png)
![penal.png](public%2Fpenal.png)
## 一键转BPMN暂不开源
![dark.png](public%2Fdark.png)
![bpmn.png](public%2Fbpmn.png)
## 加微信拉入群聊
![wx.jpg](public%2Fwx.jpg)

18
index.html Normal file
View 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
View 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
View File

@ -0,0 +1 @@
vite-starter.element-plus.org

BIN
public/bpmn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
public/penal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

1
public/vite.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

21
src/App.vue Normal file
View 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
View 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
View 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']
}
}

View File

@ -0,0 +1,5 @@
import Segmented from './src/index.vue'
export default Segmented
export * from './src/index.vue'

View 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
View 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
View File

@ -0,0 +1 @@
export * from "./dark";

7
src/env.d.ts vendored Normal file
View 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
View 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");

View File

@ -0,0 +1,11 @@
// only scss variables
$--colors: (
"primary": (
"base": #589ef8,
),
);
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
$colors: $--colors
);

View 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
View 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);
}

View 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

View 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>

View File

@ -0,0 +1,4 @@
import { FlowNode } from '../Node/index'
export interface AddNode extends FlowNode {
}

View 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>

View 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>

View 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'
}

View 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>

View File

@ -0,0 +1,5 @@
import {FlowNode} from '../Node/index'
export interface BranchNode extends FlowNode{
children: FlowNode[];
}

View 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>

View 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>

View File

@ -0,0 +1,4 @@
import { FlowNode } from '../Node/index'
export interface CcNode extends FlowNode {
users: string[]
}

View 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>

View 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>

View File

@ -0,0 +1,4 @@
import { FlowNode } from '../Node/index'
export interface ConditionNode extends FlowNode {
def: boolean
}

View 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>

View File

@ -0,0 +1,4 @@
import { FlowNode } from '../Node/index'
export interface EndNode extends FlowNode {
}

View 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>

View File

@ -0,0 +1,4 @@
import {BranchNode} from '../Branch/index'
export interface ExclusiveNode extends BranchNode {
}

View 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>

View File

@ -0,0 +1,8 @@
export interface FlowNode {
id: string;
pid: string | null;
name: string;
type: string;
child: FlowNode | null;
props?: Record<string, any>;
}

View 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>

View 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>

View File

@ -0,0 +1,4 @@
import { FlowNode } from '../Node/index'
export interface StartNode extends FlowNode {
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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(),
]
}),
],
})