feat: 初始化工程

This commit is contained in:
wanglihui 2023-07-08 15:23:24 +08:00
commit da6329608b
126 changed files with 18188 additions and 0 deletions

15
.editorconfig Executable file
View File

@ -0,0 +1,15 @@
# @see: http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
indent_style = space # 缩进风格tab | space
indent_size = 2 # 缩进大小
max_line_length = 130 # 最大行长度
[*.md] # 表示仅对 md 文件适用以下规则
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

3
.eslintignore Executable file
View File

@ -0,0 +1,3 @@
node_modules
public
dist

49
.eslintrc.js Executable file
View File

@ -0,0 +1,49 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true
},
parser: 'vue-eslint-parser',
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
// eslint-config-prettier 的缩写
'prettier',
'vue-global-api'
],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
// eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写
plugins: ['vue', '@typescript-eslint', 'prettier'],
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'vue/multi-word-component-names': 'off',
'no-undef': 'off'
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly'
}
};

24
.gitignore vendored Executable file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

21
.prettierrc.js Executable file
View File

@ -0,0 +1,21 @@
module.exports = {
printWidth: 130, // 超过最大值换行
tabWidth: 2, // 缩进字节数
useTabs: false, // 缩进使用tab不使用空格
semi: true, // 句尾添加分号
singleQuote: true, // 使用单引号代替双引号
proseWrap: "preserve", // 默认值。因为使用了一些折行敏感型的渲染器如GitHub comment而按照markdown文本样式进行折行
arrowParens: "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid省略括号
bracketSpacing: true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
endOfLine: "auto", // 结尾是 \n \r \n\r auto
jsxSingleQuote: false, // 在jsx中使用单引号代替双引号
trailingComma: "none", // 在对象或数组最后一个元素后面是否加逗号在ES5中加尾逗号
overrides: [
{
files: "*.html",
options: {
parser: "html",
},
},
],
};

3
.vscode/extensions.json vendored Executable file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

50
README.md Executable file
View File

@ -0,0 +1,50 @@
# dentist-fe
* NODE_ENV node环境变量
* APP_ENV 应用环境变量
## 安装
```sh
pnpm install
```
## 本地开发
``` sh
pnpm dev
```
## 编译
``` sh
pnpm build
```
## 本地预览
> 先执行编译再执行该命令
``` sh
pnpm serve
```
## eslint检测
``` sh
pnpm lint
```
## eslint修复
``` sh
pnpm lint:fix
```
## prettier检测
```sh
pnpm format
```
## Git commit
``` sh
pnpm cz
```

58
package.json Executable file
View File

@ -0,0 +1,58 @@
{
"name": "tsdd-control",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"new": "plop",
"lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx,",
"lint:prettier": "prettier --write src"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@icon-park/vue-next": "^1.4.2",
"@vueuse/core": "^10.1.2",
"axios": "^1.4.0",
"element-plus": "^2.3.7",
"mitt": "^3.0.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.36",
"pinia-plugin-persistedstate": "^3.1.0",
"sortablejs": "^1.15.0",
"vue": "^3.3.4",
"vue-color-kit": "^1.0.5",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"@unocss/preset-uno": "^0.51.12",
"@unocss/vite": "^0.51.12",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.12.0",
"plop": "^3.1.2",
"prettier": "^2.8.8",
"sass": "^1.62.1",
"typescript": "^5.0.4",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.1",
"unplugin-vue-router": "^0.6.4",
"unplugin-vue-setup-extend-plus": "^1.0.0",
"vite": "^4.4.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html-template": "^1.1.5",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-vue-devtools": "^0.4.14",
"vite-plugin-vue-meta-layouts": "^0.2.2",
"vue-global-api": "^0.4.1",
"vue-tsc": "^1.6.4"
}
}

View File

