chore: ⬆️update deps

main
bqy_fe 3 years ago
parent 0c37e693c2
commit 27fe5fc752
  1. 62
      .eslintrc.js
  2. 50
      .github/workflows/deploy.yml
  3. 8
      .husky/lintstagedrc.js
  4. 2
      .husky/pre-commit
  5. 2
      .vscode/extensions.json
  6. 73
      .vscode/settings.json
  7. 62
      README.md
  8. 4
      auto-imports.d.ts
  9. 11
      commitlint.config.js
  10. 44
      components.d.ts
  11. 146
      package.json
  12. 4700
      pnpm-lock.yaml
  13. 13
      prettier.config.js
  14. 87
      preview/utils/http/request.ts
  15. 36
      src/main.ts
  16. 49
      src/packages/base-widgets/image/index.tsx
  17. 30
      src/packages/base-widgets/rate/index.tsx
  18. 34
      src/packages/base-widgets/slider/index.tsx
  19. 23
      src/plugins/element-plus.ts
  20. 31
      src/store/index.ts
  21. 63
      src/visual-editor/components/common/format-input-number/index.tsx
  22. 92
      src/visual-editor/components/header/index.vue
  23. 134
      src/visual-editor/components/header/useTools.tsx
  24. 37
      src/visual-editor/components/left-aside/components/base-widgets/index.tsx
  25. 37
      src/visual-editor/components/left-aside/components/container-component/index.tsx
  26. 16
      src/visual-editor/components/left-aside/components/custom-component/index.vue
  27. 407
      src/visual-editor/components/left-aside/components/data-source/data-fetch.vue
  28. 428
      src/visual-editor/components/left-aside/components/data-source/data-model.vue
  29. 36
      src/visual-editor/components/left-aside/components/data-source/index.vue
  30. 100
      src/visual-editor/components/left-aside/components/data-source/utils.tsx
  31. 290
      src/visual-editor/components/left-aside/components/page-tree/index.vue
  32. 77
      src/visual-editor/components/left-aside/index.vue
  33. 97
      src/visual-editor/components/right-attribute-panel/components/animate/Animate.tsx
  34. 1
      src/visual-editor/components/right-attribute-panel/components/animate/animate.module.scss
  35. 105
      src/visual-editor/components/right-attribute-panel/components/attr-editor/components/cross-sortable-options-editor/cross-sortable-options-editor.tsx
  36. 78
      src/visual-editor/components/right-attribute-panel/components/attr-editor/components/prop-config/index.tsx
  37. 78
      src/visual-editor/components/right-attribute-panel/components/attr-editor/index.tsx
  38. 191
      src/visual-editor/components/right-attribute-panel/components/event-action/index.tsx
  39. 23
      src/visual-editor/components/right-attribute-panel/components/form-rule/index.tsx
  40. 35
      src/visual-editor/components/right-attribute-panel/components/page-setting/pageSetting.tsx
  41. 2
      src/visual-editor/components/right-attribute-panel/index.module.scss
  42. 31
      src/visual-editor/components/right-attribute-panel/index.tsx
  43. 0
      src/visual-editor/hooks/useSandbox.ts
  44. 274
      src/visual-editor/visual-editor.utils.ts
  45. 215
      stylelint.config.js
  46. 27
      tsconfig.json
  47. 21
      types/env.d.ts
  48. 83
      types/global.d.ts
  49. 34
      types/index.d.ts
  50. 20
      types/modules.d.ts
  51. 52
      types/shims/shims-app.d.ts
  52. 21
      types/shims/shims-vue.d.ts
  53. 34
      types/vue-router.d.ts
  54. 10
      types/vuex.d.ts
  55. 125
      vite.config.ts
  56. 5692
      yarn.lock

@ -1,9 +1,11 @@
module.exports = {
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true,
env: {
browser: true,
node: true,
es6: true
es6: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
@ -12,36 +14,66 @@ module.exports = {
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true
}
jsx: true,
},
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
'prettier'
],
rules: {
'vue/require-default-prop': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'vue/script-setup-uses-vars': 'error',
'@typescript-eslint/ban-ts-ignore': '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',
'vue/custom-event-name-casing': 'off',
'no-use-before-define': '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',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'space-before-function-paren': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always'
component: 'always',
},
svg: 'always',
math: 'always'
}
]
math: 'always',
},
],
'vue/multi-word-component-names': 'off',
},
settings: {}
}
});

@ -1,30 +1,35 @@
name: deploy
name: syncToGitee
env:
# 7 GiB by default on GitHub, setting to 6 GiB
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
NODE_OPTIONS: --max-old-space-size=6144
on:
push:
branches: [main]
jobs:
deploy:
repo-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout Code
uses: actions/checkout@v2
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
- name: Setup Node.js v14.x
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install
run: yarn install --frozen-lockfile
run: pnpm install
- name: Build
run: yarn build
run: pnpm build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
@ -33,3 +38,28 @@ jobs:
personal_token: ${{ secrets.PERSONAL_TOKEN }}
commit_message: Update ghPages
force_orphan: true
- name: Mirror the Github organization repos to Gitee.
uses: Yikun/hub-mirror-action@master
with:
src: 'github/buqiyuan'
dst: 'gitee/buqiyuan'
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }}
static_list: 'vite-vue3-lowcode'
force_update: true
debug: true
- name: Build Gitee Pages
uses: yanglbme/gitee-pages-action@main
with:
# 注意替换为你的 Gitee 用户名
gitee-username: buqiyuan
# 注意在 Settings->Secrets 配置 GITEE_PASSWORD
gitee-password: ${{ secrets.GITEE_PASSWORD }}
# 注意替换为你的 Gitee 仓库,仓库名严格区分大小写,请准确填写,否则会出错
gitee-repo: buqiyuan/vite-vue3-lowcode
# 是否强制使用 HTTPS
https: false
# 要部署的分支,默认是 master,若是其他分支,则需要指定(指定的分支必须存在)
branch: gh-pages

@ -1,8 +0,0 @@
module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
'package.json': ['prettier --write'],
'*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
'*.{scss,less,styl,html}': ['stylelint --fix', 'prettier --write'],
'*.md': ['prettier --write']
}

@ -6,5 +6,3 @@
# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged
npm run lint:pretty

@ -1,6 +1,6 @@
{
"recommendations": [
"octref.vetur",
"johnsoncodehk.volar",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",

@ -1,32 +1,10 @@
{
"cSpell.words": ["windi"],
"typescript.tsdk": "./node_modules/typescript/lib",
"volar.tsPlugin": true,
"volar.tsPluginStatus": false,
//===========================================
//============= Editor ======================
//===========================================
"explorer.openEditors.visible": 0,
"npm.packageManager": "pnpm",
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"diffEditor.ignoreTrimWhitespace": false,
"editor.trimAutoWhitespace": true,
//===========================================
//============= Other =======================
//===========================================
"breadcrumbs.enabled": true,
"open-in-browser.default": "chrome",
//===========================================
//============= emmet =======================
//===========================================
"emmet.triggerExpansionOnTab": true,
"emmet.showAbbreviationSuggestions": true,
//===========================================
//============= files =======================
//===========================================
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.eol": "\n",
"search.exclude": {
"**/node_modules": true,
@ -49,9 +27,14 @@
"CHANGELOG.md": true,
"examples": true,
"res": true,
"screenshots": true
"screenshots": true,
"yarn-error.log": true,
"**/.yarn": true
},
"files.exclude": {
"**/.cache": true,
"**/.editorconfig": true,
"**/.eslintcache": true,
"**/bower_components": true,
"**/.idea": true,
"**/tmp": true,
@ -73,26 +56,9 @@
},
"stylelint.enable": true,
"stylelint.packageManager": "yarn",
// ===========================================
// ================ Vetur ====================
// ===========================================
"vetur.experimental.templateInterpolationService": true,
"vetur.format.options.tabSize": 2,
"vetur.languageFeatures.codeActions": false,
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
"wrap_attributes": "force-expand-multiline"
}
},
"liveServer.settings.donotShowInfoMsg": true,
"telemetry.enableCrashReporter": false,
"workbench.settings.enableNaturalLanguageSearch": false,
"path-intellisense.mappings": {
"@/": "${workspaceRoot}/src"
"/@/": "${workspaceRoot}/src"
},
"prettier.requireConfig": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"workbench.sideBar.location": "left",
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
@ -112,11 +78,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false,
"source.fixAll.stylelint": true
}
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
@ -126,16 +88,23 @@
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false,
"source.fixAll.stylelint": true
"source.fixAll.eslint": false
}
},
"i18n-ally.localesPaths": ["src/locales/lang"],
"i18n-ally.localesPaths": [
"src/locales/lang"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.enabledFrameworks": ["vue", "react"]
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": [
"vue",
"react"
],
}

@ -14,6 +14,25 @@ or
git clone --depth=1 https://github.com/buqiyuan/vite-vue3-lowcode.git
```
```bash
cd vite-vue3-lowcode
pnpm install
```
- run
```bash
pnpm serve
```
- build
```bash
pnpm build
```
## 技术栈
- 编程语言:[TypeScript 4.x](https://www.typescriptlang.org/zh/) + [JavaScript](https://www.javascript.com/)
@ -43,8 +62,7 @@ git clone --depth=1 https://github.com/buqiyuan/vite-vue3-lowcode.git
### 简易说明
目前在使用表单时,需要把相关的`表单控件`放到`表单容器`内部,并且需要将`按钮`放到`表单容器`内,
然后再讲`按钮的type`设置为`表单提交按钮`这时候点击提交按钮才会自动收集表单容器内部的所有字段和值
目前在使用表单时,需要把相关的`表单控件`放到`表单容器`内部,并且需要将`按钮`放到`表单容器`内,然后再讲`按钮的type`设置为`表单提交按钮`这时候点击提交按钮才会自动收集表单容器内部的所有字段和值
### 快速生成组件属性
@ -53,41 +71,41 @@ git clone --depth=1 https://github.com/buqiyuan/vite-vue3-lowcode.git
let propObj = {
string: (config) => `createEditorInputProp(${JSON.stringify(config)})`,
number: (config) => `createEditorInputNumberProp(${JSON.stringify(config)})`,
boolean: (config) => `createEditorSwitchProp(${JSON.stringify(config)})`
}
boolean: (config) => `createEditorSwitchProp(${JSON.stringify(config)})`,
};
JSON.stringify(
$$('#props + table tbody tr').reduce((prev, curr) => {
const children = curr.children
const key = children[0].textContent.replace(/-([a-z])/g, (all, i) => i.toUpperCase())
const child3Text = children[3].textContent
const children = curr.children;
const key = children[0].textContent.replace(/-([a-z])/g, (all, i) => i.toUpperCase());
const child3Text = children[3].textContent;
const defaultValue = ['true', 'false'].includes(child3Text)
? child3Text
: `'${child3Text == '-' ? '' : child3Text}'`
: `'${child3Text == '-' ? '' : child3Text}'`;
const value = (propObj[children[2].textContent] ?? propObj['string'])({
label: `'${children[1].textContent}'`,
defaultValue
}).replaceAll('"', '')
prev[key] = value
return prev
}, {})
).replaceAll('"', '')
defaultValue,
}).replaceAll('"', '');
prev[key] = value;
return prev;
}, {}),
).replaceAll('"', '');
```
```javascript
// 在vant文档中 chrome控制台输入以下代码,快速生成组件事件
JSON.stringify(
$$('#events + table tbody tr').reduce((prev, curr) => {
const children = curr.children
const children = curr.children;
const event = {
label: children[1].textContent,
value: children[0].textContent
}
return prev.concat([event])
}, [])
value: children[0].textContent,
};
return prev.concat([event]);
}, []),
)
.replaceAll(/(?<!:)\"(?!,|})/g, '')
.replace(/\"/g, "'")
.replace(/\"/g, "'");
```
## 部分功能演示
@ -101,8 +119,8 @@ JSON.stringify(
支持现代浏览器, 不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
| :-: | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
### 提交规范

4
auto-imports.d.ts vendored

@ -0,0 +1,4 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
declare global {}
export {};

@ -7,6 +7,7 @@ module.exports = {
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
@ -25,8 +26,8 @@ module.exports = {
'wip',
'workflow',
'types',
'release'
]
]
}
}
'release',
],
],
},
};

44
components.d.ts vendored

@ -3,28 +3,28 @@
declare module 'vue' {
export interface GlobalComponents {
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElMain: typeof import('element-plus/es')['ElMain']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRow: typeof import('element-plus/es')['ElRow']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElAside: typeof import('element-plus/es')['ElAside'];
ElButton: typeof import('element-plus/es')['ElButton'];
ElCol: typeof import('element-plus/es')['ElCol'];
ElCollapse: typeof import('element-plus/es')['ElCollapse'];
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'];
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'];
ElContainer: typeof import('element-plus/es')['ElContainer'];
ElDialog: typeof import('element-plus/es')['ElDialog'];
ElDropdown: typeof import('element-plus/es')['ElDropdown'];
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'];
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'];
ElHeader: typeof import('element-plus/es')['ElHeader'];
ElMain: typeof import('element-plus/es')['ElMain'];
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'];
ElPopover: typeof import('element-plus/es')['ElPopover'];
ElRow: typeof import('element-plus/es')['ElRow'];
ElTabPane: typeof import('element-plus/es')['ElTabPane'];
ElTabs: typeof import('element-plus/es')['ElTabs'];
ElTag: typeof import('element-plus/es')['ElTag'];
ElTooltip: typeof import('element-plus/es')['ElTooltip'];
ElTree: typeof import('element-plus/es')['ElTree'];
}
}
export {}
export {};

@ -2,84 +2,92 @@
"name": "vite-vue3-lowcode",
"version": "0.0.1",
"private": false,
"description": "A Vite2.x + Vue3 + TypeScript LowCode",
"description": "A Vite2.x + Vue3.x + TypeScript LowCode",
"scripts": {
"bootstrap": "pnpm install",
"serve": "npm run dev",
"dev": "cross-env --max_old_space_size=4096 vite",
"build": "cross-env vite build",
"build:no-cache": "pnpm clean:cache && npm run build",
"build-tsc": "vue-tsc --noEmit && vite build",
"serve": "vite preview",
"preview": "npm run build && vite preview",
"preview:dist": "vite preview",
"log": "conventional-changelog -p angular -i CHANGELOG.md -s",
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
"clean:lib": "rimraf node_modules",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged",
"deploy": "gh-pages -d dist",
"format": "prettier --write ./src",
"lint": "eslint ./src --ext .vue,.js,.ts,.tsx",
"lint-fix": "eslint --fix ./src --ext .vue,.js,.ts,.tsx",
"lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
"test:gzip": "npx http-server dist --cors --gzip -c-1",
"test:br": "npx http-server dist --cors --brotli -c-1",
"reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
"prepare": "husky install"
},
"dependencies": {
"@element-plus/icons-vue": "^0.2.4",
"@vant/touch-emulator": "^1.3.2",
"@vueuse/core": "^6.2.1",
"@vueuse/integrations": "^6.2.1",
"@vueuse/core": "^7.5.1",
"@vueuse/integrations": "^7.5.1",
"animate.css": "^4.1.1",
"axios": "^0.21.4",
"dayjs": "^1.10.6",
"dexie": "^3.0.3",
"element-plus": "1.1.0-beta.9",
"axios": "^0.24.0",
"dayjs": "^1.10.7",
"dexie": "^3.2.0",
"element-plus": "1.3.0-beta.1",
"lodash": "^4.17.21",
"monaco-editor": "^0.27.0",
"nanoid": "^3.1.25",
"monaco-editor": "^0.31.1",
"nanoid": "^3.1.30",
"normalize.css": "^8.0.1",
"nprogress": "^1.0.0-1",
"qrcode": "^1.4.4",
"qs": "^6.10.1",
"vant": "3.2.2",
"vue": "3.2.9",
"vue-router": "^4.0.11",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2"
"pinia": "^2.0.9",
"qrcode": "^1.5.0",
"qs": "^6.10.2",
"vant": "3.3.7",
"vue": "3.2.26",
"vue-router": "^4.0.12",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@types/lodash": "^4.14.172",
"@types/node": "^16.7.12",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.0",
"@vitejs/plugin-legacy": "^1.5.2",
"@vitejs/plugin-vue": "^1.6.1",
"@vitejs/plugin-vue-jsx": "^1.1.7",
"@vue/compiler-sfc": "3.2.9",
"@commitlint/cli": "^16.0.1",
"@commitlint/config-conventional": "^16.0.0",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.5",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"@vitejs/plugin-legacy": "^1.6.4",
"@vitejs/plugin-vue": "^2.0.1",
"@vitejs/plugin-vue-jsx": "^1.3.3",
"@vue/compiler-sfc": "3.2.26",
"commitizen": "^4.2.4",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.3.0",
"eslint": "^7.32.0",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.24.2",
"eslint-define-config": "^1.2.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.17.0",
"eslint-plugin-vue": "^8.2.0",
"gh-pages": "^3.2.3",
"husky": "^7.0.2",
"lint-staged": "^11.1.2",
"prettier": "^2.3.2",
"pretty-quick": "^3.1.1",
"sass": "1.39.0",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0",
"stylelint-scss": "^3.20.1",
"typescript": "^4.4.2",
"vite": "2.5.3",
"vite-plugin-components": "^0.13.3",
"vite-plugin-element-plus": "^0.0.12",
"vite-plugin-windicss": "^1.4.1",
"vue-eslint-parser": "^7.11.0",
"vue-tsc": "^0.3.0",
"windicss": "^3.1.7"
"husky": "^7.0.4",
"lint-staged": "^12.1.4",
"postcss-html": "^1.3.0",
"prettier": "^2.5.1",
"sass": "1.45.2",
"stylelint": "^14.2.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0",
"stylelint-scss": "^4.1.0",
"typescript": "^4.5.4",
"unplugin-auto-import": "^0.5.5",
"unplugin-vue-components": "^0.17.11",
"vite": "2.7.10",
"vite-plugin-windicss": "^1.6.1",
"vue-eslint-parser": "^8.0.1",
"windicss": "^3.4.2"
},
"repository": {
"type": "git",
@ -97,7 +105,31 @@
"url": "https://github.com/buqiyuan/vite-vue3-lowcode/issues"
},
"homepage": "https://github.com/buqiyuan/vite-vue3-lowcode#readme",
"engines": {
"node": "^12 || >=14"
},
"lint-staged": {
"*.{vue,js,ts,tsx}": "eslint --fix"
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
"prettier --write--parser json"
],
"package.json": [
"prettier --write"
],
"*.vue": [
"eslint --fix",
"prettier --write",
"stylelint --fix"
],
"*.{scss,less,styl,html}": [
"stylelint --fix",
"prettier --write"
],
"*.md": [
"prettier --write"
]
}
}

