Repository: tsai996/lowflow-design Branch: main Commit: 8c4b11c9a68a Files: 80 Total size: 150.7 KB Directory structure: gitextract_5dzaix2j/ ├── .eslintrc-auto-import.json ├── .eslintrc.cjs ├── .github/ │ └── workflows/ │ └── gh-pages.yml ├── .gitignore ├── .prettierrc.json ├── .vscode/ │ └── extensions.json ├── LICENSE ├── README.en.md ├── README.md ├── env.d.ts ├── index.html ├── package.json ├── public/ │ └── CNAME ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── index.ts │ │ └── modules/ │ │ ├── model.ts │ │ ├── role.ts │ │ └── user.ts │ ├── components/ │ │ ├── AdvancedFilter/ │ │ │ ├── Operator.vue │ │ │ ├── Trigger.vue │ │ │ ├── index.vue │ │ │ └── type.ts │ │ ├── Render/ │ │ │ ├── index.tsx │ │ │ └── type.ts │ │ ├── RoleSelector/ │ │ │ ├── RolePicker.vue │ │ │ ├── RoleTag.vue │ │ │ └── index.vue │ │ ├── SvgIcon/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── UserSelector/ │ │ ├── UserPicker.vue │ │ ├── UserTag.vue │ │ └── index.vue │ ├── hooks/ │ │ └── useDraggableScroll.ts │ ├── main.ts │ ├── mock/ │ │ ├── index.ts │ │ ├── role.ts │ │ └── user.ts │ ├── mockProdServer.ts │ ├── router/ │ │ └── index.ts │ ├── stores/ │ │ └── counter.ts │ ├── styles/ │ │ ├── el-segmented.scss │ │ ├── element/ │ │ │ ├── dark.scss │ │ │ └── index.scss │ │ └── index.scss │ ├── typings/ │ │ ├── auto-imports.d.ts │ │ ├── components.d.ts │ │ └── index.d.ts │ └── views/ │ ├── flowDesign/ │ │ ├── index.vue │ │ ├── nodes/ │ │ │ ├── Add.vue │ │ │ ├── ApprovalNode.vue │ │ │ ├── CcNode.vue │ │ │ ├── ConditionNode.vue │ │ │ ├── EndNode.vue │ │ │ ├── ExclusiveNode.vue │ │ │ ├── GatewayNode.vue │ │ │ ├── Node.vue │ │ │ ├── NotifyNode.vue │ │ │ ├── ServiceNode.vue │ │ │ ├── StartNode.vue │ │ │ ├── TimerNode.vue │ │ │ ├── TreeNode.vue │ │ │ └── type.ts │ │ └── panels/ │ │ ├── ApprovalPanel.vue │ │ ├── AssigneePanel.vue │ │ ├── CcPanel.vue │ │ ├── ConditionPanel.vue │ │ ├── EndPanel.vue │ │ ├── ExecutionListeners.vue │ │ ├── NotifyPanel.vue │ │ ├── ServicePanel.vue │ │ ├── StartPanel.vue │ │ ├── TaskListeners.vue │ │ ├── TimerPanel.vue │ │ └── index.vue │ └── home/ │ └── index.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── unocss.config.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc-auto-import.json ================================================ { "globals": { "Component": true, "ComponentPublicInstance": true, "ComputedRef": true, "EffectScope": true, "ExtractDefaultPropTypes": true, "ExtractPropTypes": true, "ExtractPublicPropTypes": true, "InjectionKey": true, "PropType": true, "Ref": true, "VNode": true, "WritableComputedRef": true, "computed": true, "createApp": true, "customRef": true, "defineAsyncComponent": true, "defineComponent": true, "effectScope": true, "getCurrentInstance": true, "getCurrentScope": true, "h": true, "inject": true, "isProxy": true, "isReactive": true, "isReadonly": true, "isRef": true, "markRaw": true, "nextTick": true, "onActivated": true, "onBeforeMount": true, "onBeforeRouteLeave": true, "onBeforeRouteUpdate": true, "onBeforeUnmount": true, "onBeforeUpdate": true, "onDeactivated": true, "onErrorCaptured": true, "onMounted": true, "onRenderTracked": true, "onRenderTriggered": true, "onScopeDispose": true, "onServerPrefetch": true, "onUnmounted": true, "onUpdated": true, "provide": true, "reactive": true, "readonly": true, "ref": true, "resolveComponent": true, "shallowReactive": true, "shallowReadonly": true, "shallowRef": true, "toRaw": true, "toRef": true, "toRefs": true, "toValue": true, "triggerRef": true, "unref": true, "useAttrs": true, "useCssModule": true, "useCssVars": true, "useLink": true, "useRoute": true, "useRouter": true, "useSlots": true, "watch": true, "watchEffect": true, "watchPostEffect": true, "watchSyncEffect": true, "ElCheckbox": true, "ElInput": true, "ElInputNumber": true, "ElRadio": true, "ElSelect": true, "ElDivider": true } } ================================================ FILE: .eslintrc.cjs ================================================ /* eslint-env node */ require('@rushstack/eslint-patch/modern-module-resolution') module.exports = { root: true, 'extends': [ 'plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting' ], parserOptions: { ecmaVersion: 'latest' }, rules: { 'vue/no-mutating-props': ['error', { 'shallowOnly': true }], 'vue/multi-word-component-names': 'off' } } ================================================ FILE: .github/workflows/gh-pages.yml ================================================ name: GitHub Pages # 触发脚本的条件,develop分支push代码的时候 on: push: branches: - main # 要执行的任务 jobs: # 任务名称 build_and_deploy: # runs-on 指定job任务运行所需要的虚拟机环境(必填) runs-on: ubuntu-latest # 任务步骤 steps: - name: 迁出代码 # 使用action库 actions/checkout获取源码 uses: actions/checkout@v3 # 使用的工具 # 使用 pnpm - name: 使用 pnpm uses: pnpm/action-setup@v2 with: version: 8.5.0 # 安装node - name: 安装node.js # 使用action库 actions/setup-node 安装node uses: actions/setup-node@v3 with: node-version: 16.20.0 cache: 'pnpm' # 安装 - name: 安装依赖 run: pnpm install # 打包 - name: 打包 run: pnpm build:test # 部署 - name: 部署到gh-pages分支 uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist # 指定推送分支,默认为:gh-pages publish_branch: gh-pages ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/extensions.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? *.tsbuildinfo ================================================ FILE: .prettierrc.json ================================================ { "$schema": "https://json.schemastore.org/prettierrc", "semi": false, "tabWidth": 2, "singleQuote": true, "printWidth": 100, "trailingComma": "none" } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "Vue.volar", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode" ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Victor Tsai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.en.md ================================================