@ -0,0 +1,13 @@
<template>
<div>
<!-- 布局 -->
</div>
</template>
<script lang="ts" setup{{#if isGlobal}} name="{{ properCase name }}"{{/if}}>
// 逻辑代码
</script>
<style lang="scss" scoped>
// 样式
</style>

View File

@ -0,0 +1,63 @@
const fs = require('fs');
function getFolder(path) {
const components = [];
const files = fs.readdirSync(path);
files.forEach(item => {
const stat = fs.lstatSync(`${path}/${item}`);
if (stat.isDirectory() === true && item !== 'components') {
components.push(`${path}/${item}`);
components.push(...getFolder(`${path}/${item}`));
}
});
return components;
}
module.exports = {
description: '创建组件',
prompts: [
{
type: 'confirm',
name: 'isGlobal',
message: '是否为全局组件',
default: false
},
{
type: 'list',
name: 'path',
message: '请选择组件创建目录',
choices: getFolder('src/pages'),
when: answers => {
return !answers.isGlobal;
}
},
{
type: 'input',
name: 'name',
message: '请输入组件名称',
validate: v => {
if (!v || v.trim === '') {
return '组件名称不能为空';
} else {
return true;
}
}
}
],
actions: data => {
let path = '';
if (data.isGlobal) {
path = 'src/components/{{properCase name}}/index.vue';
} else {
path = `${data.path}/components/{{properCase name}}/index.vue`;
}
const actions = [
{
type: 'add',
path,
templateFile: 'plop-templates/component/index.hbs'
}
];
return actions;
}
};

20
plop-templates/page/index.hbs Executable file
View File

@ -0,0 +1,20 @@
<template>
<div>
<!-- 布局 -->
</div>
</template>
{{#if isFilesystem}}
<route lang="yaml">
meta:
title: 页面标题
</route>
{{/if}}
<script lang="ts" setup name="{{ properCase componentName }}">
// 逻辑代码
</script>
<style lang="scss" scoped>
// 样式
</style>

59
plop-templates/page/prompt.js Executable file
View File

@ -0,0 +1,59 @@
const path = require('path');
const fs = require('fs');
function getFolder(path) {
const components = [];
const files = fs.readdirSync(path);
files.forEach(item => {
const stat = fs.lstatSync(`${path}/${item}`);
if (stat.isDirectory() === true && item !== 'components') {
components.push(`${path}/${item}`);
components.push(...getFolder(`${path}/${item}`));
}
});
return components;
}
module.exports = {
description: '创建页面',
prompts: [
{
type: 'list',
name: 'path',
message: '请选择页面创建目录',
choices: getFolder('src/pages')
},
{
type: 'input',
name: 'name',
message: '请输入文件名',
validate: v => {
if (!v || v.trim === '') {
return '文件名不能为空';
} else {
return true;
}
}
},
{
type: 'confirm',
name: 'isFilesystem',
message: '是否为基于文件系统的路由页面',
default: false
}
],
actions: data => {
const relativePath = path.relative('src/pages', data.path);
const actions = [
{
type: 'add',
path: `${data.path}/{{dotCase name}}.vue`,
templateFile: 'plop-templates/page/index.hbs',
data: {
componentName: `${relativePath} ${data.name}`
}
}
];
return actions;
}
};

11
plop-templates/store/index.hbs Executable file
View File

@ -0,0 +1,11 @@
const use{{ properCase name }}Store = defineStore(
// 唯一ID
'{{ camelCase name }}',
{
state: () => ({}),
getters: {},
actions: {},
},
)
export default use{{ properCase name }}Store

27
plop-templates/store/prompt.js Executable file
View File

@ -0,0 +1,27 @@
module.exports = {
description: '创建全局状态',
prompts: [
{
type: 'input',
name: 'name',
message: '请输入模块名称',
validate: v => {
if (!v || v.trim === '') {
return '模块名称不能为空';
} else {
return true;
}
}
}
],
actions: () => {
const actions = [
{
type: 'add',
path: 'src/store/modules/{{camelCase name}}.ts',
templateFile: 'plop-templates/store/index.hbs'
}
];
return actions;
}
};

6
plopfile.js Executable file
View File

@ -0,0 +1,6 @@
module.exports = function (plop) {
plop.setWelcomeMessage('请选择需要创建的模式:');
plop.setGenerator('page', require('./plop-templates/page/prompt'));
plop.setGenerator('component', require('./plop-templates/component/prompt'));
plop.setGenerator('store', require('./plop-templates/store/prompt'));
};

5473
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

15
public/index.html Executable file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

1
public/vite.svg Executable 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

53
src/App.vue Executable file
View File

@ -0,0 +1,53 @@
<template>
<el-config-provider :locale="locale" :size="assemblySize" :button="buttonConfig">
<router-view></router-view>
</el-config-provider>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { getBrowserLang } from '@/utils';
import { useGlobalStore } from '@/stores/modules/global';
import en from 'element-plus/es/locale/lang/en';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { LanguageType } from '@/stores/interface';
import { useTheme } from '@/hooks/useTheme';
const globalStore = useGlobalStore();
// init theme
const { initTheme } = useTheme();
initTheme();
const i18n = useI18n();
onMounted(() => {
const language = globalStore.language ?? getBrowserLang();
i18n.locale.value = language;
globalStore.setGlobalState('language', language as LanguageType);
});
const locale = computed(() => {
if (globalStore.language == 'zh') return zhCn;
if (globalStore.language == 'en') return en;
return getBrowserLang() == 'zh' ? zhCn : en;
});
const assemblySize = computed(() => globalStore.assemblySize);
const buttonConfig = reactive({ autoInsertSpace: false });
</script>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

10
src/api/basic.ts Normal file
View File

@ -0,0 +1,10 @@
import request from '@/utils/axios';
// 登录
export function loginPost(data: any) {
return request({
url: '/manager/login',
method: 'post',
data
});
}

10
src/api/user.ts Normal file
View File

@ -0,0 +1,10 @@
import request from '@/utils/axios';
// 用户列表
export function userListGet(params: any) {
return request({
url: '/manager/user/list',
method: 'get',
params
});
}

BIN
src/assets/heder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

3701
src/assets/images/bg.svg Executable file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 272 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

1
src/assets/vue.svg Executable 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: 496 B

37
src/components/HelloWorld.vue Executable file
View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { ref } from 'vue';
defineProps<{ msg: string }>();
const count = ref(0);
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank">create-vue</a>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
<el-button>我是 ElButton</el-button>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<el-switch v-model="globalStore.isDark" inline-prompt :active-icon="Sunny" :inactive-icon="Moon" @change="switchDark" />
</template>
<script setup lang="ts" name="SwitchDark">
import { useTheme } from '@/hooks/useTheme';
import { useGlobalStore } from '@/stores/modules/global';
import { Sunny, Moon } from '@element-plus/icons-vue';
const { switchDark } = useTheme();
const globalStore = useGlobalStore();
</script>

View File

@ -0,0 +1,462 @@
<template>
<div class="sc-workflow-design">
<div class="box-scale">
<!-- <node-wrap v-if="nodeConfig" v-model="nodeConfig"></node-wrap> -->
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
<!-- <use-select v-if="selectVisible" ref="useselect" @closed="selectVisible = false"></use-select> -->
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss">
.sc-workflow-design {
width: 100%;
}
.sc-workflow-design .box-scale {
display: inline-block;
position: relative;
width: 100%;
padding: 54.5px 0px;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
min-width: min-content;
}
.sc-workflow-design {
.node-wrap {
display: inline-flex;
width: 100%;
flex-flow: column wrap;
justify-content: flex-start;
align-items: center;
padding: 0px 50px;
position: relative;
z-index: 1;
}
.node-wrap-box {
display: inline-flex;
flex-direction: column;
position: relative;
width: 220px;
min-height: 72px;
flex-shrink: 0;
background: rgb(255, 255, 255);
border-radius: 4px;
cursor: pointer;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
}
.node-wrap-box::before {
content: '';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
width: 0px;
border-style: solid;
border-width: 8px 6px 4px;
border-color: rgb(202, 202, 202) transparent transparent;
background: #f6f8f9;
}
.node-wrap-box.start-node:before {
content: none;
}
.node-wrap-box .title {
height: 24px;
line-height: 24px;
color: #fff;
padding-left: 16px;
padding-right: 30px;
border-radius: 4px 4px 0 0;
position: relative;
display: flex;
align-items: center;
}
.node-wrap-box .title .icon {
margin-right: 5px;
}
.node-wrap-box .title .close {
font-size: 15px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 10px;
display: none;
}
.node-wrap-box .content {
position: relative;
padding: 15px;
}
.node-wrap-box .content .placeholder {
color: #999;
}
.node-wrap-box:hover .close {
display: block;
}
.add-node-btn-box {
width: 240px;
display: inline-flex;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.add-node-btn-box:before {
content: '';
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: rgb(202, 202, 202);
}
.add-node-btn {
user-select: none;
width: 240px;
padding: 20px 0px 32px;
display: flex;
justify-content: center;
flex-shrink: 0;
flex-grow: 1;
}
.add-node-btn span {
}
.add-branch {
justify-content: center;
padding: 0px 10px;
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
transform-origin: center center;
z-index: 1;
display: inline-flex;
align-items: center;
}
.branch-wrap {
display: inline-flex;
width: 100%;
}
.branch-box-wrap {
display: flex;
flex-flow: column wrap;
align-items: center;
min-height: 270px;
width: 100%;
flex-shrink: 0;
}
.col-box {
display: inline-flex;
flex-direction: column;
align-items: center;
position: relative;
background: #f6f8f9;
}
.branch-box {
display: flex;
overflow: visible;
min-height: 180px;
height: auto;
border-bottom: 2px solid #ccc;
border-top: 2px solid #ccc;
position: relative;
margin-top: 15px;
}
.branch-box .col-box::before {
content: '';
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 0;
margin: auto;
width: 2px;
height: 100%;
background-color: rgb(202, 202, 202);
}
.condition-node {
display: inline-flex;
flex-direction: column;
min-height: 220px;
}
.condition-node-box {
padding-top: 30px;
padding-right: 50px;
padding-left: 50px;
justify-content: center;
align-items: center;
flex-grow: 1;
position: relative;
display: inline-flex;
flex-direction: column;
}
.condition-node-box::before {
content: '';
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
margin: auto;
width: 2px;
height: 100%;
background-color: rgb(202, 202, 202);
}
.auto-judge {
position: relative;
width: 220px;
min-height: 72px;
background: rgb(255, 255, 255);
border-radius: 4px;
padding: 15px 15px;
cursor: pointer;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
}
.auto-judge::before {
content: '';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
width: 0px;
border-style: solid;
border-width: 8px 6px 4px;
border-color: rgb(202, 202, 202) transparent transparent;
background: rgb(245, 245, 247);
}
.auto-judge .title {
line-height: 16px;
}
.auto-judge .title .node-title {
color: #15bc83;
}
.auto-judge .title .close {
font-size: 15px;
position: absolute;
top: 15px;
right: 15px;
color: #999;
display: none;
}
.auto-judge .title .priority-title {
position: absolute;
top: 15px;
right: 15px;
color: #999;
}
.auto-judge .content {
position: relative;
padding-top: 15px;
}
.auto-judge .content .placeholder {
color: #999;
}
.auto-judge:hover {
.close {
display: block;
}
.priority-title {
display: none;
}
}
.top-left-cover-line,
.top-right-cover-line {
position: absolute;
height: 3px;
width: 50%;
background-color: #f6f8f9;
top: -2px;
}
.bottom-left-cover-line,
.bottom-right-cover-line {
position: absolute;
height: 3px;
width: 50%;
background-color: #f6f8f9;
bottom: -2px;
}
.top-left-cover-line {
left: -1px;
}
.top-right-cover-line {
right: -1px;
}
.bottom-left-cover-line {
left: -1px;
}
.bottom-right-cover-line {
right: -1px;
}
.end-node {
border-radius: 50%;
font-size: 14px;
color: rgba(25, 31, 37, 0.4);
text-align: left;
}
.end-node-circle {
width: 10px;
height: 10px;
margin: auto;
border-radius: 50%;
background: #dbdcdc;
}
.end-node-text {
margin-top: 5px;
text-align: center;
}
.auto-judge:hover {
.sort-left {
display: flex;
}
.sort-right {
display: flex;
}
}
.auto-judge .sort-left {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
left: 0;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
.auto-judge .sort-right {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
right: 0;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
.auto-judge .sort-left:hover,
.auto-judge .sort-right:hover {
background: #eee;
}
.auto-judge:after {
pointer-events: none;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
border-radius: 4px;
transition: all 0.1s;
}
.auto-judge:hover:after {
border: 1px solid #3296fa;
box-shadow: 0 0 6px 0 rgba(50, 150, 250, 0.3);
}
.node-wrap-box:after {
pointer-events: none;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
border-radius: 4px;
transition: all 0.1s;
}
.node-wrap-box:hover:after {
border: 1px solid #3296fa;
box-shadow: 0 0 6px 0 rgba(50, 150, 250, 0.3);
}
}
.tags-list {
margin-top: 15px;
width: 100%;
}
.add-node-popover-body {
}
.add-node-popover-body li {
display: inline-block;
width: 80px;
text-align: center;
padding: 10px 0;
}
.add-node-popover-body li i {
border: 1px solid var(--el-border-color-light);
width: 40px;
height: 40px;
border-radius: 50%;
text-align: center;
line-height: 38px;
font-size: 18px;
cursor: pointer;
}
.add-node-popover-body li i:hover {
border: 1px solid #3296fa;
background: #3296fa;
color: #fff !important;
}
.add-node-popover-body li p {
font-size: 12px;
margin-top: 5px;
}
.node-wrap-drawer__title {
padding-right: 40px;
}
.node-wrap-drawer__title label {
cursor: pointer;
}
.node-wrap-drawer__title label:hover {
border-bottom: 1px dashed #409eff;
}
.node-wrap-drawer__title .node-wrap-drawer__title-edit {
color: #409eff;
margin-left: 10px;
vertical-align: middle;
}
.dark .sc-workflow-design {
.node-wrap-box,
.auto-judge {
background: #2b2b2b;
}
.col-box {
background: var(--el-bg-color);
}
.top-left-cover-line,
.top-right-cover-line,
.bottom-left-cover-line,
.bottom-right-cover-line {
background-color: var(--el-bg-color);
}
.node-wrap-box::before,
.auto-judge::before {
background-color: var(--el-bg-color);
}
.branch-box .add-branch {
background: var(--el-bg-color);
}
.end-node .end-node-text {
color: #d0d0d0;
}
.auto-judge .sort-left:hover,
.auto-judge .sort-right:hover {
background: var(--el-bg-color);
}
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<promoter v-if="nodeConfig.type == 0" v-model="nodeConfig"></promoter>
<approver v-if="nodeConfig.type == 1" v-model="nodeConfig"></approver>
<send v-if="nodeConfig.type == 2" v-model="nodeConfig"></send>
<branch v-if="nodeConfig.type == 4" v-model="nodeConfig">
<template v-slot="slot">
<node-wrap v-if="slot.node" v-model="slot.node.childNode"></node-wrap>
</template>
</branch>
<node-wrap v-if="nodeConfig.childNode" v-model="nodeConfig.childNode"></node-wrap>
</template>
<script>
import approver from './nodes/approver.vue';
import promoter from './nodes/promoter.vue';
import branch from './nodes/branch.vue';
import send from './nodes/send.vue';
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
approver,
promoter,
branch,
send
},
data() {
return {
nodeConfig: {}
};
},
watch: {
modelValue(val) {
this.nodeConfig = val;
},
nodeConfig(val) {
this.$emit('update:modelValue', val);
}
},
mounted() {
this.nodeConfig = this.modelValue;
},
methods: {}
};
</script>
<style></style>

View File

@ -0,0 +1,95 @@
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" :width="270" trigger="click" :hide-after="0" :show-after="0">
<template #reference>
<el-button type="primary" icon="el-icon-plus" circle></el-button>
</template>
<div class="add-node-popover-body">
<ul>
<li>
<el-icon style="color: #ff943e" @click="addType(1)"><el-icon-user-filled /></el-icon>
<p>审批节点</p>
</li>
<li>
<el-icon style="color: #3296fa" @click="addType(2)"><el-icon-promotion /></el-icon>
<p>抄送节点</p>
</li>
<li>
<el-icon style="color: #15bc83" @click="addType(4)"><el-icon-share /></el-icon>
<p>条件分支</p>
</li>
</ul>
</div>
</el-popover>
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
data() {
return {};
},
mounted() {},
methods: {
addType(type) {
var node = {};
if (type == 1) {
node = {
nodeName: '审核人',
type: 1, //
setType: 1, //
nodeUserList: [], //
nodeRoleList: [], //
examineLevel: 1, //
directorLevel: 1, //
selectMode: 1, //
termAuto: false, //
term: 0, //
termMode: 1, //
examineMode: 1, //
directorMode: 0, //
childNode: this.modelValue
};
} else if (type == 2) {
node = {
nodeName: '抄送人',
type: 2,
userSelectFlag: true,
nodeUserList: [],
childNode: this.modelValue
};
} else if (type == 4) {
node = {
nodeName: '条件路由',
type: 4,
conditionNodes: [
{
nodeName: '条件1',
type: 3,
priorityLevel: 1,
conditionMode: 1,
conditionList: []
},
{
nodeName: '条件2',
type: 3,
priorityLevel: 2,
conditionMode: 1,
conditionList: []
}
],
childNode: this.modelValue
};
}
this.$emit('update:modelValue', node);
}
}
};
</script>
<style></style>

View File

@ -0,0 +1,204 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #ff943e">
<el-icon class="icon"><el-icon-user-filled /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
<el-icon class="close" @click.stop="delNode()"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig)">{{ toText(nodeConfig) }}</span>
<span v-else class="placeholder">请选择</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer v-model="drawer" title="审批人设置" destroy-on-close append-to-body :size="500">
<template #header>
<div class="node-wrap-drawer__title">
<label v-if="!isEditTitle" @click="editTitle">
{{ form.nodeName }}
<el-icon class="node-wrap-drawer__title-edit"> <el-icon-edit /></el-icon>
</label>
<el-input
v-if="isEditTitle"
ref="nodeTitle"
v-model="form.nodeName"
clearable
@blur="saveTitle"
@keyup.enter="saveTitle"
></el-input>
</div>
</template>
<el-container>
<el-main style="padding: 0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="审批人员类型">
<el-select v-model="form.setType">
<el-option :value="1" label="指定成员"></el-option>
<el-option :value="2" label="主管"></el-option>
<el-option :value="3" label="角色"></el-option>
<el-option :value="4" label="发起人自选"></el-option>
<el-option :value="5" label="发起人自己"></el-option>
<el-option :value="7" label="连续多级主管"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.setType == 1" label="选择成员">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(1, form.nodeUserList)">选择人员</el-button>
<div class="tags-list">
<el-tag v-for="(user, index) in form.nodeUserList" :key="user.id" closable @close="delUser(index)">{{
user.name
}}</el-tag>
</div>
</el-form-item>
<el-form-item v-if="form.setType == 2" label="指定主管">
发起人的第 <el-input-number v-model="form.examineLevel" :min="1" /> 级主管
</el-form-item>
<el-form-item v-if="form.setType == 3" label="选择角色">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(2, form.nodeRoleList)">选择角色</el-button>
<div class="tags-list">
<el-tag v-for="(role, index) in form.nodeRoleList" :key="role.id" type="info" closable @close="delRole(index)">{{
role.name
}}</el-tag>
</div>
</el-form-item>
<el-form-item v-if="form.setType == 4" label="发起人自选">
<el-radio-group v-model="form.selectMode">
<el-radio :label="1">自选一个人</el-radio>
<el-radio :label="2">自选多个人</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.setType == 7" label="连续主管审批终点">
<el-radio-group v-model="form.directorMode">
<el-radio :label="0">直到最上层主管</el-radio>
<el-radio :label="1">自定义审批终点</el-radio>
</el-radio-group>
<p v-if="form.directorMode == 1">直到发起人的第 <el-input-number v-model="form.directorLevel" :min="1" /> 级主管</p>
</el-form-item>
<el-divider></el-divider>
<el-form-item label="">
<el-checkbox v-model="form.termAuto" label="超时自动审批"></el-checkbox>
</el-form-item>
<template v-if="form.termAuto">
<el-form-item label="审批期限(为 0 则不生效)">
<el-input-number v-model="form.term" :min="0" /> 小时
</el-form-item>
<el-form-item label="审批期限超时后执行">
<el-radio-group v-model="form.termMode">
<el-radio :label="0">自动通过</el-radio>
<el-radio :label="1">自动拒绝</el-radio>
</el-radio-group>
</el-form-item>
</template>
<el-divider></el-divider>
<el-form-item label="多人审批时审批方式">
<el-radio-group v-model="form.examineMode">
<p style="width: 100%"><el-radio :label="1">按顺序依次审批</el-radio></p>
<p style="width: 100%"><el-radio :label="2">会签 (可同时审批每个人必须审批通过)</el-radio></p>
<p style="width: 100%"><el-radio :label="3">或签 (有一人审批通过即可)</el-radio></p>
</el-radio-group>
</el-form-item>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer = false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode.vue';
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
};
},
watch: {
modelValue() {
this