File diff suppressed because it is too large Load Diff

@ -1,9 +1,10 @@
module.exports = {
useTabs: false,
tabWidth: 2,
printWidth: 100,
semi: true,
vueIndentScriptAndStyle: true,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: true,
semi: false
}
trailingComma: 'all',
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
};

@ -6,27 +6,27 @@
* @Description: axios简单的封装
* @FilePath: \vite-vue3-lowcode\src\utils\http\request.ts
*/
import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'
import store from '@/store'
import { Toast } from 'vant'
import router from '@/router'
import { ContentTypeEnum } from './httpEnum'
import axios, { AxiosRequestConfig } from 'axios';
import qs from 'qs';
// import store from '@/store'
import { Toast } from 'vant';
import router from '@/router';
import { ContentTypeEnum } from './httpEnum';
// create an axios instance
const service = axios.create({
baseURL: import.meta.env.VITE_API_URL as string, // url = base api url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 10000 // request timeout
})
timeout: 10000, // request timeout
});
interface CustomAxiosRequestConfig extends AxiosRequestConfig {
hideLoading?: boolean
hideLoading?: boolean;
}
interface BaseResponse<T = any> {
code: number
data: T
msg: string
code: number;
data: T;
msg: string;
}
// request拦截器 request interceptor
@ -36,68 +36,67 @@ service.interceptors.request.use(
if (!config.hideLoading) {
// loading
Toast.loading({
forbidClick: true
})
forbidClick: true,
});
}
console.log(store.getters, "store.getters['user']")
if (store.getters['user/token']) {
config.headers['X-Token'] = store.getters['user/token']
}
const contentType = config.headers['content-type'] || config.headers['Content-Type']
const data = config.data
// if (store.getters['user/token'] && config.headers) {
// config.headers['X-Token'] = store.getters['user/token']
// }
const contentType = config.headers?.['content-type'] || config.headers?.['Content-Type'];
const data = config.data;
if (config.method?.toLocaleUpperCase() == 'POST' && data) {
if (ContentTypeEnum.FORM_DATA == contentType) {
const fd = new FormData()
Object.keys(data).forEach((key) => fd.append(key, data[key]))
config.data = fd
const fd = new FormData();
Object.keys(data).forEach((key) => fd.append(key, data[key]));
config.data = fd;
} else if (ContentTypeEnum.FORM_URLENCODED == contentType) {
config.data = qs.stringify(config.data)
config.data = qs.stringify(config.data);
}
}
return config
return config;
},
(error) => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
console.log(error); // for debug
return Promise.reject(error);
},
);
// respone拦截器
service.interceptors.response.use(
(response) => {
Toast.clear()
const res = response.data
Toast.clear();
const res = response.data;
if (res.code && res.code !== 0) {
// 登录超时,重新登录
if (res.code === 401) {
// store.dispatch('FedLogOut').then(() => {
// location.reload()
// })
router.replace('/error')
router.replace('/error');
} else {
Toast(res.msg || '服务器访问出错了~')
Toast(res.msg || '服务器访问出错了~');
}
return Promise.reject(res || 'error')
return Promise.reject(res || 'error');
} else {
return Promise.resolve(response)
return Promise.resolve(response);
}
},
(error: Error) => {
if (error.message?.includes('timeout')) {
Toast('请求超时!')
Toast('请求超时!');
}
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
console.log('err' + error); // for debug
return Promise.reject(error);
},
);
const request = <T = any>(config: CustomAxiosRequestConfig): Promise<BaseResponse<T>> => {
return new Promise((resolve, reject) => {
service
.request<BaseResponse<T>>(config)
.then((res) => resolve(res.data))
.catch((err) => reject(err))
})
}
.catch((err) => reject(err));
});
};
export default request
export default request;

@ -1,29 +1,31 @@
import { createApp } from 'vue'
import App from './App.vue'
import { createApp } from 'vue';
import App from './App.vue';
import { setupElementPlus } from './plugins/element-plus'
import { setupVant } from './plugins/vant'
import { setupElementPlus } from './plugins/element-plus';
import { setupVant } from './plugins/vant';
import 'normalize.css'
import 'virtual:windi.css'
import 'virtual:windi-devtools'
import 'animate.css'
import 'normalize.css';
import 'virtual:windi.css';
import 'virtual:windi-devtools';
import 'animate.css';
import router from './router/'
import store from './store/'
import router from './router/';
import { setupStore } from './store/';
const app = createApp(App)
const app = createApp(App);
// 配置store
setupStore(app);
// 使用element-plus插件
setupElementPlus(app)
setupElementPlus(app);
// 使用vant插件
setupVant(app)
setupVant(app);
app.config.globalProperties.$$refs = {}
app.config.globalProperties.$$refs = {};
// if (import.meta.env.DEV) {
window.$$refs = app.config.globalProperties.$$refs
window.$$refs = app.config.globalProperties.$$refs;
// }
app.use(router).use(store)
app.use(router);
// 路由准备完毕再挂载
router.isReady().then(() => app.mount('#app'))
router.isReady().then(() => app.mount('#app'));

@ -6,14 +6,15 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\image\index.tsx
*/
import { Image } from 'vant'
import { Image } from 'vant';
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
createEditorSwitchProp,
} from '@/visual-editor/visual-editor.props';
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { useGlobalProperties } from '@/hooks/useGlobalProperties';
import { Picture } from '@element-plus/icons-vue';
export default {
key: 'image',
@ -21,28 +22,30 @@ export default {
label: '图片',
resize: {
width: true,
height: true
height: true,
},
preview: () => (
<div style="text-align:center;">
<div style="font-size:20px;background-color:#f2f2f2;color:#ccc;display:inline-flex;width:100px;height:50px;align-items:center;justify-content:center">
<i class="el-icon-picture" />
<el-icon>
<Picture></Picture>
</el-icon>
</div>
</div>
),
render: ({ props, block, styles }) => {
const { registerRef } = useGlobalProperties()
const { registerRef } = useGlobalProperties();
return () => (
<div style={styles}>
<Image ref={(el) => registerRef(el, block._vid)} {...props} />
</div>
)
);
},
props: {
src: createEditorInputProp({
label: '图片链接',
defaultValue: 'https://img.yzcdn.cn/vant/cat.jpeg'
defaultValue: 'https://img.yzcdn.cn/vant/cat.jpeg',
}),
width: createEditorInputProp({ label: '宽度', defaultValue: 100 }),
height: createEditorInputProp({ label: '高度', defaultValue: 100 }),
@ -52,46 +55,46 @@ export default {
options: [
{
label: '保持宽高缩放图片,使图片的长边能完全显示出来',
value: 'contain'
value: 'contain',
},
{
label: '保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边',
value: 'cover'
value: 'cover',
},
{
label: '拉伸图片,使图片填满元素',
value: 'fill'
value: 'fill',
},
{
label: '保持图片原有尺寸',
value: 'none'
value: 'none',
},
{
label: '取 none 或 contain 中较小的一个',
value: 'scale-down'
}
value: 'scale-down',
},
],
defaultValue: 'fill'
defaultValue: 'fill',
}),
iconPrefix: createEditorInputProp({
label: '图标类名前缀',
tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性'
tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性',
}),
iconSize: createEditorInputProp({ label: '加载图标和失败图标的大小' }),
lazyLoad: createEditorSwitchProp({
label: '是否开启图片懒加载',
tips: '须配合 Lazyload 组件使用'
tips: '须配合 Lazyload 组件使用',
}),
loadingIcon: createEditorInputProp({ label: '加载时提示的图标名称或图片链接' }),
radius: createEditorInputProp({ label: '圆角大小', tips: '默认单位为 px' }),
round: createEditorSwitchProp({ label: '是否显示为圆形' }),
'show-error': createEditorSwitchProp({ label: '是否展示图片加载失败提示' }),
'show-loading': createEditorSwitchProp({ label: '是否展示图片加载中提示' }),
alt: createEditorInputProp({ label: '替代文本' })
alt: createEditorInputProp({ label: '替代文本' }),
},
events: [
{ label: '点击图片时触发', value: 'click' },
{ label: '图片加载完毕时触发', value: 'load' },
{ label: '图片加载失败时触发', value: 'error' }
]
} as VisualEditorComponent
{ label: '图片加载失败时触发', value: 'error' },
],
} as VisualEditorComponent;

@ -6,16 +6,16 @@
* @Description: -
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\rate\index.tsx
*/
import { Field, Rate } from 'vant'
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { Field, Rate } from 'vant';
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { createFieldProps } from './createFieldProps';
import { useGlobalProperties } from '@/hooks/useGlobalProperties';
import {
createEditorInputNumberProp,
createEditorInputProp,
createEditorModelBindProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
createEditorSwitchProp,
} from '@/visual-editor/visual-editor.props';
export default {
key: 'rate',
@ -30,7 +30,7 @@ export default {
></Field>
),
render: ({ styles, block, props }) => {
const { registerRef } = useGlobalProperties()
const { registerRef } = useGlobalProperties();
return () => (
<div style={styles}>
@ -45,25 +45,25 @@ export default {
{...props}
v-model={props.modelValue}
></Rate>
)
),
}}
/>
</div>
)
);
},
props: {
modelValue: createEditorInputNumberProp({ label: '默认值', defaultValue: 0 }),
name: createEditorModelBindProp({ label: '字段绑定', defaultValue: '' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '评分' }),
count: createEditorInputNumberProp({ label: '图标总数' }),
count: createEditorInputNumberProp({ label: '图标总数', defaultValue: 5 }),
size: createEditorInputProp({ label: '图标大小' }),
'allow-half': createEditorSwitchProp({ label: '是否允许半选' }),
...createFieldProps()
...createFieldProps(),
},
resize: {
width: true
width: true,
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent
default: '绑定字段',
},
} as VisualEditorComponent;

@ -6,16 +6,17 @@
* @Description: -
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\slider\index.tsx
*/
import { Field, Slider } from 'vant'
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { Field, Slider } from 'vant';
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { createFieldProps } from './createFieldProps';
import { useGlobalProperties } from '@/hooks/useGlobalProperties';
import {
createEditorInputNumberProp,
createEditorInputProp,
createEditorModelBindProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
createEditorSwitchProp,
} from '@/visual-editor/visual-editor.props';
import { omit } from 'lodash';
export default {
key: 'slider',
@ -30,13 +31,12 @@ export default {
></Field>
),
render: ({ styles, block, props }) => {
const { registerRef } = useGlobalProperties()
const { registerRef } = useGlobalProperties();
return () => (
<div style={styles}>
<Field
{...props}
modelValue={''}
{...omit(props, 'size')}
name={Array.isArray(props.name) ? [...props.name].pop() : props.name}
v-slots={{
input: () => (
@ -45,26 +45,26 @@ export default {
{...props}
v-model={props.modelValue}
></Slider>
)
),
}}
/>
</div>
)
);
},
props: {
modelValue: createEditorInputNumberProp({ label: '默认值', defaultValue: 0 }),
name: createEditorModelBindProp({ label: '字段绑定', defaultValue: '' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '滑块' }),
min: createEditorInputNumberProp({ label: '最小值' }),
max: createEditorInputNumberProp({ label: '最大值' }),
max: createEditorInputNumberProp({ label: '最大值', defaultValue: 10 }),
size: createEditorInputNumberProp({ label: '图标大小' }),
range: createEditorSwitchProp({ label: '是否开启双滑块模式' }),
...createFieldProps()
...createFieldProps(),
},
resize: {
width: true
width: true,
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent
default: '绑定字段',
},
} as VisualEditorComponent;

@ -1,5 +1,6 @@
import { App, Component } from 'vue'
import 'element-plus/dist/index.css'
import { App, Component } from 'vue';
import 'element-plus/dist/index.css';
import {
ElAffix,
ElSkeleton,
@ -32,8 +33,8 @@ import {
ElAlert,
ElRadioButton,
ElRadioGroup,
ElInfiniteScroll
} from 'element-plus'
ElInfiniteScroll,
} from 'element-plus';
const components = [
ElAffix,
@ -65,16 +66,16 @@ const components = [
ElPagination,
ElAlert,
ElRadioButton,
ElRadioGroup
]
ElRadioGroup,
];
const plugins = [ElLoading, ElInfiniteScroll]
const plugins = [ElLoading, ElInfiniteScroll];
export function setupElementPlus(app: App) {
components.forEach((component: Component) => {
app.component(component.name!, component)
})
app.component(component.name!, component);
});
plugins.forEach((plugin) => {
app.use(plugin)
})
app.use(plugin);
});
}

@ -1,27 +1,10 @@
import { createStore } from 'vuex'
import type { App } from 'vue';
import { createPinia } from 'pinia';
const defaultState = {
count: 0
const store = createPinia();
export function setupStore(app: App<Element>) {
app.use(store);
}
// Create a new store instance.
export default createStore({
state() {
return defaultState
},
mutations: {
increment(state: typeof defaultState) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
},
getters: {
double(state: typeof defaultState) {
return 2 * state.count
}
}
})
export { store };

@ -6,51 +6,52 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\common\format-input-number\index.tsx
*/
import { defineComponent } from 'vue'
import { ElInput } from 'element-plus'
import type { PropType } from 'vue'
import { useVModel } from '@vueuse/core'
import styles from './index.module.scss'
import { defineComponent } from 'vue';
import { ElInput, ElIcon } from 'element-plus';
import type { PropType } from 'vue';
import { useVModel } from '@vueuse/core';
import styles from './index.module.scss';
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
export const FormatInputNumber = defineComponent({
props: {
modelValue: {
type: [String] as PropType<string>,
default: ''
default: '',
},
symbol: {
// 符号
type: String as PropType<string>,
default: 'px'
default: 'px',
},
max: {
type: [Number],
default: 100
default: 100,
},
min: {
type: [Number],
default: 0
}
default: 0,
},
},
emits: ['update:modelValue'],
setup(props, { attrs }) {
const modelValue = useVModel(props, 'modelValue')
const modelValue = useVModel(props, 'modelValue');
const onInput = (val) => {
let num = parseFloat(`${val}`.replace(/[^0-9]/gi, ''))
num = Number.isNaN(num) ? 0 : num
num = Math.max(props.min, num)
num = Math.min(props.max, num)
modelValue.value = num + props.symbol
}
let num = parseFloat(`${val}`.replace(/[^0-9]/gi, ''));
num = Number.isNaN(num) ? 0 : num;
num = Math.max(props.min, num);
num = Math.min(props.max, num);
modelValue.value = num + props.symbol;
};
const increment = () => {
onInput(parseFloat(modelValue.value) + 1)
}
onInput(parseFloat(modelValue.value) + 1);
};
const cutdown = () => {
onInput(Math.max(props.min, parseFloat(modelValue.value) - 1))
}
onInput(Math.max(props.min, parseFloat(modelValue.value) - 1));
};
return () => (
<div class={styles.formatInputNumber}>
@ -63,13 +64,21 @@ export const FormatInputNumber = defineComponent({
{{
append: () => (
<div class={'flex flex-col'}>
<div onClick={increment} class={'el-icon-arrow-up cursor-pointer'}></div>
<div onClick={cutdown} class={'el-icon-arrow-down cursor-pointer'}></div>
<div onClick={increment} class={'cursor-pointer leading-0'}>
<ElIcon size={14}>
<ArrowUp />
</ElIcon>
</div>
<div onClick={cutdown} class={'cursor-pointer leading-0'}>
<ElIcon size={14}>
<ArrowDown />
</ElIcon>
</div>
</div>
)
),
}}
</ElInput>
</div>
)
}
})
);
},
});