lowflow-design

Low-code Workflow Designer

中文 | English

## Overview `lowflow-design` is a workflow designer built with `Vue 3`, `Vite`, `TypeScript`, and `Element Plus`. It is designed for low-code/no-code platforms that need visual workflow configuration. You can create workflows visually and convert workflow JSON to BPMN XML with the backend converter project: - Backend converter: [GitHub](https://github.com/tsai996/lowflow-design-converter) | [Gitee](https://gitee.com/cai_xiao_feng/lowflow-design-converter) - Traditional `bpmn-js` workflow designer: [GitHub](https://github.com/tsai996/vue-bpmn-designer) | [Gitee](https://gitee.com/cai_xiao_feng/vue-bpmn-designer) ## Live Demo - Preview: - Full demo: ## Screenshots

workflow designer property panel

## Features - Approval node: supports single user, multiple users, roles, departments, initiator, manager, and custom approvers. - CC node: supports single user, multiple users, roles, departments, initiator, manager, and custom CC recipients. - Conditional branches: supports condition groups and combination logic. - Timer wait: supports second, minute, hour, day, week, month, and custom durations. - Notification node: supports in-app, email, WeCom, DingTalk, Feishu, SMS, and more. ## Tech Stack - Vue 3 - TypeScript - Vite - Element Plus - Pinia - Vue Router - UnoCSS ## Quick Start ### 1. Install dependencies ```bash npm install ``` ### 2. Start development server ```bash npm run dev ``` ### 3. Build ```bash npm run build ``` ### 4. Preview production build ```bash npm run preview ``` ## Scripts ```bash npm run dev # run dev server npm run build # production build (with type check) npm run build:dev # build in development mode npm run build:test # build in test mode npm run preview # preview production build npm run type-check # run type checking npm run lint # run ESLint (with --fix) npm run format # format src with Prettier ``` ## Project Structure ```text . |-- public/ |-- src/ | |-- api/ | |-- assets/ | |-- components/ | |-- hooks/ | |-- mock/ | |-- router/ | |-- stores/ | |-- styles/ | |-- typings/ | |-- views/ | | |-- flowDesign/ | | | |-- nodes/ | | | |-- panels/ | | | `-- index.vue | |-- App.vue | `-- main.ts |-- package.json |-- vite.config.ts `-- README.md ``` ## Repositories | Platform | Frontend | Backend Converter | | --- | --- | --- | | GitHub | | | | Gitee | | | ## Community and Support ### Community Groups

WeChat QQ Group

### Sponsor If this project helps you, sponsorship is welcome.

WeChat Pay Alipay

## Recommended Reading *In-depth Flowable Workflow Engine: Core Principles and Advanced Practice*: ![flowable](public/flowable.jpg) ================================================ FILE: README.md ================================================

lowflow-design

低代码流程设计器

中文 | English

## 项目简介 `lowflow-design` 是一个基于 `Vue 3`、`Vite`、`TypeScript`、`Element Plus` 的流程设计器,适用于低代码/无代码平台中的流程配置场景。 项目支持通过可视化方式快速搭建流程,并可配合后端转换项目将流程 JSON 转换为 BPMN XML: - 后端转换器:[GitHub](https://github.com/tsai996/lowflow-design-converter) | [Gitee](https://gitee.com/cai_xiao_feng/lowflow-design-converter) - 传统 `bpmn-js` 流程设计器:[GitHub](https://github.com/tsai996/vue-bpmn-designer) | [Gitee](https://gitee.com/cai_xiao_feng/vue-bpmn-designer) ## 在线体验 - 预览地址: - 成品示例: ## 效果预览