@ -14,7 +14,9 @@
class="tool-item flex flex-col items-center cursor-pointer"
@click="toolItem.onClick"
>
<i :class="toolItem.icon"></i>
<el-icon>
<component :is="toolItem.icon" />
</el-icon>
<div class="title">{{ toolItem.title }}</div>
</div>
</div>
@ -26,8 +28,8 @@
<el-tooltip class="item" effect="dark" content="运行" placement="bottom">
<el-button
type="primary"
size="mini"
icon="el-icon-video-play"
:icon="VideoPlay"
size="large"
circle
class="flex-shrink-0 !p-6px"
@click="runPreview"
@ -65,66 +67,52 @@
<preview v-model:visible="isShowH5Preview" />
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import Preview from './preview.vue'
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData'
import { BASE_URL } from '@/visual-editor/utils'
import { useTools } from './useTools'
<script lang="ts" setup>
import { ref } from 'vue';
import Preview from './preview.vue';
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData';
import { BASE_URL } from '@/visual-editor/utils';
import { useTools } from './useTools';
import { VideoPlay } from '@element-plus/icons-vue';
export default defineComponent({
name: 'Header',
components: { Preview },
setup() {
const state = reactive({
isShowH5Preview: false
})
const isShowH5Preview = ref(false);
const tools = useTools()
const tools = useTools();
const { jsonData } = useVisualData()
const { jsonData } = useVisualData();
const runPreview = () => {
sessionStorage.setItem(localKey, JSON.stringify(jsonData))
localStorage.setItem(localKey, JSON.stringify(jsonData))
state.isShowH5Preview = true
}
return {
...toRefs(state),
BASE_URL,
tools,
runPreview
}
}
})
const runPreview = () => {
sessionStorage.setItem(localKey, JSON.stringify(jsonData));
localStorage.setItem(localKey, JSON.stringify(jsonData));
isShowH5Preview.value = true;
};
</script>
<style lang="scss" scoped>
.header {
width: 100%;
.header {
width: 100%;
.logo {
width: 60px;
height: 60px;
background-image: url('@/assets/logo.png');
background-repeat: no-repeat;
background-size: contain;
}
.logo {
width: 60px;
height: 60px;
background-image: url('@/assets/logo.png');
background-repeat: no-repeat;
background-size: contain;
}
.tool-item {
.title {
margin-top: 4px;
font-size: 12px;
.tool-item {
.title {
margin-top: 4px;
font-size: 12px;
}
}
}
.el-button {
font-size: 22px;
}
.el-button {
font-size: 22px;
}
.right-tools > * {
margin-left: 8px;
.right-tools > * {
margin-left: 8px;
}
}
}
</style>

@ -5,39 +5,51 @@
* @descriptiontools
* @update: 2021/5/7 10:46
*/
import { reactive } from 'vue'
import { ElMessage, ElRadio, ElRadioGroup } from 'element-plus'
import { useQRCode } from '@vueuse/integrations'
import { useClipboard } from '@vueuse/core'
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData'
import { useModal } from '@/visual-editor/hooks/useModal'
import MonacoEditor from '@/visual-editor/components/common/monaco-editor/MonacoEditor'
import { reactive } from 'vue';
import { ElMessage, ElRadio, ElRadioGroup } from 'element-plus';
import { useQRCode } from '@vueuse/integrations';
import { useClipboard } from '@vueuse/core';
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData';
import { useModal } from '@/visual-editor/hooks/useModal';
import MonacoEditor from '@/visual-editor/components/common/monaco-editor/MonacoEditor';
import {
DocumentCopy,
Cellphone,
RefreshLeft,
RefreshRight,
Position,
Delete,
ChatLineSquare,
Download,
Upload,
} from '@element-plus/icons-vue';
import 'element-plus/es/components/message/style/css';
export const useTools = () => {
const { jsonData, updatePage, currentPage, overrideProject } = useVisualData()
const { jsonData, updatePage, currentPage, overrideProject } = useVisualData();
const state = reactive({
coverRadio: 'current',
importJsonValue: ''
})
importJsonValue: '',
});
const importJsonChange = (value) => {
state.importJsonValue = value
}
state.importJsonValue = value;
};
return [
{
title: '导入JSON',
icon: 'el-icon-upload2',
icon: Upload,
onClick: () => {
useModal({
title: '导入JSON',
props: {
width: 642
width: 642,
},
content: () => (
<>
<ElRadioGroup v-model={state.coverRadio}>
<ElRadio label={'current'}></ElRadio>
<ElRadio label={'all'}></ElRadio>
<ElRadio label="current"></ElRadio>
<ElRadio label="all"></ElRadio>
</ElRadioGroup>
<MonacoEditor
onChange={importJsonChange}
@ -47,119 +59,119 @@ export const useTools = () => {
</>
),
onConfirm: () => {
const isCoverCurrent = state.coverRadio == 'current'
const isCoverCurrent = state.coverRadio == 'current';
// 覆盖当前页面
if (isCoverCurrent) {
updatePage({
oldPath: currentPage.value.path,
page: JSON.parse(state.importJsonValue)
})
page: JSON.parse(state.importJsonValue),
});
} else {
// 覆盖整个项目
overrideProject(JSON.parse(state.importJsonValue))
overrideProject(JSON.parse(state.importJsonValue));
}
ElMessage({
showClose: true,
type: 'success',
duration: 2000,
message: isCoverCurrent ? '成功覆盖当前页面' : '成功覆盖整个项目'
})
}
})
}
message: isCoverCurrent ? '成功覆盖当前页面' : '成功覆盖整个项目',
});
},
});
},
},
{
title: '导出JSON',
icon: 'el-icon-download',
icon: Download,
onClick: () => {
const { copy } = useClipboard({ source: JSON.stringify(jsonData) })
const { copy } = useClipboard({ source: JSON.stringify(jsonData) });
copy()
.then(() => ElMessage.success('复制成功'))
.catch((err) => ElMessage.error(`复制失败:${err}`))
}
.catch((err) => ElMessage.error(`复制失败:${err}`));
},
},
{
title: '真机预览',
icon: 'el-icon-mobile-phone',
icon: Cellphone,
onClick: () => {
const qrcode = useQRCode(location.origin + '/preview')
const qrcode = useQRCode(location.origin + '/preview');
useModal({
title: '预览二维码(暂不可用)',
props: {
width: 300
width: 300,
},
footer: null,
content: () => (
<div class={'flex justify-center'}>
<img width={220} height={220} src={qrcode.value} />
</div>
)
})
}
),
});
},
},
{
title: '复制页面',
icon: 'el-icon-document-copy',
icon: DocumentCopy,
onClick: () => {
ElMessage({
showClose: true,
type: 'info',
duration: 2000,
message: '敬请期待!'
})
}
message: '敬请期待!',
});
},
},
{
title: '撤销',
icon: 'el-icon-refresh-left',
icon: RefreshLeft,
onClick: () => {
ElMessage({
showClose: true,
type: 'info',
duration: 2000,
message: '敬请期待!'
})
}
message: '敬请期待!',
});
},
},
{
title: '重做',
icon: 'el-icon-refresh-right',
icon: RefreshRight,
onClick: () => {
ElMessage({
showClose: true,
type: 'info',
duration: 2000,
message: '敬请期待!'
})
}
message: '敬请期待!',
});
},
},
{
title: '清空页面',
icon: 'el-icon-delete',
icon: Delete,
onClick: () => {
ElMessage({
showClose: true,
type: 'info',
duration: 2000,
message: '敬请期待!'
})
}
message: '敬请期待!',
});
},
},
{
title: '预览',
icon: 'el-icon-position',
icon: Position,
onClick: () => {
localStorage.setItem(localKey, JSON.stringify(jsonData))
window.open(location.href.replace('/#/', '/preview/#/'))
}
localStorage.setItem(localKey, JSON.stringify(jsonData));
window.open(location.href.replace('/#/', '/preview/#/'));
},
},
{
title: '反馈',
icon: 'el-icon-chat-line-square',
icon: ChatLineSquare,
onClick: () => {
window.open('https://github.com/buqiyuan/vite-vue3-lowcode/issues/new')
}
}
]
}
window.open('https://github.com/buqiyuan/vite-vue3-lowcode/issues/new');
},
},
];
};

@ -6,30 +6,31 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\base-widgets\index.tsx
*/
import { defineComponent, ref } from 'vue'
import { cloneDeep } from 'lodash'
import { visualConfig } from '@/visual.config'
import styles from './index.module.scss'
import { createNewBlock } from '@/visual-editor/visual-editor.utils'
import DraggableTransitionGroup from '@/visual-editor/components/simulator-editor/draggable-transition-group.vue'
import { defineComponent, ref } from 'vue';
import { cloneDeep } from 'lodash';
import { visualConfig } from '@/visual.config';
import styles from './index.module.scss';
import { createNewBlock } from '@/visual-editor/visual-editor.utils';
import DraggableTransitionGroup from '@/visual-editor/components/simulator-editor/draggable-transition-group.vue';
import { Edit } from '@element-plus/icons-vue';
export default defineComponent({
name: 'BaseWidgets',
label: '基本组件',
order: 3,
icon: 'el-icon-edit',
icon: Edit,
setup() {
const baseWidgets = ref(visualConfig.componentModules.baseWidgets)
const baseWidgets = ref(visualConfig.componentModules.baseWidgets);
const log = (evt) => {
window.console.log('onChange:', evt)
}
window.console.log('onChange:', evt);
};
// 克隆组件
const cloneDog = (comp) => {
console.log('当前拖拽的组件:', comp)
const newComp = cloneDeep(comp)
return createNewBlock(newComp)
}
console.log('当前拖拽的组件:', comp);
const newComp = cloneDeep(comp);
return createNewBlock(newComp);
};
return () => (
<>
@ -46,10 +47,10 @@ export default defineComponent({
<div class={styles.listGroupItem} data-label={element.label}>
{element.preview()}
</div>
)
),
}}
</DraggableTransitionGroup>
</>
)
}
})
);
},
});

@ -6,29 +6,30 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\container-component\index.tsx
*/
import { defineComponent } from 'vue'
import { cloneDeep } from 'lodash'
import { visualConfig } from '@/visual.config'
import Draggable from 'vuedraggable'
import styles from './index.module.scss'
import { createNewBlock } from '@/visual-editor/visual-editor.utils'
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { defineComponent } from 'vue';
import { cloneDeep } from 'lodash';
import { visualConfig } from '@/visual.config';
import Draggable from 'vuedraggable';
import styles from './index.module.scss';
import { createNewBlock } from '@/visual-editor/visual-editor.utils';
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { Suitcase } from '@element-plus/icons-vue';
export default defineComponent({
name: 'ContainerComponent',
label: '容器组件',
icon: 'el-icon-suitcase',
icon: Suitcase,
order: 4,
setup() {
const log = (evt) => {
window.console.log(evt)
}
window.console.log(evt);
};
// 克隆组件
const cloneDog = (comp) => {
console.log('当前拖拽的组件:', comp)
const newComp = cloneDeep(comp)
return createNewBlock(newComp)
}
console.log('当前拖拽的组件:', comp);
const newComp = cloneDeep(comp);
return createNewBlock(newComp);
};
return () => (
<>
@ -47,10 +48,10 @@ export default defineComponent({
<div class={styles.listGroupItem} data-label={element.label}>
{element.preview()}
</div>
)
),
}}
</Draggable>
</>
)
}
})
);
},
});

@ -1,13 +1,15 @@
<!--业务组件-->
<template>业务组件</template>
<script>
export default {
name: 'CustomComponent',
label: '业务组件',
order: 5,
icon: 'el-icon-upload'
}
<script lang="ts">
import { Upload } from '@element-plus/icons-vue';
export default {
name: 'CustomComponent',
label: '业务组件',
order: 5,
icon: Upload,
};
</script>
<style scoped></style>

@ -8,13 +8,11 @@
-->
<template>
<div class="!mb-10px">
<el-button type="primary" size="small" @click="showModelMoal">添加</el-button>
<el-button type="warning" size="small" @click="showImportSwaggerJsonModal"
>导入swagger</el-button
>
<el-button type="primary" @click="showModelMoal">添加</el-button>
<el-button type="warning" @click="showImportSwaggerJsonModal">导入swagger</el-button>
<el-popconfirm title="确定要删除全部接口吗?" @confirm="updateFetchApi([], true)">
<template #reference>
<el-button type="danger" size="small">清空</el-button>
<el-button type="danger">清空</el-button>
</template>
</el-popconfirm>
</div>
@ -25,17 +23,18 @@
<div class="model-item-title">
<span class="truncate w-160px">{{ item.name }}</span>
<div class="model-actions">
<i class="el-icon-edit" @click="editApiItem(item)"></i>
<el-icon size="24" color="#2196f3" @click.stop="editApiItem(item)">
<Edit />
</el-icon>
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定要删除该接口吗?"
@confirm="deleteFetchApi(item.key)"
>
<template #reference>
<i class="el-icon-delete"></i>
<el-icon size="24" color="#f44336"><Delete /></el-icon>
</template>
</el-popconfirm>
</div>
@ -50,229 +49,217 @@
</template>
<script setup lang="tsx">
import { reactive, ref, computed } from 'vue'
import {
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElButton,
ElMessage,
ElCascader
} from 'element-plus'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import type { FetchApiItem, VisualEditorModel } from '@/visual-editor/visual-editor.utils'
import { useModal } from '@/visual-editor/hooks/useModal'
import { cloneDeep } from 'lodash'
import { generateNanoid } from '@/visual-editor/utils/'
import { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum'
import { useImportSwaggerJsonModal } from './utils'
import { reactive, ref, computed } from 'vue';
import {
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElButton,
ElMessage,
ElCascader,
} from 'element-plus';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import type { FetchApiItem, VisualEditorModel } from '@/visual-editor/visual-editor.utils';
import { useModal } from '@/visual-editor/hooks/useModal';
import { cloneDeep } from 'lodash';
import { generateNanoid } from '@/visual-editor/utils/';
import { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { useImportSwaggerJsonModal } from './utils';
import { Delete, Edit } from '@element-plus/icons-vue';
interface IState {
activeNames: string[]
ruleForm: FetchApiItem
}
interface IState {
activeNames: string[];
ruleForm: FetchApiItem;
}
const { jsonData, incrementFetchApi, updateFetchApi, deleteFetchApi } = useVisualData()
const { showImportSwaggerJsonModal } = useImportSwaggerJsonModal()
/**
* @description 接口集合
*/
const apis = computed(() => cloneDeep(jsonData.actions.fetch.apis))
const { jsonData, incrementFetchApi, updateFetchApi, deleteFetchApi } = useVisualData();
const { showImportSwaggerJsonModal } = useImportSwaggerJsonModal();
/**
* @description 接口集合
*/
const apis = computed(() => cloneDeep(jsonData.actions.fetch.apis));
/**
* @description 模型集合
*/
const models = computed(() => cloneDeep(jsonData.models))
/**
* @description 模型集合
*/
const models = computed(() => cloneDeep(jsonData.models));
/**
* @description 是否处于编辑状态
*/
const isEdit = computed(() => apis.value.some((item) => item.key == state.ruleForm.key))
/**
* @description 是否处于编辑状态
*/
const isEdit = computed(() => apis.value.some((item) => item.key == state.ruleForm.key));
/**
* @description 创建空的数据接口对象
*/
const createEmptyApiItem = (): FetchApiItem => ({
key: generateNanoid(),
name: '',
options: {
url: '', // url
method: RequestEnum.GET, //
contentType: 'JSON' //
},
data: {
bind: '', //
recv: '' //
}
})
const ruleFormRef = ref<InstanceType<typeof ElForm>>()
/**
* @description 创建空的数据接口对象
*/
const createEmptyApiItem = (): FetchApiItem => ({
key: generateNanoid(),
name: '',
options: {
url: '', // url
method: RequestEnum.GET, //
contentType: 'JSON', //
},
data: {
bind: '', //
recv: '', //
},
});
const ruleFormRef = ref<InstanceType<typeof ElForm>>();
const state = reactive<IState>({
activeNames: [],
ruleForm: createEmptyApiItem()
})
const state = reactive<IState>({
activeNames: [],
ruleForm: createEmptyApiItem(),
});
const rules = {
name: [{ required: true, message: '请输入接口名称', trigger: 'change' }],
'options.url': [{ required: true, message: '请输入接口名称', trigger: 'change' }],
'options.contentType': [{ required: true, message: '请选择内容类型', trigger: 'change' }]
}
const rules = {
name: [{ required: true, message: '请输入接口名称', trigger: 'change' }],
'options.url': [{ required: true, message: '请输入接口名称', trigger: 'change' }],
'options.contentType': [{ required: true, message: '请选择内容类型', trigger: 'change' }],
};
const handleBindChange = (e: VisualEditorModel[]) => {
console.log(e, 'kkk')
}
const handleBindChange = (e: VisualEditorModel[]) => {
console.log(e, 'kkk');
};
/**
* @description 显示添加接口弹窗
*/
const showModelMoal = () => {
const operateType = isEdit.value ? '编辑' : '新增'
useModal({
title: `${operateType}接口`,
props: {
width: 600
},
content: () => (
<ElForm
model={state.ruleForm}
ref={ruleFormRef}
label-width="100px"
size={'mini'}
rules={rules}
>
<ElFormItem label="名称" prop="name">
<ElInput v-model={state.ruleForm.name} placeholder={'请输入接口名称'}></ElInput>
</ElFormItem>
<ElFormItem label="接口" prop={'options.url'}>
<ElInput v-model={state.ruleForm.options.url} placeholder={'请输入接口地址'}>
{{
prepend: () => (
<ElSelect v-model={state.ruleForm.options.method} class={'w-90px'}>
{Object.keys(RequestEnum).map((key) => (
<ElOption key={key} label={key} value={key}></ElOption>
))}
</ElSelect>
)
}}
</ElInput>
</ElFormItem>
<ElFormItem label="内容类型" prop={'options.contentType'}>
<ElSelect v-model={state.ruleForm.options.contentType}>
{Object.keys(ContentTypeEnum).map((key) => (
<ElOption key={key} label={key} value={key}></ElOption>
))}
</ElSelect>
</ElFormItem>
<ElFormItem label="请求数据" prop={'data.bind'}>
<ElCascader
v-model={state.ruleForm.data.bind}
options={models.value}
clearable={true}
props={{
checkStrictly: true,
children: 'entitys',
label: 'name',
value: 'key',
expandTrigger: 'hover'
}}
placeholder="请选择绑定的请求数据"
onChange={handleBindChange}
></ElCascader>
</ElFormItem>
<ElFormItem label="响应数据" prop={'data.recv'}>
<ElCascader
clearable={true}
props={{
checkStrictly: true,
children: 'entitys',
label: 'name',
value: 'key',
expandTrigger: 'hover'
}}
placeholder="请选择绑定的响应数据"
onChange={handleBindChange}
v-model={state.ruleForm.data.recv}
options={models.value}
></ElCascader>
</ElFormItem>
</ElForm>
),
onConfirm: () => {
return new Promise((resolve, reject) => {
ruleFormRef.value?.validate((valid) => {
if (valid) {
if (isEdit.value) {
updateFetchApi(cloneDeep(state.ruleForm))
/**
* @description 显示添加接口弹窗
*/
const showModelMoal = () => {
const operateType = isEdit.value ? '编辑' : '新增';
useModal({
title: `${operateType}接口`,
props: {
width: 600,
},
content: () => (
<ElForm model={state.ruleForm} ref={ruleFormRef} label-width="100px" rules={rules}>
<ElFormItem label="名称" prop="name">
<ElInput v-model={state.ruleForm.name} placeholder={'请输入接口名称'}></ElInput>
</ElFormItem>
<ElFormItem label="接口" prop={'options.url'}>
<ElInput v-model={state.ruleForm.options.url} placeholder={'请输入接口地址'}>
{{
prepend: () => (
<ElSelect v-model={state.ruleForm.options.method} class={'w-90px'}>
{Object.keys(RequestEnum).map((key) => (
<ElOption key={key} label={key} value={key}></ElOption>
))}
</ElSelect>
),
}}
</ElInput>
</ElFormItem>
<ElFormItem label="内容类型" prop={'options.contentType'}>
<ElSelect v-model={state.ruleForm.options.contentType}>
{Object.keys(ContentTypeEnum).map((key) => (
<ElOption key={key} label={key} value={key}></ElOption>
))}
</ElSelect>
</ElFormItem>
<ElFormItem label="请求数据" prop={'data.bind'}>
<ElCascader
v-model={state.ruleForm.data.bind}
options={models.value}
clearable={true}
props={{
checkStrictly: true,
children: 'entitys',
label: 'name',
value: 'key',
expandTrigger: 'hover',
}}
placeholder="请选择绑定的请求数据"
onChange={handleBindChange}
></ElCascader>
</ElFormItem>
<ElFormItem label="响应数据" prop={'data.recv'}>
<ElCascader
clearable={true}
props={{
checkStrictly: true,
children: 'entitys',
label: 'name',
value: 'key',
expandTrigger: 'hover',
}}
placeholder="请选择绑定的响应数据"
onChange={handleBindChange}
v-model={state.ruleForm.data.recv}
options={models.value}
></ElCascader>
</ElFormItem>
</ElForm>
),
onConfirm: () => {
return new Promise((resolve, reject) => {
ruleFormRef.value?.validate((valid) => {
if (valid) {
if (isEdit.value) {
updateFetchApi(cloneDeep(state.ruleForm));
} else {
incrementFetchApi(cloneDeep(state.ruleForm));
}
ElMessage.success(`${operateType}接口成功!`);
state.ruleForm = createEmptyApiItem();
resolve('submit!');
} else {
incrementFetchApi(cloneDeep(state.ruleForm))
reject();
console.log('error submit!!');
return false;
}
ElMessage.success(`${operateType}接口成功!`)
state.ruleForm = createEmptyApiItem()
resolve('submit!')
} else {
reject()
console.log('error submit!!')
return false
}
})
})
},
onCancel: () => (state.ruleForm = createEmptyApiItem())
})
}
});
});
},
onCancel: () => (state.ruleForm = createEmptyApiItem()),
});
};
/**
* @description 编辑模型
*/
const editApiItem = (apiItem: FetchApiItem) => {
console.log(apiItem)
state.ruleForm = cloneDeep(apiItem)
showModelMoal()
}
/**
* @description 编辑模型
*/
const editApiItem = (apiItem: FetchApiItem) => {
console.log(apiItem);
state.ruleForm = cloneDeep(apiItem);
showModelMoal();
};
</script>
<style lang="scss" scoped>
.low-model-item {
overflow: auto;
.low-model-item {
overflow: auto;
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
}
}
}
.model-item-title {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
.model-actions {
i {
padding: 6px;
margin: 0 2px;
font-weight: bold;
cursor: pointer;
border-radius: 2px;
opacity: 0.7;
transition: all 0.1s;
.model-item-title {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background-color: #f1f1f1;
opacity: 1;
}
&.el-icon-delete {
color: #f44336;
}
.model-actions {
display: flex;
i {
padding: 6px;
margin: 0 2px;
font-weight: bold;
cursor: pointer;
border-radius: 2px;
opacity: 0.7;
transition: all 0.1s;
&.el-icon-edit {
color: #2196f3;
&:hover {
background-color: #f1f1f1;
opacity: 1;
}
}
}
}
}
</style>

@ -8,13 +8,11 @@
-->
<template>
<div class="!mb-10px">
<el-button type="primary" size="small" @click="showModelMoal">添加</el-button>
<el-button type="warning" size="small" @click="showImportSwaggerJsonModal"
>导入swagger</el-button
>
<el-button type="primary" @click="showModelMoal">添加</el-button>
<el-button type="warning" @click="showImportSwaggerJsonModal">导入swagger</el-button>
<el-popconfirm title="确定要删除全部模型吗?" @confirm="updateModel([], true)">
<template #reference>
<el-button type="danger" size="small">清空</el-button>
<el-button type="danger">清空</el-button>
</template>
</el-popconfirm>
</div>
@ -25,10 +23,16 @@
<div class="model-item-title">
<span class="truncate w-160px">{{ item.name }}</span>
<div class="model-actions">
<i class="el-icon-edit" @click="editModel(item)"></i>
<el-popconfirm title="确定要删除该模型吗?" @confirm="deleteModel(item.key)">
<el-icon size="24" color="#2196f3" @click.stop="editModel(item)">
<Edit />
</el-icon>
<el-popconfirm
title="确定要删除该模型吗?"
icon-color="red"
@confirm="deleteModel(item.key)"
>
<template #reference>
<i class="el-icon-delete"></i>
<el-icon size="24" color="#f44336"><Delete /></el-icon>
</template>
</el-popconfirm>
</div>
@ -45,238 +49,232 @@
</template>
<script setup lang="tsx">
import { reactive, ref, computed } from 'vue'
import {
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElCard,
ElButton,
ElMessage
} from 'element-plus'
import { useVisualData, fieldTypes } from '@/visual-editor/hooks/useVisualData'
import type { VisualEditorModel } from '@/visual-editor/visual-editor.utils'
import { useModal } from '@/visual-editor/hooks/useModal'
import { cloneDeep } from 'lodash'
import { generateNanoid } from '@/visual-editor/utils/'
import { useImportSwaggerJsonModal } from './utils'
import { reactive, ref, computed } from 'vue';
import {
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElCard,
ElButton,
ElMessage,
} from 'element-plus';
import { useVisualData, fieldTypes } from '@/visual-editor/hooks/useVisualData';
import type { VisualEditorModel } from '@/visual-editor/visual-editor.utils';
import { useModal } from '@/visual-editor/hooks/useModal';
import { cloneDeep } from 'lodash';
import { generateNanoid } from '@/visual-editor/utils/';
import { useImportSwaggerJsonModal } from './utils';
import { Delete, Edit } from '@element-plus/icons-vue';
interface IState {
activeNames: string[]
ruleForm: VisualEditorModel
}
interface IState {
activeNames: string[];
ruleForm: VisualEditorModel;
}
const { jsonData, incrementModel, updateModel, deleteModel } = useVisualData()
const { jsonData, incrementModel, updateModel, deleteModel } = useVisualData();
const { showImportSwaggerJsonModal } = useImportSwaggerJsonModal()
/**
* @description 模型集合
*/
const models = computed(() => cloneDeep(jsonData.models))
const { showImportSwaggerJsonModal } = useImportSwaggerJsonModal();
/**
* @description 模型集合
*/
const models = computed(() => cloneDeep(jsonData.models));
/**
* @description 是否处于编辑状态
*/
const isEdit = computed(() => models.value.some((item) => item.key === state.ruleForm.key))
/**
* @description 是否处于编辑状态
*/
const isEdit = computed(() => models.value.some((item) => item.key === state.ruleForm.key));
/**
* @description 创建空的实体对象
*/
const createEmptyEntity = () => ({ key: '', name: '', type: 'string', value: '' })
/**
* @description 创建空的实体对象
*/
const createEmptyEntity = () => ({ key: '', name: '', type: 'string', value: '' });
/**
* @description 创建空的数据模型
*/
const createEmptyModel = () => ({
name: '',
key: generateNanoid(),
entitys: [createEmptyEntity()]
})
/**
* @description 创建空的数据模型
*/
const createEmptyModel = () => ({
name: '',
key: generateNanoid(),
entitys: [createEmptyEntity()],
});
const ruleFormRef = ref<InstanceType<typeof ElForm>>()
const ruleFormRef = ref<InstanceType<typeof ElForm>>();
const state = reactive<IState>({
activeNames: [],
ruleForm: createEmptyModel()
})
const state = reactive<IState>({
activeNames: [],
ruleForm: createEmptyModel(),
});
/**
* @param {number} 索引
* @description 删除实体项
*/
const deleteEntityItem = (index: number) => {
state.ruleForm.entitys.splice(index, 1)
}
/**
* @param {number} 索引
* @description 删除实体项
*/
const deleteEntityItem = (index: number) => {
state.ruleForm.entitys.splice(index, 1);
};
/**
* @description 添加实体项
*/
const addEntityItem = () => {
state.ruleForm.entitys.push(createEmptyEntity())
}
/**
* @description 添加实体项
*/
const addEntityItem = () => {
state.ruleForm.entitys.push(createEmptyEntity());
};
/**
* @description 显示添加接口弹窗
*/
const showModelMoal = () => {
const operateType = isEdit.value ? '修改' : '新增'
useModal({
title: `${operateType}数据源`,
props: {
width: 600
},
content: () => (
<ElForm model={state.ruleForm} ref={ruleFormRef} label-width="100px" size={'mini'}>
<ElFormItem
label="数据源名称"
prop="name"
rules={[{ required: true, message: '请输入数据源名称', trigger: 'change' }]}
>
<ElInput v-model={state.ruleForm.name} placeholder={'请输入数据源名称'}></ElInput>
</ElFormItem>
{!state.ruleForm.entitys.length && (
<ElFormItem>
<ElButton onClick={addEntityItem} type={'primary'} size={'mini'}>
添加实体
</ElButton>
</ElFormItem>
)}
{state.ruleForm.entitys.map((entity, index) => (
<ElCard
key={index}
shadow={'hover'}
class={'mt-10px'}
v-slots={{
header: () => (
<div class={'flex justify-between'}>
<ElFormItem
label="实体名称"
prop={`entitys.${index}.name`}
rules={[{ required: true, message: '请输入实体名称', trigger: 'change' }]}
showMessage={false}
class={'w-300px !mb-0'}
>
<ElInput v-model={entity.name} placeholder={'请输入实体名称'}></ElInput>
</ElFormItem>
<div>
<ElButton onClick={() => deleteEntityItem(index)} type={'danger'} size={'mini'}>
删除
</ElButton>
<ElButton onClick={addEntityItem} type={'primary'} size={'mini'}>
添加
</ElButton>
</div>
</div>
)
}}
/**
* @description 显示添加接口弹窗
*/
const showModelMoal = () => {
const operateType = isEdit.value ? '修改' : '新增';
useModal({
title: `${operateType}数据源`,
props: {
width: 600,
},
content: () => (
<ElForm model={state.ruleForm} ref={ruleFormRef} label-width="100px">
<ElFormItem
label="数据源名称"
prop="name"
rules={[{ required: true, message: '请输入数据源名称', trigger: 'change' }]}
>
<ElFormItem
label="实体字段"
prop={`entitys.${index}.key`}
rules={[{ required: true, message: '请输入实体字段', trigger: 'change' }]}
>
<ElInput v-model={entity.key} placeholder={'请输入实体字段'}></ElInput>
<ElInput v-model={state.ruleForm.name} placeholder={'请输入数据源名称'}></ElInput>
</ElFormItem>
{!state.ruleForm.entitys.length && (
<ElFormItem>
<ElButton onClick={addEntityItem} type={'primary'}>
添加实体
</ElButton>
</ElFormItem>
<ElFormItem
label="数据类型"
prop={`entitys.${index}.type`}
rules={[{ required: true, message: '请输入数据类型', trigger: 'change' }]}
)}
{state.ruleForm.entitys.map((entity, index) => (
<ElCard
key={index}
shadow={'hover'}
class={'mt-10px'}
v-slots={{
header: () => (
<div class={'flex justify-between'}>
<ElFormItem
label="实体名称"
prop={`entitys.${index}.name`}
rules={[{ required: true, message: '请输入实体名称', trigger: 'change' }]}
showMessage={false}
class={'w-300px !mb-0'}
>
<ElInput v-model={entity.name} placeholder={'请输入实体名称'}></ElInput>
</ElFormItem>
<div>
<ElButton onClick={() => deleteEntityItem(index)} type={'danger'}>
删除
</ElButton>
<ElButton onClick={addEntityItem} type={'primary'}>
添加
</ElButton>
</div>
</div>
),
}}
>
<ElSelect v-model={entity.type}>
{fieldTypes.map((typeItem) => (
<ElOption
key={typeItem.value}
label={typeItem.label}
value={typeItem.value}
></ElOption>
))}
</ElSelect>
</ElFormItem>
<ElFormItem label="默认数据" prop={`entitys.${index}.value`}>
<ElInput
v-model={entity.value}
placeholder={'实体默认数据,不填则为对应类型数据'}
></ElInput>
</ElFormItem>
</ElCard>
))}
</ElForm>
),
onConfirm: () => {
return new Promise((resolve, reject) => {
ruleFormRef.value?.validate((valid) => {
if (valid) {
if (isEdit.value) {
updateModel(state.ruleForm)
<ElFormItem
label="实体字段"
prop={`entitys.${index}.key`}
rules={[{ required: true, message: '请输入实体字段', trigger: 'change' }]}
>
<ElInput v-model={entity.key} placeholder={'请输入实体字段'}></ElInput>
</ElFormItem>
<ElFormItem
label="数据类型"
prop={`entitys.${index}.type`}
rules={[{ required: true, message: '请输入数据类型', trigger: 'change' }]}
>
<ElSelect v-model={entity.type}>
{fieldTypes.map((typeItem) => (
<ElOption
key={typeItem.value}
label={typeItem.label}
value={typeItem.value}
></ElOption>
))}
</ElSelect>
</ElFormItem>
<ElFormItem label="默认数据" prop={`entitys.${index}.value`}>
<ElInput
v-model={entity.value}
placeholder={'实体默认数据,不填则为对应类型数据'}
></ElInput>
</ElFormItem>
</ElCard>
))}
</ElForm>
),
onConfirm: () => {
return new Promise((resolve, reject) => {
ruleFormRef.value?.validate((valid) => {
if (valid) {
if (isEdit.value) {
updateModel(state.ruleForm);
} else {
incrementModel(state.ruleForm);
}
ElMessage.success(`${operateType}模型成功!`);
state.ruleForm = createEmptyModel();
resolve('submit!');
} else {
incrementModel(state.ruleForm)
reject();
console.log('error submit!!');
return false;
}
ElMessage.success(`${operateType}模型成功!`)
state.ruleForm = createEmptyModel()
resolve('submit!')
} else {
reject()
console.log('error submit!!')
return false
}
})
})
},
onCancel: () => (state.ruleForm = createEmptyModel())
})
}
/**
* @description 编辑模型
*/
const editModel = (model: VisualEditorModel) => {
console.log(model)
state.ruleForm = cloneDeep(model)
showModelMoal()
}
});
});
},
onCancel: () => (state.ruleForm = createEmptyModel()),
});
};
/**
* @description 编辑模型
*/
const editModel = (model: VisualEditorModel) => {
console.log(model);
state.ruleForm = cloneDeep(model);
showModelMoal();
};
</script>
<style lang="scss" scoped>
.low-model-item {
overflow: auto;
.low-model-item {
overflow: auto;
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
}
}
}
.model-item-title {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
.model-actions {
i {
padding: 6px;
margin: 0 2px;
font-weight: bold;
cursor: pointer;
border-radius: 2px;
opacity: 0.7;
transition: all 0.1s;
.model-item-title {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background-color: #f1f1f1;
opacity: 1;
}
&.el-icon-delete {
color: #f44336;
}
.model-actions {
display: flex;
i {
padding: 6px;
margin: 0 2px;
font-weight: bold;
cursor: pointer;
border-radius: 2px;
opacity: 0.7;
transition: all 0.1s;
&.el-icon-edit {
color: #2196f3;
&:hover {
background-color: #f1f1f1;
opacity: 1;
}
}
}
}
}
</style>

@ -18,28 +18,30 @@
</template>
<script lang="tsx">
export default {
label: '数据源',
order: 2,
icon: 'el-icon-data-board'
}
import { DataBoard } from '@element-plus/icons-vue';
export default {
label: '数据源',
order: 2,
icon: DataBoard,
};
</script>
<script setup lang="tsx" name="基本组件">
import DataModel from './data-model.vue'
import DataFetch from './data-fetch.vue'
import DataModel from './data-model.vue';
import DataFetch from './data-fetch.vue';
</script>
<style lang="scss" scoped>
.data-source :deep {
.el-tabs__header {
position: sticky;
top: 0;
z-index: 10;
}
.data-source :deep {
.el-tabs__header {
position: sticky;
top: 0;
z-index: 10;
}
.el-tabs__content {
contain: layout;
content-visibility: auto;
.el-tabs__content {
contain: layout;
content-visibility: auto;
}
}
}
</style>

@ -6,87 +6,87 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\utils.tsx
*/
import { generateNanoid } from '@/visual-editor/utils'
import type { FetchApiItem } from '@/visual-editor/visual-editor.utils'
import { RequestEnum } from '@/enums/httpEnum'
import MonacoEditor from '@/visual-editor/components/common/monaco-editor/MonacoEditor'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import type { VisualEditorModel } from '@/visual-editor/visual-editor.utils'
import { useModal } from '@/visual-editor/hooks/useModal'
import { ElMessage } from 'element-plus'
import { generateNanoid } from '@/visual-editor/utils';
import type { FetchApiItem } from '@/visual-editor/visual-editor.utils';
import { RequestEnum } from '@/enums/httpEnum';
import MonacoEditor from '@/visual-editor/components/common/monaco-editor/MonacoEditor';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import type { VisualEditorModel } from '@/visual-editor/visual-editor.utils';
import { useModal } from '@/visual-editor/hooks/useModal';
import { ElMessage } from 'element-plus';
/**
* @description
* @description , eg: 简单的解析代码
* @param {object} swagger JSON字符串
* @returns { apis, models }
*/
export const importSwaggerJson = (swagger: any) => {
swagger = typeof swagger == 'string' ? JSON.parse(swagger) : swagger
const models: VisualEditorModel[] = []
swagger = typeof swagger == 'string' ? JSON.parse(swagger) : swagger;
const models: VisualEditorModel[] = [];
Object.keys(swagger.definitions).forEach((model) => {
const properties = swagger.definitions[model].properties
const properties = swagger.definitions[model].properties;
const modelItem: VisualEditorModel = {
name: model,
key: generateNanoid(),
entitys: []
}
entitys: [],
};
Object.keys(properties).forEach((field) => {
modelItem.entitys.push({
key: field,
name: properties[field].description || field,
type: properties[field].type,
value: ''
})
})
models.push(modelItem)
})
const apis: FetchApiItem[] = []
value: '',
});
});
models.push(modelItem);
});
const apis: FetchApiItem[] = [];
Object.keys(swagger.paths).forEach((url) => {
Object.keys(swagger.paths[url]).forEach((method) => {
const apiUrlObj = swagger.paths[url][method]
const model = apiUrlObj.parameters?.[0]?.schema?.$ref?.split('/').pop()
const bindTarget = model ? models.find((item) => item.name == model) : undefined
typeof bindTarget == 'object' && (bindTarget.name = apiUrlObj.summary)
const apiUrlObj = swagger.paths[url][method];
const model = apiUrlObj.parameters?.[0]?.schema?.$ref?.split('/').pop();
const bindTarget = model ? models.find((item) => item.name == model) : undefined;
typeof bindTarget == 'object' && (bindTarget.name = apiUrlObj.summary);
const api: FetchApiItem = {
key: generateNanoid(),
name: apiUrlObj.summary,
options: {
url: url, // 请求的url
method: method.toLocaleUpperCase() as RequestEnum, // 请求的方法
contentType: apiUrlObj.produces[0] || apiUrlObj.consumes[0] // 请求的内容类型
contentType: apiUrlObj.produces[0] || apiUrlObj.consumes[0], // 请求的内容类型
},
data: {
bind: bindTarget?.key || '', // 请求绑定对应的某个实体
recv: '' // 响应的结果绑定到某个实体上
}
}
apis.push(api)
})
})
return { apis, models }
}
recv: '', // 响应的结果绑定到某个实体上
},
};
apis.push(api);
});
});
return { apis, models };
};
/**
* @description swagger JSON模态框
*/
export const useImportSwaggerJsonModal = () => {
const { updateModel, updateFetchApi } = useVisualData()
const { updateModel, updateFetchApi } = useVisualData();
const shema = {}
const shema = {};
const handleSchemaChange = (val) => {
try {
const newObj = JSON.parse(val)
Object.assign(shema, newObj)
const newObj = JSON.parse(val);
Object.assign(shema, newObj);
} catch (e) {
console.log('JSON格式有误:', e)
console.log('JSON格式有误:', e);
}
}
};
return {
showImportSwaggerJsonModal: () =>
useModal({
title: '导入swagger JSON (支持swagger: 2.0)',
props: {
width: 760
width: 760,
},
content: () => (
<MonacoEditor
@ -99,15 +99,15 @@ export const useImportSwaggerJsonModal = () => {
),
onConfirm: () => {
try {
const { models, apis } = importSwaggerJson(shema)
updateModel(models, true)
updateFetchApi(apis, true)
ElMessage.success('导入成功!')
console.log({ models, apis }, '导入的swagger')
const { models, apis } = importSwaggerJson(shema);
updateModel(models, true);
updateFetchApi(apis, true);
ElMessage.success('导入成功!');
console.log({ models, apis }, '导入的swagger');
} catch (e) {
ElMessage.success('导入失败!请检查swagger JSON是否有误!')
ElMessage.success('导入失败!请检查swagger JSON是否有误!');
}
}
})
}
}
},
}),
};
};