流程设计器 属性面板

## 功能特性 - 审批节点:支持单人、多人、角色、部门、发起人、上级领导、自定义审批人等。 - 抄送节点:支持单人、多人、角色、部门、发起人、上级领导、自定义抄送人等。 - 条件分支:支持条件组及组合逻辑。 - 计时等待:支持秒、分、时、天、周、月及自定义时长。 - 消息通知:支持站内信、邮件、企业微信、钉钉、飞书、短信等通知方式。 ## 技术栈 - Vue 3 - TypeScript - Vite - Element Plus - Pinia - Vue Router - UnoCSS ## 快速开始 ### 1. 安装依赖 ```bash npm install ``` ### 2. 启动开发环境 ```bash npm run dev ``` ### 3. 构建 ```bash npm run build ``` ### 4. 预览构建产物 ```bash npm run preview ``` ## 常用脚本 ```bash npm run dev # 本地开发 npm run build # 生产构建(含类型检查) npm run build:dev # development 模式构建 npm run build:test # test 模式构建 npm run preview # 预览构建产物 npm run type-check # 类型检查 npm run lint # ESLint(自动修复) npm run format # Prettier 格式化(src) ``` ## 项目结构 ```text . |-- public/ |-- src/ | |-- api/ | |-- assets/ | |-- components/ | |-- hooks/ | |-- mock/ | |-- router/ | |-- stores/ | |-- styles/ | |-- typings/ | |-- views/ | | |-- flowDesign/ | | | |-- nodes/ | | | |-- panels/ | | | `-- index.vue | |-- App.vue | `-- main.ts |-- package.json |-- vite.config.ts `-- README.md ``` ## 源码仓库 | 平台 | 前端 | 后端转换器 | | --- | --- | --- | | GitHub | | | | Gitee | | | ## 交流与支持 ### 交流群

微信 QQ群

### 赞助 如果这个项目对你有帮助,欢迎赞助支持。

微信赞助 支付宝赞助