@ -1,11 +1,6 @@
<!--页面树-->
<template>
<el-button
type="primary"
size="small"
class="!my-10px !mx-6px"
icon="el-icon-plus"
@click="addPage"
<el-button type="primary" class="!my-10px !mx-6px" :icon="Plus" @click="addPage"
>添加页面</el-button
>
<el-tree
@ -21,23 +16,19 @@
<span
>{{ node.label }}{{ data.path }}
<template v-if="data.isDefault">
<el-tag size="mini">默认</el-tag>
<el-tag size="default">默认</el-tag>
</template>
</span>
<span @click.stop>
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<i class="el-icon-more"></i>
<el-icon><more-filled /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="el-icon-edit" @click="editPage(data)"
>编辑</el-dropdown-item
>
<el-dropdown-item icon="el-icon-delete" @click="delPage(data)"
>删除</el-dropdown-item
>
<el-dropdown-item icon="el-icon-link" @click="setDefaultPage(data)"
<el-dropdown-item :icon="Edit" @click="editPage(data)">编辑</el-dropdown-item>
<el-dropdown-item :icon="Delete" @click="delPage(data)">删除</el-dropdown-item>
<el-dropdown-item :icon="Link" @click="setDefaultPage(data)"
>设为首页</el-dropdown-item
>
</el-dropdown-menu>
@ -50,157 +41,148 @@
</template>
<script lang="tsx">
import { defineComponent, reactive, ref, computed, toRefs } from 'vue'
import { useVisualData, createNewPage } from '@/visual-editor/hooks/useVisualData'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElForm, ElFormItem, ElInput } from 'element-plus'
import { useModal } from '@/visual-editor/hooks/useModal'
export default {
name: 'PageTree',
label: '页面',
order: 1,
icon: Tickets,
};
</script>
const rules = {
title: [{ required: true, message: '请输入页面标题', trigger: 'blur' }],
path: [{ required: true, message: '请输入页面路径', trigger: 'blur' }]
}
<script lang="tsx" setup>
import { ref, computed } from 'vue';
import { useVisualData, createNewPage } from '@/visual-editor/hooks/useVisualData';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElForm, ElFormItem, ElInput } from 'element-plus';
import { useModal } from '@/visual-editor/hooks/useModal';
import type { VisualEditorPage } from '@/visual-editor/visual-editor.utils';
import { Tickets, Plus, MoreFilled, Edit, Delete, Link } from '@element-plus/icons-vue';
export default defineComponent({
name: 'PageTree',
label: '页面',
order: 1,
icon: 'el-icon-tickets',
setup() {
const router = useRouter()
const route = useRoute()
const rules = {
title: [{ required: true, message: '请输入页面标题', trigger: 'blur' }],
path: [{ required: true, message: '请输入页面路径', trigger: 'blur' }],
};
const { jsonData, setCurrentPage, deletePage, updatePage, incrementPage } = useVisualData()
const router = useRouter();
const route = useRoute();
const ruleFormRef = ref<InstanceType<typeof ElForm>>()
const { jsonData, setCurrentPage, deletePage, updatePage, incrementPage } = useVisualData();
const state = reactive({
defaultProps: {
children: 'children',
label: 'title'
},
currentNodeKey: route.path,
operatePageData: null as any, //
form: {
//
title: '',
path: ''
}
})
const ruleFormRef = ref<InstanceType<typeof ElForm>>();
const defaultProps = ref({
children: 'children',
label: 'title',
});
const currentNodeKey = ref(route.path);
//
const operatePageData = ref<VisualEditorPage | null>(null);
//
const form = ref({
title: '',
path: '',
});
//
const pages = computed(() =>
Object.keys(jsonData.pages).map((key) => ({
title: jsonData.pages[key].title,
path: key
}))
)
//
const pages = computed(() =>
Object.keys(jsonData.pages).map((key) => ({
title: jsonData.pages[key].title,
path: key,
})),
);
//
const handleNodeClick = (data) => {
setCurrentPage(data.path)
router.push(data.path)
}
//
const handleNodeClick = (data) => {
setCurrentPage(data.path);
router.push(data.path);
};
/**
* @description 显示新增/编辑模态框
*/
const showOparateModal = () =>
useModal({
title: state.operatePageData ? '编辑页面' : '新增页面',
props: {
width: 380
},
content: () => (
<ElForm ref={ruleFormRef} model={state.form} rules={rules}>
<ElFormItem prop={'title'} label={'页面标题'} labelWidth={'80px'}>
<ElInput v-model={state.form.title} />
</ElFormItem>
<ElFormItem prop={'path'} label={'页面路径'} labelWidth={'80px'}>
<ElInput v-model={state.form.path} />
</ElFormItem>
</ElForm>
),
onConfirm: () => {
return new Promise((resolve, reject) => {
ruleFormRef.value?.validate(async (valid) => {
if (valid) {
const { title, path } = state.form
if ([title.trim(), path.trim()].includes('')) {
return ElMessage.error('标题或路径不能为空!')
}
if (state.operatePageData) {
updatePage({
newPath: path,
oldPath: state.operatePageData.path || path,
page: { title }
})
await router.replace(path)
state.currentNodeKey = path
} else {
incrementPage(path, createNewPage({ title }))
}
resolve(true)
/**
* @description 显示新增/编辑模态框
*/
const showOparateModal = () =>
useModal({
title: operatePageData.value ? '编辑页面' : '新增页面',
props: {
width: 380,
},
content: () => (
<ElForm ref={ruleFormRef} model={form.value} rules={rules}>
<ElFormItem prop={'title'} label={'页面标题'} labelWidth={'80px'}>
<ElInput v-model={form.value.title} />
</ElFormItem>
<ElFormItem prop={'path'} label={'页面路径'} labelWidth={'80px'}>
<ElInput v-model={form.value.path} />
</ElFormItem>
</ElForm>
),
onConfirm: () => {
return new Promise((resolve, reject) => {
ruleFormRef.value?.validate(async (valid) => {
if (valid) {
const { title, path } = form.value;
if ([title.trim(), path.trim()].includes('')) {
return ElMessage.error('标题或路径不能为空!');
}
if (operatePageData.value) {
updatePage({
newPath: path,
oldPath: operatePageData.value.path || path,
page: { title },
});
await router.replace(path);
currentNodeKey.value = path;
} else {
console.log('error submit!!')
reject()
return false
incrementPage(path, createNewPage({ title }));
}
})
})
}
})
//
const addPage = () => {
state.operatePageData = null
state.form = {
title: '',
path: ''
}
showOparateModal()
}
//
const editPage = (data) => {
state.operatePageData = data
state.form = {
title: data.title,
path: data.path
}
showOparateModal()
console.log('子页面数据:', data)
}
//
const delPage = (data) => {
console.log('删除子页面数据', data)
deletePage(data.path, '/')
}
//
const setDefaultPage = (data) => {
console.log('设置该页面为默认页面', data)
}
resolve(true);
} else {
console.log('error submit!!');
reject();
return false;
}
});
});
},
});
return {
...toRefs(state),
pages,
setCurrentPage,
setDefaultPage,
handleNodeClick,
addPage,
editPage,
delPage
}
}
})
//
const addPage = () => {
operatePageData.value = null;
form.value = {
title: '',
path: '',
};
showOparateModal();
};
//
const editPage = (data) => {
operatePageData.value = data;
form.value = {
title: data.title,
path: data.path,
};
showOparateModal();
console.log('子页面数据:', data);
};
//
const delPage = (data) => {
console.log('删除子页面数据', data);
deletePage(data.path, '/');
};
//
const setDefaultPage = (data) => {
console.log('设置该页面为默认页面', data);
};
</script>
<style lang="scss" scoped>
.custom-tree-node {
display: flex;
padding-right: 8px;
font-size: 14px;
flex: 1;
align-items: center;
justify-content: space-between;
}
.custom-tree-node {
display: flex;
padding-right: 8px;
font-size: 14px;
flex: 1;
align-items: center;
justify-content: space-between;
}
</style>

@ -12,71 +12,64 @@
<el-tab-pane :name="tabItem.name" lazy>
<template #label>
<div class="tab-item">
<i :class="tabItem.icon"></i>
<el-icon :size="26"><component :is="tabItem.icon" /></el-icon>
{{ tabItem.label }}
</div>
</template>
<component :is="tabItem.name" v-bind="$attrs" />
<component :is="tabItem.comp" v-bind="$attrs" />
</el-tab-pane>
</template>
</el-tabs>
</template>
<script lang="ts">
/**
* @description 左侧边栏
*/
import { defineComponent, reactive, toRefs } from 'vue'
import components from './components'
export default {
name: 'LeftAside',
};
</script>
const tabs = Object.keys(components)
.map((name) => {
const { label, icon, order } = components[name]
return { label, icon, name, order }
})
.sort((a, b) => a.order - b.order)
<script lang="ts" setup>
/**
* @description 左侧边栏
*/
import { ref } from 'vue';
import components from './components';
export default defineComponent({
name: 'LeftAside',
components,
setup() {
const state = reactive({
activeName: tabs[0].name
const tabs = Object.keys(components)
.map((name) => {
const { label, icon, order } = components[name];
return { label, icon, name, order, comp: components[name] };
})
.sort((a, b) => a.order - b.order);
return {
...toRefs(state),
tabs
}
}
})
const activeName = ref(tabs[0].name);
</script>
<style lang="scss" scoped>
.left-aside {
height: 100%;
contain: layout;
.left-aside {
height: 100%;
contain: layout;
> :deep(.el-tabs__header) {
margin-right: 0;
> :deep(.el-tabs__header) {
margin-right: 0;
.el-tabs__item {
height: 80px;
padding: 20px 16px;
.el-tabs__item {
height: 80px;
padding: 20px 16px;
.tab-item {
@apply flex flex-col items-center justify-center;
.tab-item {
@apply flex flex-col items-center justify-center;
[class^='el-icon-'] {
font-size: 20px;
[class^='el-icon-'] {
font-size: 20px;
}
}
}
}
}
> :deep(.el-tabs__content) {
height: 100%;
overflow-y: auto;
> :deep(.el-tabs__content) {
height: 100%;
overflow-y: auto;
}
}
}
</style>

@ -6,38 +6,40 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\animate\Animate.tsx
*/
import { defineComponent, reactive, ref, watchEffect } from 'vue'
import { ElTabs, ElTabPane, ElRow, ElCol, ElButton, ElSwitch, ElAlert } from 'element-plus'
import { animationTabs } from './animateConfig'
import styles from './animate.module.scss'
import { onClickOutside } from '@vueuse/core'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import type { Animation } from '@/visual-editor/visual-editor.utils'
import { useAnimate } from '@/hooks/useAnimate'
import { defineComponent, reactive, ref, watchEffect } from 'vue';
import { ElTabs, ElTabPane, ElRow, ElCol, ElButton, ElSwitch, ElAlert, ElIcon } from 'element-plus';
import { animationTabs } from './animateConfig';
import styles from './animate.module.scss';
import { onClickOutside } from '@vueuse/core';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import type { Animation } from '@/visual-editor/visual-editor.utils';
import { useAnimate } from '@/hooks/useAnimate';
import { Plus, CaretRight } from '@element-plus/icons-vue';
import 'element-plus/es/components/alert/style/css';
export const Animate = defineComponent({
setup() {
const { currentBlock } = useVisualData()
const target = ref<InstanceType<typeof HTMLDivElement>>()
const { currentBlock } = useVisualData();
const target = ref<InstanceType<typeof HTMLDivElement>>();
const state = reactive({
activeName: '',
isAddAnimates: false, // 是否显示添加动画集
changeTargetIndex: -1 // 要修改的动画的索引
})
changeTargetIndex: -1, // 要修改的动画的索引
});
onClickOutside(target, () => (state.isAddAnimates = false))
onClickOutside(target, () => (state.isAddAnimates = false));
watchEffect((onInvalidate) => {
if (state.isAddAnimates) {
state.activeName = 'in'
state.activeName = 'in';
} else {
state.changeTargetIndex = -1
state.changeTargetIndex = -1;
}
onInvalidate(() => {
console.log('onInvalidate')
})
})
console.log('onInvalidate');
});
});
/**
* @description
@ -45,47 +47,47 @@ export const Animate = defineComponent({
const runAnimation = (animation: Animation | Animation[] = []) => {
let animateEl =
(window.$$refs[currentBlock.value._vid]?.$el as HTMLElement) ??
(window.$$refs[currentBlock.value._vid] as HTMLElement)
(window.$$refs[currentBlock.value._vid] as HTMLElement);
animateEl = animateEl?.closest('.list-group-item')?.firstChild as HTMLElement
animateEl = animateEl?.closest('.list-group-item')?.firstChild as HTMLElement;
if (animateEl) {
useAnimate(animateEl, animation)
useAnimate(animateEl, animation);
}
}
};
/**
* @description
*/
const clickAnimateName = (index) => {
state.changeTargetIndex = index
state.isAddAnimates = true
}
state.changeTargetIndex = index;
state.isAddAnimates = true;
};
/**
* @description
* @param index
* @returns
*/
const delAnimate = (index: number) => currentBlock.value.animations?.splice(index, 1)
const delAnimate = (index: number) => currentBlock.value.animations?.splice(index, 1);
/**
* @description /
*/
const addOrChangeAnimate = (animateItem: Animation) => {
const animation: Animation = {
...animateItem
}
...animateItem,
};
if (state.changeTargetIndex == -1) {
currentBlock.value.animations?.push(animation)
currentBlock.value.animations?.push(animation);
} else {
// 修改动画
currentBlock.value.animations![state.changeTargetIndex] = animation
state.changeTargetIndex = -1
currentBlock.value.animations![state.changeTargetIndex] = animation;
state.changeTargetIndex = -1;
}
state.isAddAnimates = false
console.log(currentBlock.value.animations, '当前组件的动画')
}
state.isAddAnimates = false;
console.log(currentBlock.value.animations, '当前组件的动画');
};
// 已添加的动画列表组件
const AddedAnimateList = () => (
@ -104,11 +106,12 @@ export const Animate = defineComponent({
<span onClick={() => clickAnimateName(index)} class={'label'}>
{item.label}
</span>
<span
onClick={() => runAnimation(item)}
class={'el-icon-caret-right play'}
title={'播放'}
></span>
<span onClick={() => runAnimation(item)} class={'play'} title={'播放'}>
<ElIcon size={20}>
<CaretRight></CaretRight>
</ElIcon>
</span>
</div>
),
default: () => (
@ -129,12 +132,12 @@ export const Animate = defineComponent({
</ElRow>
<ElSwitch v-model={item.infinite}></ElSwitch>
</>
)
),
}}
</ElAlert>
))}
</>
)
);
// 可添加的动画列表组件
const AnimateList = () => (
@ -157,7 +160,7 @@ export const Animate = defineComponent({
</ElTabPane>
))}
</ElTabs>
)
);
return () => (
<div ref={target} class={styles.animate}>
@ -166,7 +169,7 @@ export const Animate = defineComponent({
type={'primary'}
disabled={!currentBlock.value.animations}
plain
icon={'el-icon-plus'}
icon={Plus}
onClick={() => (state.isAddAnimates = true)}
>
@ -175,7 +178,7 @@ export const Animate = defineComponent({
type={'primary'}
disabled={!currentBlock.value.animations?.length}
plain
icon={'el-icon-caret-right'}
icon={CaretRight}
onClick={() => runAnimation(currentBlock.value.animations)}
>
@ -184,6 +187,6 @@ export const Animate = defineComponent({
</div>
<AnimateList v-show={state.isAddAnimates} />
</div>
)
}
})
);
},
});