## 推荐阅读 推荐搭配阅读《深入 Flowable 流程引擎:核心原理与高阶实战》: ![flowable](public/flowable.jpg) ================================================ FILE: env.d.ts ================================================ /// interface ImportMetaEnv { readonly VITE_API_URL: string; readonly VITE_PUBLIC_PATH: string; } interface ImportMeta { readonly env: ImportMetaEnv } ================================================ FILE: index.html ================================================ Vite App
================================================ FILE: package.json ================================================ { "name": "lowflow-design", "version": "0.0.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "run-p type-check \"build-only {@}\" --", "build:dev": "vite build --mode development", "build:test": "vite build --mode test", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --build --force", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "format": "prettier --write src/" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", "@vueuse/core": "^10.9.0", "axios": "^1.6.8", "element-plus": "^2.7.2", "file-saver": "^2.0.5", "lodash-es": "^4.17.21", "pinia": "^2.1.7", "vue": "^3.4.21", "vue-router": "^4.3.0" }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", "@tsconfig/node20": "^20.1.4", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", "@types/node": "^20.12.5", "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", "eslint": "^8.57.0", "eslint-plugin-vue": "^9.23.0", "mockjs": "^1.1.0", "npm-run-all2": "^6.1.2", "prettier": "^3.2.5", "sass": "^1.77.0", "typescript": "~5.4.0", "unocss": "^0.59.4", "unplugin-auto-import": "^0.17.5", "unplugin-vue-components": "^0.27.0", "vite": "^5.2.8", "vite-plugin-mock": "^2.9.6", "vite-plugin-svg-icons": "^2.0.1", "vite-plugin-vue-setup-extend": "^0.4.0", "vue-tsc": "^2.0.11" } } ================================================ FILE: public/CNAME ================================================ vite-starter.element-plus.org ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/api/index.ts ================================================ import axios, { type AxiosInstance, AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig, type AxiosResponse } from 'axios' import { ElNotification } from 'element-plus' export interface Result { code: number success: boolean message: string } export interface ResultData extends Result { data: T } /** * axios配置 */ const config = { baseURL: import.meta.env.VITE_API_URL, timeout: 8000 } class RequestHttp { service: AxiosInstance /** * 请求构造函数 * @param config */ public constructor(config: AxiosRequestConfig) { this.service = axios.create(config) this.service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { return config }, (error: AxiosError) => { return Promise.reject(error) } ) this.service.interceptors.response.use( (response: AxiosResponse) => { const { data } = response return data }, (error: AxiosError) => { const { response, message } = error const data = response?.data as ResultData const errMsg = data ? data.message : message ElNotification.error(errMsg || '未知错误') return Promise.reject(response?.data || error) } ) } /** * get请求 * @param url * @param params * @param config */ get(url: string, params?: object, config = {}): Promise> { return this.service.get(url, { params, ...config }) } /** * post请求 * @param url * @param data * @param config */ post(url: string, data?: object, config = {}): Promise> { return this.service.post(url, data, config) } /** * request请求 * @param config */ request(config: AxiosRequestConfig): Promise> { return this.service.request(config) } /** * 下载文件 * @param url * @param data * @param config */ download(url: string, data?: object, config = {}): Promise { return this.service.post(url, data, { ...config, responseType: 'blob' }) } } export default new RequestHttp(config) ================================================ FILE: src/api/modules/model.ts ================================================ import http from '@/api' import FileSaver from 'file-saver' export const downloadXml = async (data: object) => { const res = await http.download('https://demo.lowflow.vip/api/model/download', data) FileSaver.saveAs( new Blob([res], { type: 'application/octet-stream;charset=utf-8' }), '测试流程.bpmn20.xml' ) } ================================================ FILE: src/api/modules/role.ts ================================================ import http from '@/api/index' export interface Role { id: string name: string } /** * 获取角色信息 * @param id */ export const getById = (id: string) => { return http.get(`/role/info`, { id: id }) } /** * 查询角色列表 */ export const getList = (roleIds?: string[]) => { const params = roleIds ? { roleIds: roleIds } : {} return http.post('/role/list', params) } ================================================ FILE: src/api/modules/user.ts ================================================ import http from '@/api/index' export interface User { id: string username: string name: string avatar: string } /** * 获取用户信息 * @param username */ export const getByUsername = (username: string) => { return http.get(`/user/info`, { username: username }) } /** * 查询用户列表 */ export const getList = (userIds?: string[]) => { const params = userIds ? { userIds: userIds } : {} return http.post('/user/list', params) } ================================================ FILE: src/components/AdvancedFilter/Operator.vue ================================================ ================================================ FILE: src/components/AdvancedFilter/Trigger.vue ================================================ ================================================ FILE: src/components/AdvancedFilter/index.vue ================================================ ================================================ FILE: src/components/AdvancedFilter/type.ts ================================================ /** * 字段筛选结果 */ export interface Condition { // 筛选字段 field: string | null // 条件运算符 operator: string // 筛选值 value: any | null } /** * 筛选规则 */ export interface FilterRules { operator: 'or' | 'and' conditions: Condition[] groups: FilterRules[] } ================================================ FILE: src/components/Render/index.tsx ================================================ import { cloneDeep } from 'lodash-es' import type { Field } from './type' import type { PropType } from 'vue' export default defineComponent({ props: { modelValue: { type: [String, Number, Boolean, Array, Object] as PropType, default: undefined, required: false }, field: { type: Object as PropType, required: true } }, emits: ['update:modelValue'], components: { ElInput: defineAsyncComponent(() => import('element-plus/es').then(({ ElInput }) => ElInput)), ElInputNumber: defineAsyncComponent(() => import('element-plus/es').then(({ ElInputNumber }) => ElInputNumber) ), ElSelect: defineAsyncComponent(() => import('element-plus/es').then(({ ElSelect }) => ElSelect) ), ElRadio: defineAsyncComponent(() => import('element-plus/es').then(({ ElRadio }) => ElRadio)), ElCheckbox: defineAsyncComponent(() => import('element-plus/es').then(({ ElCheckbox }) => ElCheckbox) ), UserSelector: defineAsyncComponent(() => import('@/components/UserSelector/index.vue')), RoleSelector: defineAsyncComponent(() => import('@/components/RoleSelector/index.vue')) }, setup(props, { emit }) { /** * 构建属性参数 * @param fieldClone */ const buildProps = (fieldClone: Field) => { const dataObject: Record = {} const _props = fieldClone.props || {} Object.keys(_props).forEach((key) => { dataObject[key] = _props[key] }) if (props.modelValue !== undefined) { dataObject.modelValue = props.modelValue } else { dataObject.modelValue = fieldClone.value } dataObject['onUpdate:modelValue'] = (value: any) => { emit('update:modelValue', value) } delete dataObject.options return dataObject } /** * 构建插槽 * @param fieldClone */ const buildSlots = (fieldClone: Field) => { const children: Record = {} const slotFunctions: Record = { ElSelect: (conf: Field) => { return conf.props.options.map((item: any) => { return }) }, ElRadio: (conf: Field) => { return conf.props.options.map((item: any) => { return {item.label} }) }, ElCheckbox: (conf: Field) => { return conf.props.options.map((item: any) => { return {item.label} }) } } const slotFunction = slotFunctions[fieldClone.name] if (slotFunction) { children.default = () => { return slotFunction(fieldClone) } } return children } return { buildProps, buildSlots } }, render() { const fieldClone: Field = cloneDeep(this.field) const slots = this.buildSlots(fieldClone) const props = this.buildProps(fieldClone) const eleComponent = resolveComponent(fieldClone.name) if (typeof eleComponent === 'string') { return h(eleComponent, props, slots) } return h(eleComponent, props, slots) } }) ================================================ FILE: src/components/Render/type.ts ================================================ export interface Field { id: string type: 'formItem' | 'container' label: string name: string value: any readonly?: boolean required?: boolean hidden: boolean props: Recordable children?: Field[] } ================================================ FILE: src/components/RoleSelector/RolePicker.vue ================================================ ================================================ FILE: src/components/RoleSelector/RoleTag.vue ================================================ ================================================ FILE: src/components/RoleSelector/index.vue ================================================ ================================================ FILE: src/components/SvgIcon/index.scss ================================================ .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } ================================================ FILE: src/components/SvgIcon/index.tsx ================================================ import './index.scss' import type { CSSProperties, PropType } from 'vue' export default defineComponent({ name: 'SvgIcon', props: { name: { type: String as PropType, required: true }, prefix: { type: String as PropType, default: 'icon' }, color: { type: String as PropType }, size: { type: Number as PropType }, className: { type: String as PropType } }, setup(props) { const symbolId = computed(() => `#${props.prefix}-${props.name}`) const svgClass = computed(() => [ 'svg-icon', props.name && props.name.replace('el:', ''), props.className ]) const fill = computed(() => (props.color ? props.color : 'currentColor')) const style = computed(() => { const { size } = props if (!size) return {} return { fontSize: `${size}px` } }) return { symbolId, svgClass, fill, style } }, render() { const { $attrs, symbolId, svgClass, fill } = this if (this.name) { if (this.name.startsWith('el:')) { return ( {h(resolveComponent(this.name.slice(3)))} ) } else { return ( ) } } return null } }) ================================================ FILE: src/components/UserSelector/UserPicker.vue ================================================ ================================================ FILE: src/components/UserSelector/UserTag.vue ================================================ ================================================ FILE: src/components/UserSelector/index.vue ================================================ ================================================ FILE: src/hooks/useDraggableScroll.ts ================================================ import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue' export function useDraggableScroll(containerRef: Ref) { const isDragging = ref(false) let startX: number, startY: number let scrollLeft: number, scrollTop: number const onMouseDown = (e: MouseEvent) => { if (!containerRef.value) return isDragging.value = true startX = e.pageX startY = e.pageY scrollLeft = containerRef.value.scrollLeft scrollTop = containerRef.value.scrollTop document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) } const onMouseMove = (e: MouseEvent) => { if (!isDragging.value || !containerRef.value) return const deltaX = e.pageX - startX const deltaY = e.pageY - startY containerRef.value.scrollLeft = scrollLeft - deltaX containerRef.value.scrollTop = scrollTop - deltaY } const onMouseUp = () => { isDragging.value = false document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) } onMounted(() => { containerRef.value?.addEventListener('mousedown', onMouseDown) }) onBeforeUnmount(() => { containerRef.value?.removeEventListener('mousedown', onMouseDown) }) return { isDragging } } ================================================ FILE: src/main.ts ================================================ import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' import 'virtual:svg-icons-register' import 'uno.css' import '@/styles/index.scss' // If you want to use ElMessage, import it. import 'element-plus/theme-chalk/src/message.scss' import 'element-plus/theme-chalk/src/notification.scss' import 'element-plus/theme-chalk/el-input-number.css' const app = createApp(App) import * as Icons from '@element-plus/icons-vue' for (const [key, component] of Object.entries(Icons)) { app.component(key, component) } app.use(router).use(createPinia()) app.mount('#app') ================================================ FILE: src/mock/index.ts ================================================ import user from './user' import role from './role' import type { MockMethod } from 'vite-plugin-mock' const mockModules: MockMethod[] = [...user, ...role] export default mockModules ================================================ FILE: src/mock/role.ts ================================================ import type { MockMethod } from 'vite-plugin-mock' import type { ResultData } from '@/api' const roleList = [ { id: '1', name: '项目经理' }, { id: '2', name: '产品经理' }, { id: '3', name: '高级开发工程师' }, { id: '4', name: '中级开发工程师' }, { id: '5', name: '项目总监' }, { id: '6', name: '产品策划' }, { id: '7', name: '客服' }, { id: '8', name: '销售经理' } ] const role = [ { url: '/api/role/info', method: 'get', response: (req: any) => { const id = req.query.id return { code: 200, success: true, message: '操作成功', data: roleList.find((item) => item.id === id) } as ResultData } }, { url: '/api/role/list', method: 'post', response: (req: any) => { const roleIds = req.body.roleIds return { code: 200, success: true, message: '操作成功', data: Array.isArray(roleIds) ? roleList.filter((item) => roleIds.includes(item.id)) : roleList } as ResultData } } ] as MockMethod[] export default role ================================================ FILE: src/mock/user.ts ================================================ import type { MockMethod } from 'vite-plugin-mock' import type { ResultData } from '@/api' const userList = [ { id: 1, name: '张三', username: 'admin', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 2, name: '李四', username: 'lisi', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 3, name: '王五', username: 'wangwu', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 4, name: '赵六', username: 'zhaoliu', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 5, name: '孙七', username: 'sunqi', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 6, name: '周八', username: 'zhouba', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 7, name: '吴九', username: 'wujui', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' }, { id: 8, name: '郑十', username: 'zhengshi', avatar: 'https://avatars.githubusercontent.com/u/44080404?v=4' } ] const user = [ { url: '/api/user/info', method: 'get', response: (req: any) => { const username = req.query.username return { code: 200, success: true, message: '操作成功', data: userList.find((item) => item.username === username) } as ResultData } }, { url: '/api/user/list', method: 'post', response: (req: any) => { const userIds = req.body.userIds return { code: 200, success: true, message: '操作成功', data: Array.isArray(userIds) ? userList.filter((item) => userIds.includes(item.username)) : userList } as ResultData } } ] as MockMethod[] export default user ================================================ FILE: src/mockProdServer.ts ================================================ import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' import mock from './mock' export function setupProdMockServer() { createProdMockServer(mock) } ================================================ FILE: src/router/index.ts ================================================ import { createRouter, createWebHistory } from 'vue-router' import Home from '@/views/home/index.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'Home', component: Home } ] }) export default router ================================================ FILE: src/stores/counter.ts ================================================ import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } return { count, doubleCount, increment } }) ================================================ FILE: src/styles/el-segmented.scss ================================================ .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, 0.08); --el-segmented-hover-bg: rgba(0, 0, 0, 0.04); --el-segmented-disabled-color: var(--el-text-color-placeholder); :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 0.2s, background-color 0.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; } } } ================================================ FILE: src/styles/element/dark.scss ================================================ // only scss variables $--colors: ( 'primary': ( 'base': #589ef8 ) ); @forward 'element-plus/theme-chalk/src/dark/var.scss' with ( $colors: $--colors ); ================================================ FILE: src/styles/element/index.scss ================================================ $--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'; ================================================ FILE: src/styles/index.scss ================================================ // 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, 0.08); --el-segmented-hover-bg: rgba(0, 0, 0, 0.04); --el-segmented-disabled-color: var(--el-text-color-placeholder); } } @media (max-width: 1200px) { .el-drawer.rtl { width: 90% !important; } .el-dialog { width: 90% !important; } .el-dialog.is-fullscreen { width: 100% !important; } } // 自定义抽屉样式 .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); } html, body, #app { width: 100%; height: 100%; padding: 0; margin: 0; } code { border-radius: 2px; padding: 2px 4px; background-color: var(--el-color-primary-light-9); color: var(--el-color-primary); } ================================================ FILE: src/typings/auto-imports.d.ts ================================================ /* eslint-disable */ /* prettier-ignore */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import export {} declare global { const EffectScope: (typeof import('vue'))['EffectScope'] const ElCheckbox: (typeof import('element-plus/es'))['ElCheckbox'] const ElDivider: (typeof import('element-plus/es'))['ElDivider'] const ElInput: (typeof import('element-plus/es'))['ElInput'] const ElInputNumber: (typeof import('element-plus/es'))['ElInputNumber'] const ElRadio: (typeof import('element-plus/es'))['ElRadio'] const ElSelect: (typeof import('element-plus/es'))['ElSelect'] const computed: (typeof import('vue'))['computed'] const createApp: (typeof import('vue'))['createApp'] const customRef: (typeof import('vue'))['customRef'] const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'] const defineComponent: (typeof import('vue'))['defineComponent'] const effectScope: (typeof import('vue'))['effectScope'] const getCurrentInstance: (typeof import('vue'))['getCurrentInstance'] const getCurrentScope: (typeof import('vue'))['getCurrentScope'] const h: (typeof import('vue'))['h'] const inject: (typeof import('vue'))['inject'] const isProxy: (typeof import('vue'))['isProxy'] const isReactive: (typeof import('vue'))['isReactive'] const isReadonly: (typeof import('vue'))['isReadonly'] const isRef: (typeof import('vue'))['isRef'] const markRaw: (typeof import('vue'))['markRaw'] const nextTick: (typeof import('vue'))['nextTick'] const onActivated: (typeof import('vue'))['onActivated'] const onBeforeMount: (typeof import('vue'))['onBeforeMount'] const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave'] const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate'] const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'] const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'] const onDeactivated: (typeof import('vue'))['onDeactivated'] const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'] const onMounted: (typeof import('vue'))['onMounted'] const onRenderTracked: (typeof import('vue'))['onRenderTracked'] const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'] const onScopeDispose: (typeof import('vue'))['onScopeDispose'] const onServerPrefetch: (typeof import('vue'))['onServerPrefetch'] const onUnmounted: (typeof import('vue'))['onUnmounted'] const onUpdated: (typeof import('vue'))['onUpdated'] const provide: (typeof import('vue'))['provide'] const reactive: (typeof import('vue'))['reactive'] const readonly: (typeof import('vue'))['readonly'] const ref: (typeof import('vue'))['ref'] const resolveComponent: (typeof import('vue'))['resolveComponent'] const shallowReactive: (typeof import('vue'))['shallowReactive'] const shallowReadonly: (typeof import('vue'))['shallowReadonly'] const shallowRef: (typeof import('vue'))['shallowRef'] const toRaw: (typeof import('vue'))['toRaw'] const toRef: (typeof import('vue'))['toRef'] const toRefs: (typeof import('vue'))['toRefs'] const toValue: (typeof import('vue'))['toValue'] const triggerRef: (typeof import('vue'))['triggerRef'] const unref: (typeof import('vue'))['unref'] const useAttrs: (typeof import('vue'))['useAttrs'] const useCssModule: (typeof import('vue'))['useCssModule'] const useCssVars: (typeof import('vue'))['useCssVars'] const useLink: (typeof import('vue-router'))['useLink'] const useRoute: (typeof import('vue-router'))['useRoute'] const useRouter: (typeof import('vue-router'))['useRouter'] const useSlots: (typeof import('vue'))['useSlots'] const watch: (typeof import('vue'))['watch'] const watchEffect: (typeof import('vue'))['watchEffect'] const watchPostEffect: (typeof import('vue'))['watchPostEffect'] const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'] } // for type re-export declare global { // @ts-ignore export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' import('vue') } ================================================ FILE: src/typings/components.d.ts ================================================ /* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { AdvancedFilter: typeof import('./../components/AdvancedFilter/index.vue')['default'] ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElBadge: typeof import('element-plus/es')['ElBadge'] ElButton: typeof import('element-plus/es')['ElButton'] ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCol: typeof import('element-plus/es')['ElCol'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 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'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 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'] ElRow: typeof import('element-plus/es')['ElRow'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSpace: typeof import('element-plus/es')['ElSpace'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] ElText: typeof import('element-plus/es')['ElText'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] Operator: typeof import('./../components/AdvancedFilter/Operator.vue')['default'] Render: typeof import('./../components/Render/index.tsx')['default'] RolePicker: typeof import('./../components/RoleSelector/RolePicker.vue')['default'] RoleSelector: typeof import('./../components/RoleSelector/index.vue')['default'] RoleTag: typeof import('./../components/RoleSelector/RoleTag.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SvgIcon: typeof import('./../components/SvgIcon/index.tsx')['default'] Trigger: typeof import('./../components/AdvancedFilter/Trigger.vue')['default'] UserPicker: typeof import('./../components/UserSelector/UserPicker.vue')['default'] UserSelector: typeof import('./../components/UserSelector/index.vue')['default'] UserTag: typeof import('./../components/UserSelector/UserTag.vue')['default'] } } ================================================ FILE: src/typings/index.d.ts ================================================ type Recordable = Record ================================================ FILE: src/views/flowDesign/index.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/Add.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/ApprovalNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/CcNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/ConditionNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/EndNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/ExclusiveNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/GatewayNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/Node.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/NotifyNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/ServiceNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/StartNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/TimerNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/TreeNode.vue ================================================ ================================================ FILE: src/views/flowDesign/nodes/type.ts ================================================ import type { FilterRules } from '@/components/AdvancedFilter/type' export type NodeType = | 'start' | 'approval' | 'cc' | 'exclusive' | 'timer' | 'notify' | 'service' | 'condition' | 'end' export interface FlowNode { id: string pid?: string name: string type: NodeType executionListeners?: NodeListener[] next?: FlowNode } export interface NodeListener { event: string implementationType: 'class' | 'expression' | 'delegateExpression' implementation: string } export interface StartNode extends FlowNode { formProperties: FormProperty[] } export interface EndNode extends FlowNode {} export interface AssigneeNode extends FlowNode { // 审批方式 assigneeType: | 'user' | 'role' | 'choice' | 'self' | 'leader' | 'orgLeader' | 'formUser' | 'formRole' | 'autoRefuse' // 审批人 users: string[] // 审批角色 roles: string[] // 表单内人员 formUser: string // 表单内角色 formRole: string // 主管 leader: number // 组织主管 orgLeader: number // 自选:true-多选,false-单选 choice: boolean // 发起人自己 self: boolean } export interface CcNode extends AssigneeNode { formProperties: FormProperty[] } export interface NotifyNode extends AssigneeNode { types: ('site' | 'email' | 'sms' | 'wechat' | 'dingtalk' | 'feishu')[] subject: string content: string } export interface ApprovalNode extends AssigneeNode { // 多人审批方式 multi: 'sequential' | 'joint' | 'single' // 审批人为空时处理方式:reject-拒绝,pass-通过,admin-管理员,assign-指定人员 nobody: 'refuse' | 'pass' | 'admin' | 'assign' // 多人审批通过比例 multiPercent: number // 审批人为空时,指定人员 nobodyUsers: string[] // 表单字段 formProperties: FormProperty[] // 操作权限 operations: OperationPermissions // 任务监听器 taskListeners?: NodeListener[] } export interface ServiceNode extends FlowNode { implementationType: string implementation: string } export interface TimerNode extends FlowNode { waitType: 'duration' | 'date' unit: 'PT%sS' | 'PT%sM' | 'PT%sH' | 'P%sD' | 'P%sW' | 'P%sM' duration: number timeDate?: string } export interface ConditionNode extends FlowNode { def: boolean conditions: FilterRules } export interface BranchNode extends FlowNode { branches: FlowNode[] } export interface ExclusiveNode extends BranchNode { branches: ConditionNode[] } export interface ErrorInfo { id: string name: string message: string } export interface FormProperty { // 字段ID id: string // 字段名称 name: string // 只读 readonly: boolean // 必填 required: boolean // 隐藏 hidden: boolean } export interface OperationPermissions { // 同意 complete: boolean // 拒绝 refuse: boolean // 回退 back: boolean // 转交 transfer: boolean // 委派 delegate: boolean // 加签 addMulti: boolean // 减签 minusMulti: boolean } ================================================ FILE: src/views/flowDesign/panels/ApprovalPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/AssigneePanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/CcPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/ConditionPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/EndPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/ExecutionListeners.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/NotifyPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/ServicePanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/StartPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/TaskListeners.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/TimerPanel.vue ================================================ ================================================ FILE: src/views/flowDesign/panels/index.vue ================================================ ================================================ FILE: src/views/home/index.vue ================================================ ================================================ FILE: tsconfig.app.json ================================================ { "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } } ================================================ FILE: tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.app.json" } ] } ================================================ FILE: tsconfig.node.json ================================================ { "extends": "@tsconfig/node20/tsconfig.json", "include": [ "vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*" ], "compilerOptions": { "composite": true, "noEmit": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "module": "ESNext", "moduleResolution": "Bundler", "types": ["node"] } } ================================================ FILE: unocss.config.ts ================================================ import { defineConfig, presetAttributify, presetIcons, presetUno, transformerDirectives, transformerVariantGroup, } from 'unocss' export default defineConfig({ presets: [ presetUno({ dark: 'class' }), presetAttributify(), presetIcons({ scale: 1.2, warn: true, }), ], // 自定义组合样式 shortcuts: { bgc: 'flex red', 'flex-col-center': 'flex-center flex-col', 'flex-col': 'flex flex-col', 'flex-items-center': 'flex items-center', 'flex-center': 'flex-items-center justify-center', 'flex-between': 'flex-items-center justify-between', 'flex-space': 'flex-items-center flex-justify-between', 'wh-full': 'w-full h-full', }, transformers: [ transformerDirectives(), transformerVariantGroup(), ] }) ================================================ FILE: vite.config.ts ================================================ import {fileURLToPath, URL} from 'node:url' import {defineConfig} from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import VueSetupExtend from 'vite-plugin-vue-setup-extend' import Components from 'unplugin-vue-components/vite' import AutoImport from 'unplugin-auto-import/vite' import {viteMockServe} from "vite-plugin-mock"; import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import {ElementPlusResolver} from "unplugin-vue-components/resolvers"; import Unocss from 'unocss/vite' import {resolve} from "path"; // https://vitejs.dev/config/ export default defineConfig({ base: '/lowflow-design', resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server: { host: '0.0.0.0', port: 3200, open: true, proxy: { '/api': { target: 'http://localhost:8084', changeOrigin: true, ws: true, rewrite: (path) => path.replace(/^\/api/, ''), secure: false } } }, plugins: [ vue(), vueJsx(), Unocss(), VueSetupExtend(), viteMockServe({ mockPath: './src/mock', localEnabled: true, prodEnabled: true, injectCode: ` import { setupProdMockServer } from './mockProdServer'; setupProdMockServer(); `, }), Components({ extensions: ['vue', 'tsx', 'md'], globs: ['src/components/*/*.vue', 'src/components/*/*.tsx'], include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.[tj]sx?$/], resolvers: [ ElementPlusResolver({ importStyle: 'sass', }), ], dts: 'src/typings/components.d.ts' }), AutoImport({ imports: ['vue', 'vue-router'], resolvers: [ElementPlusResolver()], dts: 'src/typings/auto-imports.d.ts', eslintrc: { enabled: true, filepath: './.eslintrc-auto-import.json' } }), // svg 图标 createSvgIconsPlugin({ iconDirs: [resolve(process.cwd(), 'src/assets/icons')], symbolId: 'icon-[dir]-[name]' }), ], build: { rollupOptions: { output: { // 打包分类 chunkFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js', assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', // 打包后的文件夹名称生成规则-->解决部分静态服务器无法正常返回_plugin-vue_export-helper文件 sanitizeFileName(name) { const match = /^[a-z]:/i.exec(name) const driveLetter = match ? match[0] : '' return ( driveLetter + // eslint-disable-next-line no-control-regex name.substring(driveLetter.length).replace(/[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,]/g, '') ) } } } } })