@ -42,6 +42,7 @@
.play {
font-size: 16px;
cursor: pointer;
vertical-align: middle;
}
span {

@ -7,8 +7,8 @@
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\components\cross-sortable-options-editor\cross-sortable-options-editor.tsx
*/
import { defineComponent, reactive, computed, PropType } from 'vue'
import Draggable from 'vuedraggable'
import { defineComponent, reactive, computed, PropType } from 'vue';
import Draggable from 'vuedraggable';
import {
ElInput,
ElCheckboxGroup,
@ -17,80 +17,82 @@ import {
ElCollapseItem,
ElTabs,
ElTabPane,
ElForm
} from 'element-plus'
import { useVModel } from '@vueuse/core'
import { isObject } from '@/visual-editor/utils/is'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { PropConfig } from '../prop-config'
import { VisualEditorBlockData, VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { cloneDeep } from 'lodash'
ElForm,
ElIcon,
} from 'element-plus';
import { useVModel } from '@vueuse/core';
import { isObject } from '@/visual-editor/utils/is';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import { PropConfig } from '../prop-config';
import { VisualEditorBlockData, VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { cloneDeep } from 'lodash';
import { Rank, CirclePlus, Remove } from '@element-plus/icons-vue';
interface OptionItem extends LabelValue {
component?: VisualEditorComponent
block?: VisualEditorBlockData
component?: VisualEditorComponent;
block?: VisualEditorBlockData;
}
export const CrossSortableOptionsEditor = defineComponent({
props: {
modelValue: {
type: Array as PropType<(string | OptionItem)[]>,
default: () => []
default: () => [],
},
multiple: Boolean, // 是否多选
showItemPropsConfig: Boolean // 是否多选
showItemPropsConfig: Boolean, // 是否多选
},
setup(props, { emit }) {
const { currentBlock } = useVisualData()
const { currentBlock } = useVisualData();
const state = reactive({
list: useVModel(props, 'modelValue', emit),
drag: false
})
drag: false,
});
const checkList = computed({
get: () => {
const value = currentBlock.value.props.modelValue
return Array.isArray(value) ? value : [...new Set(value?.split(','))]
const value = currentBlock.value.props.modelValue;
return Array.isArray(value) ? value : [...new Set(value?.split(','))];
},
set(value) {
currentBlock.value.props.modelValue = value
}
})
currentBlock.value.props.modelValue = value;
},
});
const dragOptions = computed(() => {
return {
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost'
}
})
ghostClass: 'ghost',
};
});
/**
* @description
*/
const onChange = (val: any[]) => {
val = val.filter((item) => item !== '')
val = val.filter((item) => item !== '');
val = props.multiple
? val
: val.filter((n) => !currentBlock.value.props.modelValue?.includes(n))
currentBlock.value.props.modelValue = val.join(',')
}
: val.filter((n) => !currentBlock.value.props.modelValue?.includes(n));
currentBlock.value.props.modelValue = val.join(',');
};
/**
* @param {number} index -
*/
const incrementOption = (index: number) => {
const length = state.list.length + 1
const length = state.list.length + 1;
const newItem = state.list.some((item) => isObject(item))
? Object.assign(cloneDeep(state.list[0]), {
label: `选项${length}`,
value: `选项${length}`
value: `选项${length}`,
})
: ''
state.list.splice(index + 1, 0, newItem)
}
: '';
state.list.splice(index + 1, 0, newItem);
};
return () => (
<div>
@ -106,7 +108,7 @@ export const CrossSortableOptionsEditor = defineComponent({
component-data={{
tag: 'ul',
type: 'transition-group',
name: !state.drag ? 'flip-list' : null
name: !state.drag ? 'flip-list' : null,
}}
handle=".handle"
{...dragOptions.value}
@ -117,7 +119,9 @@ export const CrossSortableOptionsEditor = defineComponent({
{{
item: ({ element, index }) => (
<div class={'flex items-center justify-between'}>
<i class={'el-icon-rank handle cursor-move'}></i>
<ElIcon class="handle cursor-move">
<Rank></Rank>
</ElIcon>
{isObject(element) ? (
<>
<ElCheckbox label={element.value} class={'ml-5px'}>
@ -128,12 +132,14 @@ export const CrossSortableOptionsEditor = defineComponent({
v-model={element.label}
class={'my-12px mx-3px'}
style={{ width: '108px' }}
size="small"
></ElInput>
value:
<ElInput
v-model={element.value}
class={'my-12px mx-3px'}
style={{ width: '106px' }}
size="small"
></ElInput>
</>
) : (
@ -141,20 +147,25 @@ export const CrossSortableOptionsEditor = defineComponent({
v-model={state.list[index]}
class={'m-12px'}
style={{ width: '270px' }}
size="small"
></ElInput>
)}
<div class={'flex flex-col'}>
<i
class={'el-icon-circle-plus-outline hover:text-blue-400 cursor-pointer'}
<ElIcon
class="hover:text-blue-400 cursor-pointer"
onClick={() => incrementOption(index)}
></i>
<i
class={'el-icon-remove-outline hover:text-red-500 cursor-pointer'}
>
<CirclePlus></CirclePlus>
</ElIcon>
<ElIcon
class="hover:text-red-500 cursor-pointer"
onClick={() => state.list.splice(index, 1)}
></i>
>
<Remove></Remove>
</ElIcon>
</div>
</div>
)
),
}}
</Draggable>
</ElCheckboxGroup>
@ -164,7 +175,7 @@ export const CrossSortableOptionsEditor = defineComponent({
<ElTabs type={'border-card'}>
{state.list.map((item: OptionItem) => (
<ElTabPane label={item.label} key={item.label}>
<ElForm size="mini" labelPosition={'left'}>
<ElForm labelPosition={'left'}>
<PropConfig component={item.component} block={item.block} />
</ElForm>
</ElTabPane>
@ -174,6 +185,6 @@ export const CrossSortableOptionsEditor = defineComponent({
</ElCollapse>
)}
</div>
)
}
})
);
},
});

@ -7,7 +7,7 @@
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\components\prop-config\index.tsx
*/
import { computed, defineComponent, PropType } from 'vue'
import { computed, defineComponent, PropType } from 'vue';
import {
ElColorPicker,
ElInput,
@ -17,51 +17,53 @@ import {
ElCascader,
ElInputNumber,
ElFormItem,
ElPopover
} from 'element-plus'
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props'
import { TablePropEditor, CrossSortableOptionsEditor } from '../../components'
import { cloneDeep } from 'lodash'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { VisualEditorBlockData, VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
ElTooltip,
ElIcon,
} from 'element-plus';
import { useDotProp } from '@/visual-editor/hooks/useDotProp';
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props';
import { TablePropEditor, CrossSortableOptionsEditor } from '../../components';
import { cloneDeep } from 'lodash';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import { VisualEditorBlockData, VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { Warning } from '@element-plus/icons-vue';
export const PropConfig = defineComponent({
props: {
component: {
type: Object as PropType<VisualEditorComponent>,
default: () => ({})
default: () => ({}),
},
block: {
type: Object as PropType<VisualEditorBlockData>,
default: () => ({})
}
default: () => ({}),
},
},
setup(props) {
const { jsonData } = useVisualData()
const { jsonData } = useVisualData();
/**
* @description
*/
const models = computed(() => cloneDeep(jsonData.models))
const models = computed(() => cloneDeep(jsonData.models));
const renderPropItem = (propName: string, propConfig: VisualEditorProps) => {
const { propObj, prop } = useDotProp(props.block.props, propName)
const { propObj, prop } = useDotProp(props.block.props, propName);
propObj[prop] ??= propConfig.defaultValue
propObj[prop] ??= propConfig.defaultValue;
return {
[VisualEditorPropsType.input]: () => {
if (!Object.is(propObj[prop], undefined) && !Object.is(propObj[prop], null)) {
propObj[prop] = `${propObj[prop]}`
propObj[prop] = `${propObj[prop]}`;
}
return (
<ElInput v-model={propObj[prop]} placeholder={propConfig.tips || propConfig.label} />
)
);
},
[VisualEditorPropsType.inputNumber]: () => {
const parseRes = parseFloat(propObj[prop])
propObj[prop] = Number.isNaN(parseRes) ? 0 : parseRes
return <ElInputNumber v-model={propObj[prop]} />
const parseRes = parseFloat(propObj[prop]);
propObj[prop] = Number.isNaN(parseRes) ? 0 : parseRes;
return <ElInputNumber v-model={propObj[prop]} />;
},
[VisualEditorPropsType.switch]: () => <ElSwitch v-model={propObj[prop]} />,
[VisualEditorPropsType.color]: () => <ElColorPicker v-model={propObj[prop]} />,
@ -90,15 +92,15 @@ export const PropConfig = defineComponent({
children: 'entitys',
label: 'name',
value: 'key',
expandTrigger: 'hover'
expandTrigger: 'hover',
}}
placeholder="请选择绑定的请求数据"
v-model={propObj[prop]}
options={models.value}
></ElCascader>
)
}[propConfig.type]()
}
),
}[propConfig.type]();
};
return () => {
return Object.entries(props.component.props ?? {}).map(([propName, propConfig]) => (
@ -110,7 +112,7 @@ export const PropConfig = defineComponent({
? {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start'
alignItems: 'flex-start',
}
: {}
}
@ -119,20 +121,24 @@ export const PropConfig = defineComponent({
label: () => (
<>
{propConfig.tips && (
<ElPopover width={200} trigger={'hover'} content={propConfig.tips}>
{{
reference: () => <i class={'el-icon-warning-outline'}></i>
}}
</ElPopover>
<ElTooltip
placement="left-start"
popper-class="max-w-200px"
content={propConfig.tips}
>
<ElIcon>
<Warning />
</ElIcon>
</ElTooltip>
)}
{propConfig.label}
</>
),
default: () => renderPropItem(propName, propConfig)
default: () => renderPropItem(propName, propConfig),
}}
</ElFormItem>
</>
))
}
}
})
));
};
},
});

@ -6,17 +6,18 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\attr-editor\index.tsx
*/
import { defineComponent, computed, watch } from 'vue'
import { ElForm, ElFormItem, ElPopover, ElRadioGroup, ElRadioButton } from 'element-plus'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { FormatInputNumber } from '@/visual-editor/components/common/format-input-number'
import { PropConfig } from './components/prop-config'
import { defineComponent, computed, watch } from 'vue';
import { ElForm, ElFormItem, ElPopover, ElRadioGroup, ElRadioButton, ElIcon } from 'element-plus';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import { FormatInputNumber } from '@/visual-editor/components/common/format-input-number';
import { PropConfig } from './components/prop-config';
import { Warning } from '@element-plus/icons-vue';
export const AttrEditor = defineComponent({
setup() {
const { visualConfig, currentBlock } = useVisualData()
const { visualConfig, currentBlock } = useVisualData();
const compPaddingAttrs = ['paddingTop', 'paddingLeft', 'paddingRight', 'paddingBottom']
const compPaddingAttrs = ['paddingTop', 'paddingLeft', 'paddingRight', 'paddingBottom'];
/**
* @description padding值的变化
@ -24,16 +25,16 @@ export const AttrEditor = defineComponent({
watch(
compPaddingAttrs.map((item) => () => currentBlock.value.styles?.[item]),
(val: string[]) => {
const isSame = val.every((item) => currentBlock.value.styles?.tempPadding == item)
const isSame = val.every((item) => currentBlock.value.styles?.tempPadding == item);
if (isSame || new Set(val).size === 1) {
if (Reflect.has(currentBlock.value, 'styles')) {
currentBlock.value.styles.tempPadding = val[0]
currentBlock.value.styles.tempPadding = val[0];
}
} else {
currentBlock.value.styles.tempPadding = ''
currentBlock.value.styles.tempPadding = '';
}
}
)
},
);
/**
* @description padding变化时进行的操作
@ -41,18 +42,18 @@ export const AttrEditor = defineComponent({
const compPadding = computed({
get: () => currentBlock.value.styles?.tempPadding,
set(val) {
compPaddingAttrs.forEach((item) => (currentBlock.value.styles[item] = val))
currentBlock.value.styles.tempPadding = val
}
})
compPaddingAttrs.forEach((item) => (currentBlock.value.styles[item] = val));
currentBlock.value.styles.tempPadding = val;
},
});
// 表单项
const FormEditor = () => {
const content: JSX.Element[] = []
const content: JSX.Element[] = [];
if (currentBlock.value) {
const { componentKey } = currentBlock.value
const component = visualConfig.componentMap[componentKey]
console.log('props.block:', currentBlock.value)
const { componentKey } = currentBlock.value;
const component = visualConfig.componentMap[componentKey];
console.log('props.block:', currentBlock.value);
content.push(
<>
<ElFormItem label="组件ID" labelWidth={'76px'}>
@ -60,23 +61,28 @@ export const AttrEditor = defineComponent({
<ElPopover
width={200}
trigger="hover"
effect="dark"
content={`你可以利用该组件ID。对该组件进行获取和设置其属性,组件可用属性可在控制台输入:$$refs.${currentBlock.value._vid} 进行查看`}
>
{{
reference: () => <i class={'el-icon-warning-outline ml-6px'}></i>
reference: () => (
<ElIcon class={'ml-6px'}>
<Warning />
</ElIcon>
),
}}
</ElPopover>
</ElFormItem>
</>
)
</>,
);
if (!!component) {
if (!!component.props) {
content.push(<PropConfig component={component} block={currentBlock.value} />)
content.push(<PropConfig component={component} block={currentBlock.value} />);
{
currentBlock.value.showStyleConfig &&
content.push(
<ElFormItem label={'组件对齐方式'} labelWidth={'90px'}>
<ElRadioGroup v-model={currentBlock.value.styles.justifyContent} size="mini">
<ElRadioGroup v-model={currentBlock.value.styles.justifyContent}>
<ElRadioButton label="flex-start">{'左对齐'}</ElRadioButton>
<ElRadioButton label="center">{'居中'}</ElRadioButton>
<ElRadioButton label="flex-end">{'右对齐'}</ElRadioButton>
@ -86,7 +92,7 @@ export const AttrEditor = defineComponent({
{{
label: () => (
<div class={'flex justify-between mb-2'}>
<div></div>
<div class="mr-3"></div>
<FormatInputNumber v-model={compPadding.value} class={'!w-100px'} />
</div>
),
@ -112,27 +118,25 @@ export const AttrEditor = defineComponent({
class={'!w-100px col-span-full col-start-2'}
/>
</div>
)
),
}}
</ElFormItem>
)
</ElFormItem>,
);
}
}
}
}
return (
<>
<ElForm size="mini" labelPosition={'left'}>
{content}
</ElForm>
<ElForm labelPosition={'left'}>{content}</ElForm>
</>
)
}
);
};
return () => (
<>
<FormEditor />
</>
)
}
})
);
},
});

@ -6,8 +6,8 @@
* @Description: -
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\event-action\index.tsx
*/
import { computed, ref, defineComponent, reactive } from 'vue'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { computed, ref, defineComponent, reactive } from 'vue';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import {
ElForm,
ElFormItem,
@ -19,16 +19,16 @@ import {
ElCascader,
ElCollapse,
ElCollapseItem,
ElPopconfirm
} from 'element-plus'
import type { Action } from '@/visual-editor/visual-editor.utils'
import { generateNanoid } from '@/visual-editor/utils/'
import { useModal } from '@/visual-editor/hooks/useModal'
import { cloneDeep } from 'lodash'
ElPopconfirm,
} from 'element-plus';
import type { Action } from '@/visual-editor/visual-editor.utils';
import { generateNanoid } from '@/visual-editor/utils/';
import { useModal } from '@/visual-editor/hooks/useModal';
import { cloneDeep } from 'lodash';
interface IState {
activeNames: string[]
ruleForm: Action
activeNames: string[];
ruleForm: Action;
}
/**
* @description
@ -36,8 +36,8 @@ interface IState {
const createEmptyActionHandle = () => ({
key: generateNanoid(),
name: '',
link: []
})
link: [],
});
/**
* @description
@ -46,24 +46,24 @@ const createEmptyAction = (): Action => ({
key: generateNanoid(),
name: '',
event: '',
handle: [createEmptyActionHandle()]
})
handle: [createEmptyActionHandle()],
});
export const EventAction = defineComponent({
setup() {
const { currentBlock, currentPage, jsonData } = useVisualData()
const { currentBlock, currentPage, jsonData } = useVisualData();
/**
* @description
*/
const isEdit = computed(() =>
currentBlock.value.actions?.some((item) => item.key === state.ruleForm.key)
)
const ruleFormRef = ref<InstanceType<typeof ElForm>>()
currentBlock.value.actions?.some((item) => item.key === state.ruleForm.key),
);
const ruleFormRef = ref<InstanceType<typeof ElForm>>();
const state = reactive<IState>({
activeNames: [],
ruleForm: createEmptyAction()
})
ruleForm: createEmptyAction(),
});
/**
* @description
@ -73,17 +73,17 @@ export const EventAction = defineComponent({
label: '全局',
value: 'global',
children: Object.keys(jsonData.actions).map((actionKey) => {
const item = cloneDeep(jsonData.actions[actionKey])
item.value = actionKey
item.label = item.name
const arrKey = Object.keys(item).find((key) => Array.isArray(item[key]))
const item = cloneDeep(jsonData.actions[actionKey]);
item.value = actionKey;
item.label = item.name;
const arrKey = Object.keys(item).find((key) => Array.isArray(item[key]));
item.children = (item[arrKey] || []).map((item: any) => {
item.label = item.name
item.value = item.key
return item
})
return item
})
item.label = item.name;
item.value = item.key;
return item;
});
return item;
}),
},
{
label: '组件',
@ -91,77 +91,77 @@ export const EventAction = defineComponent({
children: cloneDeep(currentPage.value.blocks)
.filter((item) => item.actions?.length)
.map((item) => {
item.value = item._vid
item.label = item.label
item.value = item._vid;
item.label = item.label;
item.children = (item.actions || []).map((item: any) => {
item.label = item.name
item.value = item.key
return item
})
return item
})
}
])
item.label = item.name;
item.value = item.key;
return item;
});
return item;
}),
},
]);
/**
* @description
*/
const getActionPath = (link: string[]) => {
const result: string[] = []
const result: string[] = [];
link.reduce((prev, curr) => {
const target = prev?.find((item) => item.value == curr)
result.push(`${target?.label}`)
return target?.children
}, actionOptions.value)
return result.join(' => ')
}
const target = prev?.find((item) => item.value == curr);
result.push(`${target?.label}`);
return target?.children;
}, actionOptions.value);
return result.join(' => ');
};
/**
* @description
*/
const deleteActionItem = (index: number) => {
currentBlock.value.actions.splice(index, 1)
}
currentBlock.value.actions.splice(index, 1);
};
/**
* @description
*/
const deleteActionHandleItem = (index: number) => {
state.ruleForm.handle.splice(index, 1)
}
state.ruleForm.handle.splice(index, 1);
};
/**
* @description
*/
const addActionItem = () => {
state.ruleForm = createEmptyAction()
showOperateModal()
}
state.ruleForm = createEmptyAction();
showOperateModal();
};
/**
* @description
*/
const addActionHanleItem = () => {
state.ruleForm.handle.push(createEmptyActionHandle())
}
state.ruleForm.handle.push(createEmptyActionHandle());
};
/**
* @description
*/
const showEditActionModal = (action: Action) => {
state.ruleForm = cloneDeep(action)
showOperateModal()
}
state.ruleForm = cloneDeep(action);
showOperateModal();
};
/**
* @description
*/
const showOperateModal = () => {
const operateType = isEdit.value ? '编辑' : '新增'
const operateType = isEdit.value ? '编辑' : '新增';
useModal({
title: `${operateType}动作`,
props: { width: 600 },
content: () => (
<ElForm model={state.ruleForm} ref={ruleFormRef} label-width="100px" size={'mini'}>
<ElForm model={state.ruleForm} ref={ruleFormRef} label-width="100px">
<ElFormItem
label="事件"
prop={'event'}
@ -186,7 +186,7 @@ export const EventAction = defineComponent({
</ElFormItem>
{!state.ruleForm.handle?.length && (
<ElFormItem>
<ElButton onClick={addActionHanleItem} type={'primary'} size={'mini'}>
<ElButton onClick={addActionHanleItem} type={'primary'}>
</ElButton>
</ElFormItem>
@ -207,19 +207,15 @@ export const EventAction = defineComponent({
<ElInput v-model={handleItem.name} placeholder={'请输入动作名称'}></ElInput>
</ElFormItem>
<div>
<ElButton
onClick={() => deleteActionHandleItem(index)}
type={'danger'}
size={'mini'}
>
<ElButton onClick={() => deleteActionHandleItem(index)} type={'danger'}>
</ElButton>
<ElButton onClick={addActionHanleItem} type={'primary'} size={'mini'}>
<ElButton onClick={addActionHanleItem} type={'primary'}>
</ElButton>
</div>
</div>
)
),
}}
>
<ElFormItem
@ -244,35 +240,30 @@ export const EventAction = defineComponent({
ruleFormRef.value?.validate((valid) => {
if (valid) {
const index = currentBlock.value.actions.findIndex(
(item) => item.key == state.ruleForm.key
)
(item) => item.key == state.ruleForm.key,
);
if (index === -1) {
currentBlock.value.actions.push(state.ruleForm)
currentBlock.value.actions.push(state.ruleForm);
} else {
currentBlock.value.actions.splice(index, 1, state.ruleForm)
currentBlock.value.actions.splice(index, 1, state.ruleForm);
}
state.ruleForm = createEmptyAction()
resolve('submit!')
state.ruleForm = createEmptyAction();
resolve('submit!');
} else {
reject()
console.log('error submit!!')
return false
reject();
console.log('error submit!!');
return false;
}
})
})
});
});
},
onCancel: () => (state.ruleForm = createEmptyAction())
})
}
onCancel: () => (state.ruleForm = createEmptyAction()),
});
};
return () => (
<>
<ElButton
onClick={addActionItem}
disabled={!currentBlock.value.actions}
type={'primary'}
size={'mini'}
>
<ElButton onClick={addActionItem} disabled={!currentBlock.value.actions} type="primary">
</ElButton>
@ -290,30 +281,22 @@ export const EventAction = defineComponent({
onConfirm={() => deleteActionItem(index)}
>
{{
reference: () => (
<ElButton type={'danger'} size={'mini'}>
</ElButton>
)
reference: () => <ElButton type={'danger'}></ElButton>,
}}
</ElPopconfirm>
<ElButton
onClick={() => showEditActionModal(actionItem)}
type={'primary'}
size={'mini'}
>
<ElButton onClick={() => showEditActionModal(actionItem)} type="primary">
</ElButton>
</div>
</div>
)
),
}}
>
<ElCollapse v-model={state.activeNames}>
{actionItem.handle.map((item, index) => (
<ElCollapseItem title={`${index + 1}. ${item.name}`} key={item.key} name={item.key}>
{{
default: () => <div>{getActionPath(item.link)}</div>
default: () => <div>{getActionPath(item.link)}</div>,
}}
</ElCollapseItem>
))}
@ -321,6 +304,6 @@ export const EventAction = defineComponent({
</ElCard>
))}
</>
)
}
})
);
},
});

@ -6,8 +6,9 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\form-rule\index.tsx
*/
import { defineComponent } from 'vue'
import { ElCard, ElTooltip } from 'element-plus'
import { defineComponent } from 'vue';
import { ElCard, ElTooltip } from 'element-plus';
import { QuestionFilled } from '@element-plus/icons-vue';
export const FormRule = defineComponent({
setup() {
@ -19,11 +20,13 @@ export const FormRule = defineComponent({
<div class="flex justify-between">
<span></span>
<ElTooltip content="当前面题目选中某些选项时才出现此题" placement="bottom-end">
<i class={'el-icon-question'}></i>
<el-icon>
<QuestionFilled />
</el-icon>
</ElTooltip>
</div>
),
default: () => <div></div>
default: () => <div></div>,
}}
</ElCard>
<ElCard shadow={'always'} bodyStyle={{ padding: 1 ? '0' : '20px' }} class={'mb-20px'}>
@ -35,14 +38,16 @@ export const FormRule = defineComponent({
content="当前面题目选择某些选项时才出现此题的某些选项 "
placement="bottom-end"
>
<i class={'el-icon-question'}></i>
<el-icon>
<QuestionFilled />
</el-icon>
</ElTooltip>
</div>
),
default: () => null
default: () => null,
}}
</ElCard>
</>
)
}
})
);
},
});

@ -6,25 +6,26 @@
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\page-setting\pageSetting.tsx
*/
import { defineComponent } from 'vue'
import { ElForm, ElFormItem, ElInput, ElUpload, ElColorPicker, ElSwitch } from 'element-plus'
import styles from './styles.module.scss'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { defineComponent } from 'vue';
import { ElForm, ElFormItem, ElInput, ElUpload, ElColorPicker, ElSwitch } from 'element-plus';
import styles from './styles.module.scss';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import { Plus } from '@element-plus/icons-vue';
export const PageSetting = defineComponent({
setup() {
const { currentPage } = useVisualData()
const { currentPage } = useVisualData();
const pageConfig = currentPage.value.config
const pageConfig = currentPage.value.config;
const beforeUpload = (file: File) => {
console.log(file, '要上传的文件')
const fileReader = new FileReader()
console.log(file, '要上传的文件');
const fileReader = new FileReader();
fileReader.onload = (event) => {
pageConfig.bgImage = event.target?.result as string
}
fileReader.readAsDataURL(file)
}
pageConfig.bgImage = event.target?.result as string;
};
fileReader.readAsDataURL(file);
};
return () => (
<>
@ -42,11 +43,13 @@ export const PageSetting = defineComponent({
{pageConfig.bgImage ? (
<img src={pageConfig.bgImage} />
) : (
<i class="el-icon-plus uploader-icon"></i>
<el-icon class="uploader-icon">
<Plus />
</el-icon>
)}
</ElUpload>
</ElForm>
</>
)
}
})
);
},
});

@ -72,7 +72,7 @@ $boxShadow: -2px 0 4px 0 rgb(0 0 0 / 10%);
font-size: 12px;
}
.el-form-item--mini .el-form-item__content {
.el-form-item .el-form-item__content {
display: flex;
justify-content: flex-end;
align-items: center;

@ -8,36 +8,37 @@
* RightAttributePanel
*/
import { defineComponent, reactive, watch } from 'vue'
import styles from './index.module.scss'
import { ElTabPane, ElTabs } from 'element-plus'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import { AttrEditor, Animate, PageSetting, EventAction, FormRule } from './components'
import { defineComponent, reactive, watch } from 'vue';
import styles from './index.module.scss';
import { ElTabPane, ElTabs } from 'element-plus';
import { useVisualData } from '@/visual-editor/hooks/useVisualData';
import { AttrEditor, Animate, PageSetting, EventAction, FormRule } from './components';
import { DArrowLeft, DArrowRight } from '@element-plus/icons-vue';
export default defineComponent({
name: 'RightAttributePanel',
setup() {
const { currentBlock } = useVisualData()
const { currentBlock } = useVisualData();
const state = reactive({
activeName: 'attr',
isOpen: true
})
isOpen: true,
});
watch(
() => currentBlock.value.label,
(newLabel) => {
if (!newLabel?.startsWith('表单') && state.activeName == 'form-rule') {
state.activeName = 'attr'
state.activeName = 'attr';
}
}
)
},
);
return () => (
<>
<div class={[styles.drawer, { [styles.isOpen]: state.isOpen }]}>
<div class={styles.floatingActionBtn} onClick={() => (state.isOpen = !state.isOpen)}>
<i class={`el-icon-d-arrow-${state.isOpen ? 'right' : 'left'}`}></i>
{state.isOpen ? <DArrowRight /> : <DArrowLeft />}
</div>
<div class={styles.attrs}>
<ElTabs
@ -67,6 +68,6 @@ export default defineComponent({
</div>
</div>
</>
)
}
})
);
},
});

@ -1,66 +1,68 @@
import type { VisualEditorProps } from './visual-editor.props'
import { inject, provide } from 'vue'
import type { CSSProperties } from 'vue'
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
import type { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum'
import { generateNanoid } from '@/visual-editor/utils'
import type { VisualEditorProps } from './visual-editor.props';
import { inject, provide } from 'vue';
import type { CSSProperties } from 'vue';
import { useDotProp } from '@/visual-editor/hooks/useDotProp';
import type { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { generateNanoid } from '@/visual-editor/utils';
/**
* @description
*/
export interface VisualEditorBlockData {
/** 组件id 时间戳, 组件唯一标识 */
_vid: string
_vid: string;
/** 组件所属的模块(基础组件、容器组件) */
moduleName: keyof ComponentModules
moduleName: keyof ComponentModules;
/** 映射 VisualEditorConfig 中 componentMap 的 component对象 */
componentKey: string
componentKey: string;
/** 组件标签名称 */
label: string
label: string;
/** 是否需要调整位置 */
adjustPosition: boolean
adjustPosition: boolean;
/** 当前是否为选中状态 */
focus: boolean
focus: boolean;
/** 当前组件的样式 */
styles: CSSProperties
styles: CSSProperties & {
tempPadding?: string;
};
/** 是否调整过宽度或者高度 */
hasResize: boolean
hasResize: boolean;
/** 组件的设计属性 */
props: Record<string, any>
props: Record<string, any>;
/** 绑定的字段 */
model: Record<string, string>
model: Record<string, string>;
/** 组件是否可以被拖拽 */
draggable: boolean
draggable: boolean;
/** 是否显示组件样式配置项 */
showStyleConfig?: boolean
showStyleConfig?: boolean;
/** 动画集 */
animations?: Animation[]
animations?: Animation[];
/** 组件动作集合 */
actions: Action[]
actions: Action[];
/** 组件事件集合 */
events: { label: string; value: string }[]
[prop: string]: any
events: { label: string; value: string }[];
[prop: string]: any;
}
/**
* @description
*/
export interface ActionHandle {
key: string
name: string
link: string[]
key: string;
name: string;
link: string[];
data?: {
bind?: string
recv?: string
}
bind?: string;
recv?: string;
};
}
/**
* @description
*/
export interface Action {
key: string
name: string
event: string
handle: ActionHandle[]
key: string;
name: string;
event: string;
handle: ActionHandle[];
}
/**
@ -68,80 +70,80 @@ export interface Action {
*/
export interface PageConfig {
/** 背景图片 */
bgImage: string
bgImage: string;
/** 背景颜色 */
bgColor: string
bgColor: string;
/** 是否缓存当前页面 */
keepAlive: boolean
keepAlive: boolean;
}
/**
* @description
*/
export interface VisualEditorPage {
/** 页面标题 */
title: string
title: string;
/** 页面路径 */
path: string
path: string;
/** 404是重定向到默认页面 */
isDefault?: boolean
isDefault?: boolean;
/** 页面配置 */
config: PageConfig
config: PageConfig;
/** 当前页面的所有组件 */
blocks: VisualEditorBlockData[]
blocks: VisualEditorBlockData[];
}
/**
* @description =>
*/
export interface VisualEditorPages {
[path: string]: VisualEditorPage
[path: string]: VisualEditorPage;
}
/**
* @description
*/
export type EntityType = {
/** 绑定的字段 输入 */
key: string
key: string;
/** 实体名称 输入 */
name: string
name: string;
/** 数据类型 选择 */
type: string
type: string;
/** 默认值 输入 */
value: string
}
value: string;
};
/**
* @description
*/
export interface VisualEditorModel {
/** 数据源名称 */
name: string
name: string;
/** 绑定的字段 该字段创建的时候生成 */
key: string
key: string;
/** 实体集合 */
entitys: EntityType[]
entitys: EntityType[];
}
/**
* @description
*/
export interface FetchApiItem {
/** 随机生成的key */
key: string
key: string;
/** 随机生成的key */
name: string
name: string;
options: {
/** 请求的url */
url: string
url: string;
/** 请求的方法 */
method: keyof typeof RequestEnum
method: keyof typeof RequestEnum;
/** 请求的内容类型 */
contentType: keyof typeof ContentTypeEnum
}
contentType: keyof typeof ContentTypeEnum;
};
data: {
/** 请求绑定对应的某个实体 */
bind: string
bind: string;
/** 响应的结果绑定到某个实体上 */
recv: string
}
recv: string;
};
}
/**
@ -149,81 +151,81 @@ export interface FetchApiItem {
*/
export interface VisualEditorActions {
fetch: {
name: '接口请求'
apis: FetchApiItem[]
}
name: '接口请求';
apis: FetchApiItem[];
};
dialog: {
name: '对话框'
handlers: []
}
name: '对话框';
handlers: [];
};
}
/**
* @description
*/
export interface VisualEditorModelValue {
/** 页面 */
pages: VisualEditorPages
pages: VisualEditorPages;
/** 实体 */
models: VisualEditorModel[]
models: VisualEditorModel[];
/** 动作 */
actions: VisualEditorActions
actions: VisualEditorActions;
}
/**
* @description
*/
export interface Animation {
/** 动画名称 */
label: string
label: string;
/** 动画类名 */
value: string
value: string;
/** 动画持续时间 */
duration: number
duration: number;
/** 动画延迟多久执行 */
delay: number
delay: number;
/** 动画执行次数 */
count: number
count: number;
/** 是否无限循环动画 */
infinite: boolean
infinite: boolean;
}
/**
* @description
*/
export interface VisualEditorComponent {
/** 组件name */
key: string
key: string;
/** 组件所属模块名称 */
moduleName: keyof ComponentModules
moduleName: keyof ComponentModules;
/** 组件唯一id */
_vid?: string
_vid?: string;
/** 组件中文名称 */
label: string
label: string;
/** 组件预览函数 */
preview: () => JSX.Element
preview: () => JSX.Element;
/** 组件渲染函数 */
render: (data: {
props: any
model: any
styles: CSSProperties
block: VisualEditorBlockData
custom: Record<string, any>
}) => () => JSX.Element
props: any;
model: any;
styles: CSSProperties;
block: VisualEditorBlockData;
custom: Record<string, any>;
}) => () => JSX.Element;
/** 组件是否可以被拖拽 */
draggable?: boolean
draggable?: boolean;
/** 是否显示组件的样式配置项 */
showStyleConfig?: boolean
showStyleConfig?: boolean;
/** 组件属性 */
props?: Record<string, VisualEditorProps>
props?: Record<string, VisualEditorProps>;
/** 动画集 */
animations?: Animation[]
animations?: Animation[];
/** 组件事件集合 */
events?: { label: string; value: string }[]
events?: { label: string; value: string }[];
/** 组件样式 */
styles?: CSSProperties
styles?: CSSProperties;
}
export interface VisualEditorMarkLines {
x: { left: number; showLeft: number }[]
y: { top: number; showTop: number }[]
x: { left: number; showLeft: number }[];
y: { top: number; showTop: number }[];
}
export function createNewBlock(component: VisualEditorComponent): VisualEditorBlockData {
@ -241,54 +243,54 @@ export function createNewBlock(component: VisualEditorComponent): VisualEditorBl
paddingRight: '0',
paddingLeft: '0',
paddingBottom: '0',
tempPadding: '0'
tempPadding: '0',
},
hasResize: false,
props: Object.keys(component.props || {}).reduce((prev, curr) => {
const { propObj, prop } = useDotProp(prev, curr)
const { propObj, prop } = useDotProp(prev, curr);
if (component.props![curr]?.defaultValue) {
propObj[prop] = prev[curr] = component.props![curr]?.defaultValue
propObj[prop] = prev[curr] = component.props![curr]?.defaultValue;
}
return prev
return prev;
}, {}),
draggable: component.draggable ?? true, // 是否可以拖拽
showStyleConfig: component.showStyleConfig ?? true, // 是否显示组件样式配置
animations: [], // 动画集
actions: [], // 动作集合
events: component.events || [], // 事件集合
model: {}
}
model: {},
};
}
export interface VisualDragEvent {
dragstart: {
on: (cb: () => void) => void
off: (cb: () => void) => void
emit: () => void
}
on: (cb: () => void) => void;
off: (cb: () => void) => void;
emit: () => void;
};
dragend: {
on: (cb: () => void) => void
off: (cb: () => void) => void
emit: () => void
}
on: (cb: () => void) => void;
off: (cb: () => void) => void;
emit: () => void;
};
}
export const VisualDragProvider = (() => {
const VISUAL_DRAG_PROVIDER = '@@VISUAL_DRAG_PROVIDER'
const VISUAL_DRAG_PROVIDER = '@@VISUAL_DRAG_PROVIDER';
return {
provide: (data: VisualDragEvent) => {
provide(VISUAL_DRAG_PROVIDER, data)
provide(VISUAL_DRAG_PROVIDER, data);
},
inject: () => {
return inject(VISUAL_DRAG_PROVIDER) as VisualDragEvent
}
}
})()
return inject(VISUAL_DRAG_PROVIDER) as VisualDragEvent;
},
};
})();
// 组件模块
export interface ComponentModules {
baseWidgets: VisualEditorComponent[] // 基础组件
containerComponents: VisualEditorComponent[] // 容器组件
baseWidgets: VisualEditorComponent[]; // 基础组件
containerComponents: VisualEditorComponent[]; // 容器组件
}
/**
* @description
@ -297,40 +299,40 @@ export interface ComponentModules {
export function createVisualEditorConfig() {
const componentModules: ComponentModules = {
baseWidgets: [],
containerComponents: []
}
containerComponents: [],
};
// const componentList: VisualEditorComponent[] = []
const componentMap: Record<string, VisualEditorComponent> = {}
const componentMap: Record<string, VisualEditorComponent> = {};
return {
componentModules,
componentMap,
registry: <
_,
Props extends Record<string, VisualEditorProps> = {},
Model extends Record<string, string> = {}
Model extends Record<string, string> = {},
>(
moduleName: keyof ComponentModules,
key: string,
component: {
label: string
preview: () => JSX.Element
label: string;
preview: () => JSX.Element;
render: (data: {
props: { [k in keyof Props]: any }
model: Partial<{ [k in keyof Model]: any }>
styles: CSSProperties
block: VisualEditorBlockData
custom: Record<string, any>
}) => () => JSX.Element
props?: Props
model?: Model
styles?: CSSProperties
}
props: { [k in keyof Props]: any };
model: Partial<{ [k in keyof Model]: any }>;
styles: CSSProperties;
block: VisualEditorBlockData;
custom: Record<string, any>;
}) => () => JSX.Element;
props?: Props;
model?: Model;
styles?: CSSProperties;
},
) => {
const comp = { ...component, key, moduleName }
componentModules[moduleName].push(comp)
componentMap[key] = comp
}
}
const comp = { ...component, key, moduleName };
componentModules[moduleName].push(comp);
componentMap[key] = comp;
},
};
}
export type VisualEditorConfig = ReturnType<typeof createVisualEditorConfig>
export type VisualEditorConfig = ReturnType<typeof createVisualEditorConfig>;

@ -1,25 +1,38 @@
module.exports = {
root: true,
plugins: ['stylelint-order', 'stylelint-scss'],
plugins: ['stylelint-order'],
customSyntax: 'postcss-html',
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
rules: {
'selector-class-pattern': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'v-deep', 'deep']
}
ignorePseudoClasses: ['global'],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep', 'deep']
}
ignorePseudoElements: ['v-deep'],
},
],
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'for']
}
ignoreAtRules: [
'tailwind',
'apply',
'variants',
'responsive',
'screen',
'function',
'if',
'each',
'include',
'mixin',
],
},
],
'no-empty-source': null,
'named-grid-areas-no-invalid': null,
@ -28,12 +41,12 @@ module.exports = {
'font-family-no-missing-generic-family-keyword': null,
'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never',
'declaration-block-trailing-semicolon': ['always', { ignore: ['single-declaration'] }],
// 'declaration-block-trailing-semicolon': 'always',
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'first-nested']
}
ignore: ['after-comment', 'first-nested'],
},
],
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
'order/order': [
@ -44,165 +57,37 @@ module.exports = {
'declarations',
{
type: 'at-rule',
name: 'supports'
name: 'supports',
},
{
type: 'at-rule',
name: 'media'
name: 'media',
},
'rules'
'rules',
],
{ severity: 'warning' }
{ severity: 'warning' },
],
// Specify the alphabetical order of the attributes in the declaration block
'order/properties-order': [
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'display',
'float',
'width',
'height',
'max-width',
'max-height',
'min-width',
'min-height',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-collapse',
'margin-top-collapse',
'margin-right-collapse',
'margin-bottom-collapse',
'margin-left-collapse',
'overflow',
'overflow-x',
'overflow-y',
'clip',
'clear',
'font',
'font-family',
'font-size',
'font-smoothing',
'osx-font-smoothing',
'font-style',
'font-weight',
'hyphens',
'src',
'line-height',
'letter-spacing',
'word-spacing',
'color',
'text-align',
'text-decoration',
'text-indent',
'text-overflow',
'text-rendering',
'text-size-adjust',
'text-shadow',
'text-transform',
'word-break',
'word-wrap',
'white-space',
'vertical-align',
'list-style',
'list-style-type',
'list-style-position',
'list-style-image',
'pointer-events',
'cursor',
'background',
'background-attachment',
'background-color',
'background-image',
'background-position',
'background-repeat',
'background-size',
'border',
'border-collapse',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-color',
'border-image',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-spacing',
'border-style',
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'border-width',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-radius',
'border-top-right-radius',
'border-bottom-right-radius',
'border-bottom-left-radius',
'border-top-left-radius',
'border-radius-topright',
'border-radius-bottomright',
'border-radius-bottomleft',
'border-radius-topleft',
'content',
'quotes',
'outline',
'outline-offset',
'opacity',
'filter',
'visibility',
'size',
'zoom',
'transform',
'box-align',
'box-flex',
'box-orient',
'box-pack',
'box-shadow',
'box-sizing',
'table-layout',
'animation',
'animation-delay',
'animation-duration',
'animation-iteration-count',
'animation-name',
'animation-play-state',
'animation-timing-function',
'animation-fill-mode',
'transition',
'transition-delay',
'transition-duration',
'transition-property',
'transition-timing-function',
'background-clip',
'backface-visibility',
'resize',
'appearance',
'user-select',
'interpolation-mode',
'direction',
'marks',
'page',
'set-link-source',
'unicode-bidi',
'speak'
]
},
ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts']
}
ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
overrides: [
{
files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
extends: ['stylelint-config-recommended', 'stylelint-config-html'],
rules: {
'keyframes-name-pattern': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['deep', 'global'],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
},
],
},
},
],
};

@ -24,12 +24,26 @@
"removeComments": false,
"strictFunctionTypes": false,
"baseUrl": ".",
"types": ["vite/client", "node"],
"typeRoots": ["./node_modules/@types/", "./types"],
"types": [
"vite/client",
"node",
"element-plus/global"
],
"typeRoots": [
"./node_modules/@types/",
"./types"
],
"paths": {
"@/*": ["src/*"]
"@/*": [
"src/*"
]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
@ -43,5 +57,8 @@
"preview/**/*.tsx",
"preview/**/*.vue"
],
"exclude": ["node_modules", "dist"]
"exclude": [
"node_modules",
"dist"
]
}

21
types/env.d.ts vendored

@ -0,0 +1,21 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
/** 网站标题 */
readonly VITE_APP_TITLE: string;
/** 网站部署的目录 */
readonly VITE_BASE_URL: string;
/** API 接口路径 */
readonly VITE_BASE_API: string;
/** socket 请求路径前缀 */
readonly VITE_BASE_SOCKET_PATH: string;
/** socket 命名空间 */
readonly VITE_BASE_SOCKET_NSP: string;
/** mock API 路径 */
readonly VITE_MOCK_API: string;
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

83
types/global.d.ts vendored

@ -0,0 +1,83 @@
import type {
ComponentRenderProxy,
VNode,
VNodeChild,
ComponentPublicInstance,
FunctionalComponent,
PropType as VuePropType,
} from 'vue';
declare global {
const __APP_INFO__: {
pkg: {
name: string;
version: string;
dependencies: Recordable<string>;
devDependencies: Recordable<string>;
};
lastBuildTime: string;
};
// declare interface Window {
// // Global vue app instance
// __APP__: App<Element>;
// }
// vue
declare type PropType<T> = VuePropType<T>;
declare type VueNode = VNodeChild | JSX.Element;
export type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
type RemoveIndex<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
};
declare type Nullable<T> = T | null;
declare type NonNullable<T> = T extends null | undefined ? never : T;
declare type Recordable<T = any> = Record<string, T>;
declare type ReadonlyRecordable<T = any> = {
readonly [key: string]: T;
};
declare type Indexable<T = any> = {
[key: string]: T;
};
declare type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
declare type TimeoutHandle = ReturnType<typeof setTimeout>;
declare type IntervalHandle = ReturnType<typeof setInterval>;
declare interface ChangeEvent extends Event {
target: HTMLInputElement;
}
declare interface WheelEvent {
path?: EventTarget[];
}
declare function parseInt(s: string | number, radix?: number): number;
declare function parseFloat(string: string | number): number;
namespace JSX {
// tslint:disable no-empty-interface
type Element = VNode;
// tslint:disable no-empty-interface
type ElementClass = ComponentRenderProxy;
interface ElementAttributesProperty {
$props: any;
}
interface IntrinsicElements {
[elem: string]: any;
}
interface IntrinsicAttributes {
[elem: string]: any;
}
}
}
declare module 'vue' {
export type JSXComponent<Props = any> =
| { new (): ComponentPublicInstance<Props> }
| FunctionalComponent<Props>;
}

34
types/index.d.ts vendored

@ -0,0 +1,34 @@
declare interface Fn<T = any, R = T> {
(...arg: T[]): R;
}
declare interface PromiseFn<T = any, R = T> {
(...arg: T[]): Promise<R>;
}
declare type RefType<T> = T | null;
declare type LabelValueOptions = {
label: string;
value: any;
[key: string]: string | number | boolean;
}[];
declare type EmitType = (event: string, ...args: any[]) => void;
declare type TargetContext = '_self' | '_blank';
declare interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
$el: T;
}
/** 将联合类型转为交叉类型 */
declare type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null;
declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>;

20
types/modules.d.ts vendored

@ -0,0 +1,20 @@
declare module '*.vue' {
import { DefineComponent } from 'vue';
const Component: DefineComponent<{}, {}, any>;
export default Component;
}
declare module 'mitt' {
import mitt from 'mitt';
export default mitt;
}
declare module 'blueimp-md5' {
import md5 from 'blueimp-md5';
export default md5;
}
declare module 'virtual:*' {
const result: any;
export default result;
}

@ -1,67 +1,67 @@
declare module '*.bmp' {
const src: string
export default src
const src: string;
export default src;
}
declare module '*.gif' {
const src: string
export default src
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string
export default src
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string
export default src
const src: string;
export default src;
}
declare module '*.png' {
const src: string
export default src
const src: string;
export default src;
}
declare module '*.webp' {
const src: string
export default src
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string }
export default classes
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string }
export default classes
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.less' {
const classes: { readonly [key: string]: string }
export default classes
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string }
export default classes
const classes: { readonly [key: string]: string };
export default classes;
}
declare module 'moment/locale/*' {
const LocaleMessage: { [key: string]: any }
export default LocaleMessage
const LocaleMessage: { [key: string]: any };
export default LocaleMessage;
}
declare module 'ant-design-vue/es/locale-provider/*' {
const LocaleMessage: { [key: string]: any }
export default LocaleMessage
const LocaleMessage: { [key: string]: any };
export default LocaleMessage;
}
// ant-design-vue/es/base
declare module 'ant-design-vue/es/base' {
class Base {
static install(vue: typeof Vue): void
static install(vue: typeof Vue): void;
}
export default Base
export default Base;
}

@ -1,7 +1,7 @@
declare module '*.vue' {
import { defineComponent } from 'vue'
const component: ReturnType<typeof defineComponent>
export default component
import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}
// declare module '*.vue' {
@ -10,15 +10,16 @@ declare module '*.vue' {
// }
// declare module '@vue/runtime-core' {
// interface ComponentCustomProperties {
// $createLoading: () => any
// }
// export interface ComponentCustomProperties {
// $http: typeof axios
// $validate: (data: object, rule: object) => boolean
// }
// }
declare type Nullable<T> = T | null
declare type Nullable<T> = T | null;
declare type CustomizedHTMLElement<T> = HTMLElement & T
declare type CustomizedHTMLElement<T> = HTMLElement & T;
declare type Indexable<T> = {
[key: string]: T
}
[key: string]: T;
};

@ -0,0 +1,34 @@
import { type PermissionType } from '@/core/permission/modules/types';
interface Title18n {
zh_CN: string;
en_US: string;
}
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, unknown> {
/** 标题 */
title: string | Title18n;
/** 当前路由是否需要权限验证 */
isAuth?: boolean;
/** 当前路由权限 */
perms?: PermissionType[];
/** 是否需要缓存 */
keepAlive?: boolean;
/** 当前路由namePath 祖先name集合 */
namePath?: string[];
/** 当前路由所在的完整路径 */
fullPath?: string;
/** 是否固定在标签栏 */
affix?: boolean;
/** 菜单图标 */
icon?: string;
/** 当前页面切换动画 */
transitionName?: string;
/** 不在菜单中显示 */
hideInMenu?: boolean;
/** 设置当前路由高亮的菜单项,值为route fullPath或route name,一般用于详情页 */
activeMenu?: string;
isLink?: boolean;
}
}

10
types/vuex.d.ts vendored

@ -1,16 +1,14 @@
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
// import { ComponentCustomProperties } from 'vue';
import { Store } from 'vuex';
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number
count: number;
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
$store: Store<State>;
}
}
export {}

@ -1,59 +1,91 @@
import { ConfigEnv, loadEnv, UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import legacy from '@vitejs/plugin-legacy'
import { resolve } from 'path'
import ViteComponents, { ElementPlusResolver, VantResolver } from 'vite-plugin-components'
import WindiCSS from 'vite-plugin-windicss'
import VitePluginElementPlus from 'vite-plugin-element-plus'
import { ConfigEnv, loadEnv, UserConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from '@vitejs/plugin-legacy';
import { resolve } from 'path';
import { ElementPlusResolver, VantResolver } from 'unplugin-vue-components/resolvers';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import WindiCSS from 'vite-plugin-windicss';
const CWD = process.cwd()
const CWD = process.cwd();
const prefix = `monaco-editor/esm/vs`
const prefix = `monaco-editor/esm/vs`;
// https://cn.vitejs.dev/config/
export default ({ mode }: ConfigEnv): UserConfig => {
// 环境变量
const { VITE_BASE_URL } = loadEnv(mode, CWD)
const { VITE_BASE_URL } = loadEnv(mode, CWD);
return {
base: VITE_BASE_URL, // 设置打包路径
css: {
modules: {
localsConvention: 'camelCase' // 默认只支持驼峰,修改为同时支持横线和驼峰
}
localsConvention: 'camelCase', // 默认只支持驼峰,修改为同时支持横线和驼峰
},
preprocessorOptions: {
scss: {
charset: false,
},
less: {
charset: false,
},
},
// TODO 构建包含@charset问题 https://github.com/vitejs/vite/issues/5833
charset: false,
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove();
}
},
},
},
],
},
},
plugins: [
vue(),
vueJsx(),
WindiCSS(),
legacy({
targets: ['defaults', 'not IE 11']
targets: ['defaults', 'not IE 11'],
}),
AutoImport({
resolvers: [ElementPlusResolver(), VantResolver()],
}),
ViteComponents({
globalComponentsDeclaration: true,
// 自动导入组件(还不够完善,可能会有样式丢失)
// valid file extensions for components.
extensions: ['vue', 'tsx', 'js'],
customComponentResolvers: [ElementPlusResolver(), VantResolver()]
Components({
resolvers: [ElementPlusResolver(), VantResolver()],
}),
VitePluginElementPlus({
// 如果你需要使用 [component name].scss 源文件,你需要把下面的注释取消掉。
// 对于所有的 API 你可以参考 https://github.com/element-plus/vite-plugin-element-plus
// 的文档注释
// useSource: true
format: mode === 'development' ? 'esm' : 'cjs'
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src') // 设置 `@` 指向 `src` 目录
}
alias: [
{
find: '@',
replacement: resolve(__dirname, './src'),
},
],
},
build: {
cssCodeSplit: true, // 如果设置为false,整个项目中的所有 CSS 将被提取到一个 CSS 文件中
sourcemap: false, // 构建后是否生成 source map 文件。如果为 true,将会创建一个独立的 source map 文件
target: 'modules', // 设置最终构建的浏览器兼容目标。默认值是一个 Vite 特有的值——'modules' 还可设置为 'es2015' 'es2016'等
chunkSizeWarningLimit: 550, // 单位kb 打包后文件大小警告的限制 (文件大于此此值会出现警告)
assetsInlineLimit: 4096, // 单位字节(1024等于1kb) 小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求。设置为 0 可以完全禁用此项。
minify: 'terser', // 'terser' 相对较慢,但大多数情况下构建后的文件体积更小。'esbuild' 最小化混淆更快但构建后的文件相对更大。
terserOptions: {
compress: {
drop_console: true, // 生产环境去除console
drop_debugger: true, // 生产环境去除debugger
},
},
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
preview: resolve(__dirname, 'preview/index.html')
preview: resolve(__dirname, 'preview/index.html'),
},
output: {
manualChunks: {
@ -61,22 +93,13 @@ export default ({ mode }: ConfigEnv): UserConfig => {
cssWorker: [`${prefix}/language/css/css.worker`],
htmlWorker: [`${prefix}/language/html/html.worker`],
tsWorker: [`${prefix}/language/typescript/ts.worker`],
editorWorker: [`${prefix}/editor/editor.worker`]
}
}
}
editorWorker: [`${prefix}/editor/editor.worker`],
},
},
},
},
optimizeDeps: {
include: [
'vue',
'vue-router',
'@vueuse/core',
'element-plus',
'vant',
'lodash',
'vuedraggable'
],
exclude: ['vue-demi']
include: ['@vueuse/core', 'element-plus', 'vant', 'lodash', 'vuedraggable'],
},
server: {
host: '0.0.0.0',
@ -90,9 +113,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
target: 'http://29135jo738.zicp.vip/api/v1',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace('/api/', '/')
}
}
}
}
}
rewrite: (path) => path.replace('/api/', '/'),
},
},
},
};
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save