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, root: true,
env: { env: {
browser: true, browser: true,
node: true, node: true,
es6: true es6: true,
}, },
parser: 'vue-eslint-parser', parser: 'vue-eslint-parser',
parserOptions: { parserOptions: {
@ -12,36 +14,66 @@ module.exports = {
sourceType: 'module', sourceType: 'module',
jsxPragma: 'React', jsxPragma: 'React',
ecmaFeatures: { ecmaFeatures: {
jsx: true jsx: true,
} },
}, },
extends: [ extends: [
'plugin:vue/vue3-recommended', 'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
'prettier'
], ],
rules: { rules: {
'vue/require-default-prop': 'off', 'vue/script-setup-uses-vars': 'error',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': '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/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': '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': [ 'vue/html-self-closing': [
'error', 'error',
{ {
html: { html: {
void: 'always', void: 'always',
normal: 'never', normal: 'never',
component: 'always' component: 'always',
}, },
svg: 'always', svg: 'always',
math: 'always' math: 'always',
} },
] ],
'vue/multi-word-component-names': 'off',
}, },
settings: {} });
}

@ -1,30 +1,35 @@
name: deploy name: syncToGitee
env: env:
# 7 GiB by default on GitHub, setting to 6 GiB # 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 # 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 NODE_OPTIONS: --max-old-space-size=6144
on: on:
push: push:
branches: [main] branches: [main]
jobs: jobs:
deploy: repo-sync:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Setup Node.js v14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: '14.x' node-version: '14.x'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Install - name: Install
run: yarn install --frozen-lockfile run: pnpm install
- name: Build - name: Build
run: yarn build run: pnpm build
- name: Deploy - name: Deploy
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
@ -33,3 +38,28 @@ jobs:
personal_token: ${{ secrets.PERSONAL_TOKEN }} personal_token: ${{ secrets.PERSONAL_TOKEN }}
commit_message: Update ghPages commit_message: Update ghPages
force_orphan: true 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 # Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged npm run lint:lint-staged
npm run lint:pretty

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

@ -1,32 +1,10 @@
{ {
"cSpell.words": ["windi"],
"typescript.tsdk": "./node_modules/typescript/lib", "typescript.tsdk": "./node_modules/typescript/lib",
"volar.tsPlugin": true, "volar.tsPlugin": true,
"volar.tsPluginStatus": false, "volar.tsPluginStatus": false,
//=========================================== "npm.packageManager": "pnpm",
//============= Editor ======================
//===========================================
"explorer.openEditors.visible": 0,
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode", "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", "files.eol": "\n",
"search.exclude": { "search.exclude": {
"**/node_modules": true, "**/node_modules": true,
@ -49,9 +27,14 @@
"CHANGELOG.md": true, "CHANGELOG.md": true,
"examples": true, "examples": true,
"res": true, "res": true,
"screenshots": true "screenshots": true,
"yarn-error.log": true,
"**/.yarn": true
}, },
"files.exclude": { "files.exclude": {
"**/.cache": true,
"**/.editorconfig": true,
"**/.eslintcache": true,
"**/bower_components": true, "**/bower_components": true,
"**/.idea": true, "**/.idea": true,
"**/tmp": true, "**/tmp": true,
@ -73,26 +56,9 @@
}, },
"stylelint.enable": true, "stylelint.enable": true,
"stylelint.packageManager": "yarn", "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": { "path-intellisense.mappings": {
"@/": "${workspaceRoot}/src" "/@/": "${workspaceRoot}/src"
}, },
"prettier.requireConfig": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"workbench.sideBar.location": "left",
"[javascriptreact]": { "[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
@ -112,11 +78,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[scss]": { "[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false,
"source.fixAll.stylelint": true
}
}, },
"[markdown]": { "[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
@ -126,16 +88,23 @@
}, },
"[vue]": { "[vue]": {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": false, "source.fixAll.eslint": false
"source.fixAll.stylelint": true
} }
}, },
"i18n-ally.localesPaths": ["src/locales/lang"], "i18n-ally.localesPaths": [
"src/locales/lang"
],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,
"i18n-ally.namespace": true, "i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}", "i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.enabledParsers": ["ts"], "i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sourceLanguage": "en", "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 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/) - 编程语言:[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 = { let propObj = {
string: (config) => `createEditorInputProp(${JSON.stringify(config)})`, string: (config) => `createEditorInputProp(${JSON.stringify(config)})`,
number: (config) => `createEditorInputNumberProp(${JSON.stringify(config)})`, number: (config) => `createEditorInputNumberProp(${JSON.stringify(config)})`,
boolean: (config) => `createEditorSwitchProp(${JSON.stringify(config)})` boolean: (config) => `createEditorSwitchProp(${JSON.stringify(config)})`,
} };
JSON.stringify( JSON.stringify(
$$('#props + table tbody tr').reduce((prev, curr) => { $$('#props + table tbody tr').reduce((prev, curr) => {
const children = curr.children const children = curr.children;
const key = children[0].textContent.replace(/-([a-z])/g, (all, i) => i.toUpperCase()) const key = children[0].textContent.replace(/-([a-z])/g, (all, i) => i.toUpperCase());
const child3Text = children[3].textContent const child3Text = children[3].textContent;
const defaultValue = ['true', 'false'].includes(child3Text) const defaultValue = ['true', 'false'].includes(child3Text)
? child3Text ? child3Text
: `'${child3Text == '-' ? '' : child3Text}'` : `'${child3Text == '-' ? '' : child3Text}'`;
const value = (propObj[children[2].textContent] ?? propObj['string'])({ const value = (propObj[children[2].textContent] ?? propObj['string'])({
label: `'${children[1].textContent}'`, label: `'${children[1].textContent}'`,
defaultValue defaultValue,
}).replaceAll('"', '') }).replaceAll('"', '');
prev[key] = value prev[key] = value;
return prev return prev;
}, {}) }, {}),
).replaceAll('"', '') ).replaceAll('"', '');
``` ```
```javascript ```javascript
// 在vant文档中 chrome控制台输入以下代码,快速生成组件事件 // 在vant文档中 chrome控制台输入以下代码,快速生成组件事件
JSON.stringify( JSON.stringify(
$$('#events + table tbody tr').reduce((prev, curr) => { $$('#events + table tbody tr').reduce((prev, curr) => {
const children = curr.children const children = curr.children;
const event = { const event = {
label: children[1].textContent, label: children[1].textContent,
value: children[0].textContent value: children[0].textContent,
} };
return prev.concat([event]) return prev.concat([event]);
}, []) }, []),
) )
.replaceAll(/(?<!:)\"(?!,|})/g, '') .replaceAll(/(?<!:)\"(?!,|})/g, '')
.replace(/\"/g, "'") .replace(/\"/g, "'");
``` ```
## 部分功能演示 ## 部分功能演示
@ -101,8 +119,8 @@ JSON.stringify(
支持现代浏览器, 不支持 IE 支持现代浏览器, 不支持 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 | | [<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], 'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'], 'subject-empty': [2, 'never'],
'type-empty': [2, 'never'], 'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [ 'type-enum': [
2, 2,
'always', 'always',
@ -25,8 +26,8 @@ module.exports = {
'wip', 'wip',
'workflow', 'workflow',
'types', 'types',
'release' 'release',
] ],
] ],
} },
} };

44
components.d.ts vendored

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

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

@ -6,27 +6,27 @@
* @Description: axios简单的封装 * @Description: axios简单的封装
* @FilePath: \vite-vue3-lowcode\src\utils\http\request.ts * @FilePath: \vite-vue3-lowcode\src\utils\http\request.ts
*/ */
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig } from 'axios';
import qs from 'qs' import qs from 'qs';
import store from '@/store' // import store from '@/store'
import { Toast } from 'vant' import { Toast } from 'vant';
import router from '@/router' import router from '@/router';
import { ContentTypeEnum } from './httpEnum' import { ContentTypeEnum } from './httpEnum';
// create an axios instance // create an axios instance
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_API_URL as string, // url = base api url + request url baseURL: import.meta.env.VITE_API_URL as string, // url = base api url + request url
withCredentials: true, // send cookies when cross-domain requests withCredentials: true, // send cookies when cross-domain requests
timeout: 10000 // request timeout timeout: 10000, // request timeout
}) });
interface CustomAxiosRequestConfig extends AxiosRequestConfig { interface CustomAxiosRequestConfig extends AxiosRequestConfig {
hideLoading?: boolean hideLoading?: boolean;
} }
interface BaseResponse<T = any> { interface BaseResponse<T = any> {
code: number code: number;
data: T data: T;
msg: string msg: string;
} }
// request拦截器 request interceptor // request拦截器 request interceptor
@ -36,68 +36,67 @@ service.interceptors.request.use(
if (!config.hideLoading) { if (!config.hideLoading) {
// loading // loading
Toast.loading({ Toast.loading({
forbidClick: true forbidClick: true,
}) });
} }
console.log(store.getters, "store.getters['user']") // if (store.getters['user/token'] && config.headers) {
if (store.getters['user/token']) { // config.headers['X-Token'] = store.getters['user/token']
config.headers['X-Token'] = store.getters['user/token'] // }
} const contentType = config.headers?.['content-type'] || config.headers?.['Content-Type'];
const contentType = config.headers['content-type'] || config.headers['Content-Type'] const data = config.data;
const data = config.data
if (config.method?.toLocaleUpperCase() == 'POST' && data) { if (config.method?.toLocaleUpperCase() == 'POST' && data) {
if (ContentTypeEnum.FORM_DATA == contentType) { if (ContentTypeEnum.FORM_DATA == contentType) {
const fd = new FormData() const fd = new FormData();
Object.keys(data).forEach((key) => fd.append(key, data[key])) Object.keys(data).forEach((key) => fd.append(key, data[key]));
config.data = fd config.data = fd;
} else if (ContentTypeEnum.FORM_URLENCODED == contentType) { } else if (ContentTypeEnum.FORM_URLENCODED == contentType) {
config.data = qs.stringify(config.data) config.data = qs.stringify(config.data);
} }
} }
return config return config;
}, },
(error) => { (error) => {
// do something with request error // do something with request error
console.log(error) // for debug console.log(error); // for debug
return Promise.reject(error) return Promise.reject(error);
} },
) );
// respone拦截器 // respone拦截器
service.interceptors.response.use( service.interceptors.response.use(
(response) => { (response) => {
Toast.clear() Toast.clear();
const res = response.data const res = response.data;
if (res.code && res.code !== 0) { if (res.code && res.code !== 0) {
// 登录超时,重新登录 // 登录超时,重新登录
if (res.code === 401) { if (res.code === 401) {
// store.dispatch('FedLogOut').then(() => { // store.dispatch('FedLogOut').then(() => {
// location.reload() // location.reload()
// }) // })
router.replace('/error') router.replace('/error');
} else { } else {
Toast(res.msg || '服务器访问出错了~') Toast(res.msg || '服务器访问出错了~');
} }
return Promise.reject(res || 'error') return Promise.reject(res || 'error');
} else { } else {
return Promise.resolve(response) return Promise.resolve(response);
} }
}, },
(error: Error) => { (error: Error) => {
if (error.message?.includes('timeout')) { if (error.message?.includes('timeout')) {
Toast('请求超时!') Toast('请求超时!');
} }
console.log('err' + error) // for debug console.log('err' + error); // for debug
return Promise.reject(error) return Promise.reject(error);
} },
) );
const request = <T = any>(config: CustomAxiosRequestConfig): Promise<BaseResponse<T>> => { const request = <T = any>(config: CustomAxiosRequestConfig): Promise<BaseResponse<T>> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
service service
.request<BaseResponse<T>>(config) .request<BaseResponse<T>>(config)
.then((res) => resolve(res.data)) .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 { createApp } from 'vue';
import App from './App.vue' import App from './App.vue';
import { setupElementPlus } from './plugins/element-plus' import { setupElementPlus } from './plugins/element-plus';
import { setupVant } from './plugins/vant' import { setupVant } from './plugins/vant';
import 'normalize.css' import 'normalize.css';
import 'virtual:windi.css' import 'virtual:windi.css';
import 'virtual:windi-devtools' import 'virtual:windi-devtools';
import 'animate.css' import 'animate.css';
import router from './router/' import router from './router/';
import store from './store/' import { setupStore } from './store/';
const app = createApp(App) const app = createApp(App);
// 配置store
setupStore(app);
// 使用element-plus插件 // 使用element-plus插件
setupElementPlus(app) setupElementPlus(app);
// 使用vant插件 // 使用vant插件
setupVant(app) setupVant(app);
app.config.globalProperties.$$refs = {} app.config.globalProperties.$$refs = {};
// if (import.meta.env.DEV) { // 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: * @Description:
* @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\image\index.tsx * @FilePath: \vite-vue3-lowcode\src\packages\base-widgets\image\index.tsx
*/ */
import { Image } from 'vant' import { Image } from 'vant';
import { import {
createEditorInputProp, createEditorInputProp,
createEditorSelectProp, createEditorSelectProp,
createEditorSwitchProp createEditorSwitchProp,
} from '@/visual-editor/visual-editor.props' } from '@/visual-editor/visual-editor.props';
import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils' import type { VisualEditorComponent } from '@/visual-editor/visual-editor.utils';
import { useGlobalProperties } from '@/hooks/useGlobalProperties' import { useGlobalProperties } from '@/hooks/useGlobalProperties';
import { Picture } from '@element-plus/icons-vue';
export default { export default {
key: 'image', key: 'image',
@ -21,28 +22,30 @@ export default {
label: '图片', label: '图片',
resize: { resize: {
width: true, width: true,
height: true height: true,
}, },
preview: () => ( preview: () => (
<div style="text-align:center;"> <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"> <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>
</div> </div>
), ),
render: ({ props, block, styles }) => { render: ({ props, block, styles }) => {
const { registerRef } = useGlobalProperties() const { registerRef } = useGlobalProperties();
return () => ( return () => (
<div style={styles}> <div style={styles}>
<Image ref={(el) => registerRef(el, block._vid)} {...props} /> <Image ref={(el) => registerRef(el, block._vid)} {...props} />
</div> </div>
) );
}, },
props: { props: {
src: createEditorInputProp({ src: createEditorInputProp({
label: '图片链接', label: '图片链接',
defaultValue: 'https://img.yzcdn.cn/vant/cat.jpeg' defaultValue: 'https://img.yzcdn.cn/vant/cat.jpeg',
}), }),
width: createEditorInputProp({ label: '宽度', defaultValue: 100 }), width: createEditorInputProp({ label: '宽度', defaultValue: 100 }),
height: createEditorInputProp({ label: '高度', defaultValue: 100 }), height: createEditorInputProp({ label: '高度', defaultValue: 100 }),
@ -52,46 +55,46 @@ export default {
options: [ options: [
{ {
label: '保持宽高缩放图片,使图片的长边能完全显示出来', label: '保持宽高缩放图片,使图片的长边能完全显示出来',
value: 'contain' value: 'contain',
}, },
{ {
label: '保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边', label: '保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边',
value: 'cover' value: 'cover',
}, },
{ {
label: '拉伸图片,使图片填满元素', label: '拉伸图片,使图片填满元素',
value: 'fill' value: 'fill',
}, },
{ {
label: '保持图片原有尺寸', label: '保持图片原有尺寸',
value: 'none' value: 'none',
}, },
{ {
label: '取 none 或 contain 中较小的一个', label: '取 none 或 contain 中较小的一个',
value: 'scale-down' value: 'scale-down',
} },
], ],
defaultValue: 'fill' defaultValue: 'fill',
}), }),
iconPrefix: createEditorInputProp({ iconPrefix: createEditorInputProp({
label: '图标类名前缀', label: '图标类名前缀',
tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性' tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性',
}), }),
iconSize: createEditorInputProp({ label: '加载图标和失败图标的大小' }), iconSize: createEditorInputProp({ label: '加载图标和失败图标的大小' }),
lazyLoad: createEditorSwitchProp({ lazyLoad: createEditorSwitchProp({
label: '是否开启图片懒加载', label: '是否开启图片懒加载',
tips: '须配合 Lazyload 组件使用' tips: '须配合 Lazyload 组件使用',
}), }),
loadingIcon: createEditorInputProp({ label: '加载时提示的图标名称或图片链接' }), loadingIcon: createEditorInputProp({ label: '加载时提示的图标名称或图片链接' }),
radius: createEditorInputProp({ label: '圆角大小', tips: '默认单位为 px' }), radius: createEditorInputProp({ label: '圆角大小', tips: '默认单位为 px' }),
round: createEditorSwitchProp({ label: '是否显示为圆形' }), round: createEditorSwitchProp({ label: '是否显示为圆形' }),
'show-error': createEditorSwitchProp({ label: '是否展示图片加载失败提示' }), 'show-error': createEditorSwitchProp({ label: '是否展示图片加载失败提示' }),
'show-loading': createEditorSwitchProp({ label: '是否展示图片加载中提示' }), 'show-loading': createEditorSwitchProp({ label: '是否展示图片加载中提示' }),
alt: createEditorInputProp({ label: '替代文本' }) alt: createEditorInputProp({ label: '替代文本' }),
}, },
events: [ events: [
{ label: '点击图片时触发', value: 'click' }, { label: '点击图片时触发', value: 'click' },
{ label: '图片加载完毕时触发', value: 'load' }, { label: '图片加载完毕时触发', value: 'load' },
{ label: '图片加载失败时触发', value: 'error' } { label: '图片加载失败时触发', value: 'error' },
] ],
} as VisualEditorComponent } as VisualEditorComponent;

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

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

@ -1,5 +1,6 @@
import { App, Component } from 'vue' import { App, Component } from 'vue';
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css';
import { import {
ElAffix, ElAffix,
ElSkeleton, ElSkeleton,
@ -32,8 +33,8 @@ import {
ElAlert, ElAlert,
ElRadioButton, ElRadioButton,
ElRadioGroup, ElRadioGroup,
ElInfiniteScroll ElInfiniteScroll,
} from 'element-plus' } from 'element-plus';
const components = [ const components = [
ElAffix, ElAffix,
@ -65,16 +66,16 @@ const components = [
ElPagination, ElPagination,
ElAlert, ElAlert,
ElRadioButton, ElRadioButton,
ElRadioGroup ElRadioGroup,
] ];
const plugins = [ElLoading, ElInfiniteScroll] const plugins = [ElLoading, ElInfiniteScroll];
export function setupElementPlus(app: App) { export function setupElementPlus(app: App) {
components.forEach((component: Component) => { components.forEach((component: Component) => {
app.component(component.name!, component) app.component(component.name!, component);
}) });
plugins.forEach((plugin) => { 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 = { const store = createPinia();
count: 0
export function setupStore(app: App<Element>) {
app.use(store);
} }
// Create a new store instance. export { store };
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
}
}
})

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

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

@ -5,39 +5,51 @@
* @descriptiontools * @descriptiontools
* @update: 2021/5/7 10:46 * @update: 2021/5/7 10:46
*/ */
import { reactive } from 'vue' import { reactive } from 'vue';
import { ElMessage, ElRadio, ElRadioGroup } from 'element-plus' import { ElMessage, ElRadio, ElRadioGroup } from 'element-plus';
import { useQRCode } from '@vueuse/integrations' import { useQRCode } from '@vueuse/integrations';
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core';
import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData' import { useVisualData, localKey } from '@/visual-editor/hooks/useVisualData';
import { useModal } from '@/visual-editor/hooks/useModal' import { useModal } from '@/visual-editor/hooks/useModal';
import MonacoEditor from '@/visual-editor/components/common/monaco-editor/MonacoEditor' 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 = () => { export const useTools = () => {
const { jsonData, updatePage, currentPage, overrideProject } = useVisualData() const { jsonData, updatePage, currentPage, overrideProject } = useVisualData();
const state = reactive({ const state = reactive({
coverRadio: 'current', coverRadio: 'current',
importJsonValue: '' importJsonValue: '',
}) });
const importJsonChange = (value) => { const importJsonChange = (value) => {
state.importJsonValue = value state.importJsonValue = value;
} };
return [ return [
{ {
title: '导入JSON', title: '导入JSON',
icon: 'el-icon-upload2', icon: Upload,
onClick: () => { onClick: () => {
useModal({ useModal({
title: '导入JSON', title: '导入JSON',
props: { props: {
width: 642 width: 642,
}, },
content: () => ( content: () => (
<> <>
<ElRadioGroup v-model={state.coverRadio}> <ElRadioGroup v-model={state.coverRadio}>
<ElRadio label={'current'}></ElRadio> <ElRadio label="current"></ElRadio>
<ElRadio label={'all'}></ElRadio> <ElRadio label="all"></ElRadio>
</ElRadioGroup> </ElRadioGroup>
<MonacoEditor <MonacoEditor
onChange={importJsonChange} onChange={importJsonChange}
@ -47,119 +59,119 @@ export const useTools = () => {
</> </>
), ),
onConfirm: () => { onConfirm: () => {
const isCoverCurrent = state.coverRadio == 'current' const isCoverCurrent = state.coverRadio == 'current';
// 覆盖当前页面 // 覆盖当前页面
if (isCoverCurrent) { if (isCoverCurrent) {
updatePage({ updatePage({
oldPath: currentPage.value.path, oldPath: currentPage.value.path,
page: JSON.parse(state.importJsonValue) page: JSON.parse(state.importJsonValue),
}) });
} else { } else {
// 覆盖整个项目 // 覆盖整个项目
overrideProject(JSON.parse(state.importJsonValue)) overrideProject(JSON.parse(state.importJsonValue));
} }
ElMessage({ ElMessage({
showClose: true, showClose: true,
type: 'success', type: 'success',
duration: 2000, duration: 2000,
message: isCoverCurrent ? '成功覆盖当前页面' : '成功覆盖整个项目' message: isCoverCurrent ? '成功覆盖当前页面' : '成功覆盖整个项目',
}) });
} },
}) });
} },
}, },
{ {
title: '导出JSON', title: '导出JSON',
icon: 'el-icon-download', icon: Download,
onClick: () => { onClick: () => {
const { copy } = useClipboard({ source: JSON.stringify(jsonData) }) const { copy } = useClipboard({ source: JSON.stringify(jsonData) });
copy() copy()
.then(() => ElMessage.success('复制成功')) .then(() => ElMessage.success('复制成功'))
.catch((err) => ElMessage.error(`复制失败:${err}`)) .catch((err) => ElMessage.error(`复制失败:${err}`));
} },
}, },
{ {
title: '真机预览', title: '真机预览',
icon: 'el-icon-mobile-phone', icon: Cellphone,
onClick: () => { onClick: () => {
const qrcode = useQRCode(location.origin + '/preview') const qrcode = useQRCode(location.origin + '/preview');
useModal({ useModal({
title: '预览二维码(暂不可用)', title: '预览二维码(暂不可用)',
props: { props: {
width: 300 width: 300,
}, },
footer: null, footer: null,
content: () => ( content: () => (
<div class={'flex justify-center'}> <div class={'flex justify-center'}>
<img width={220} height={220} src={qrcode.value} /> <img width={220} height={220} src={qrcode.value} />
</div> </div>
) ),
}) });
} },
}, },
{ {
title: '复制页面', title: '复制页面',
icon: 'el-icon-document-copy', icon: DocumentCopy,
onClick: () => { onClick: () => {
ElMessage({ ElMessage({
showClose: true, showClose: true,
type: 'info', type: 'info',
duration: 2000, duration: 2000,
message: '敬请期待!' message: '敬请期待!',
}) });
} },
}, },
{ {
title: '撤销', title: '撤销',
icon: 'el-icon-refresh-left', icon: RefreshLeft,
onClick: () => { onClick: () => {
ElMessage({ ElMessage({
showClose: true, showClose: true,
type: 'info', type: 'info',
duration: 2000, duration: 2000,
message: '敬请期待!' message: '敬请期待!',
}) });
} },
}, },
{ {
title: '重做', title: '重做',
icon: 'el-icon-refresh-right', icon: RefreshRight,
onClick: () => { onClick: () => {
ElMessage({ ElMessage({
showClose: true, showClose: true,
type: 'info', type: 'info',
duration: 2000, duration: 2000,
message: '敬请期待!' message: '敬请期待!',
}) });
} },
}, },
{ {
title: '清空页面', title: '清空页面',
icon: 'el-icon-delete', icon: Delete,
onClick: () => { onClick: () => {
ElMessage({ ElMessage({
showClose: true, showClose: true,
type: 'info', type: 'info',
duration: 2000, duration: 2000,
message: '敬请期待!' message: '敬请期待!',
}) });
} },
}, },
{ {
title: '预览', title: '预览',
icon: 'el-icon-position', icon: Position,
onClick: () => { onClick: () => {
localStorage.setItem(localKey, JSON.stringify(jsonData)) localStorage.setItem(localKey, JSON.stringify(jsonData));
window.open(location.href.replace('/#/', '/preview/#/')) window.open(location.href.replace('/#/', '/preview/#/'));
} },
}, },
{ {
title: '反馈', title: '反馈',
icon: 'el-icon-chat-line-square', icon: ChatLineSquare,
onClick: () => { 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: * @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\base-widgets\index.tsx * @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\base-widgets\index.tsx
*/ */
import { defineComponent, ref } from 'vue' import { defineComponent, ref } from 'vue';
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash';
import { visualConfig } from '@/visual.config' import { visualConfig } from '@/visual.config';
import styles from './index.module.scss' import styles from './index.module.scss';
import { createNewBlock } from '@/visual-editor/visual-editor.utils' import { createNewBlock } from '@/visual-editor/visual-editor.utils';
import DraggableTransitionGroup from '@/visual-editor/components/simulator-editor/draggable-transition-group.vue' import DraggableTransitionGroup from '@/visual-editor/components/simulator-editor/draggable-transition-group.vue';
import { Edit } from '@element-plus/icons-vue';
export default defineComponent({ export default defineComponent({
name: 'BaseWidgets', name: 'BaseWidgets',
label: '基本组件', label: '基本组件',
order: 3, order: 3,
icon: 'el-icon-edit', icon: Edit,
setup() { setup() {
const baseWidgets = ref(visualConfig.componentModules.baseWidgets) const baseWidgets = ref(visualConfig.componentModules.baseWidgets);
const log = (evt) => { const log = (evt) => {
window.console.log('onChange:', evt) window.console.log('onChange:', evt);
} };
// 克隆组件 // 克隆组件
const cloneDog = (comp) => { const cloneDog = (comp) => {
console.log('当前拖拽的组件:', comp) console.log('当前拖拽的组件:', comp);
const newComp = cloneDeep(comp) const newComp = cloneDeep(comp);
return createNewBlock(newComp) return createNewBlock(newComp);
} };
return () => ( return () => (
<> <>
@ -46,10 +47,10 @@ export default defineComponent({
<div class={styles.listGroupItem} data-label={element.label}> <div class={styles.listGroupItem} data-label={element.label}>
{element.preview()} {element.preview()}
</div> </div>
) ),
}} }}
</DraggableTransitionGroup> </DraggableTransitionGroup>
</> </>
) );
} },
}) });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,25 +1,38 @@
module.exports = { module.exports = {
root: true, root: true,
plugins: ['stylelint-order', 'stylelint-scss'], plugins: ['stylelint-order'],
customSyntax: 'postcss-html',
extends: ['stylelint-config-standard', 'stylelint-config-prettier'], extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
rules: { rules: {
'selector-class-pattern': null,
'selector-pseudo-class-no-unknown': [ 'selector-pseudo-class-no-unknown': [
true, true,
{ {
ignorePseudoClasses: ['global', 'v-deep', 'deep'] ignorePseudoClasses: ['global'],
} },
], ],
'selector-pseudo-element-no-unknown': [ 'selector-pseudo-element-no-unknown': [
true, true,
{ {
ignorePseudoElements: ['v-deep', 'deep'] ignorePseudoElements: ['v-deep'],
} },
], ],
'at-rule-no-unknown': [ 'at-rule-no-unknown': [
true, true,
{ {
ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'for'] ignoreAtRules: [
} 'tailwind',
'apply',
'variants',
'responsive',
'screen',
'function',
'if',
'each',
'include',
'mixin',
],
},
], ],
'no-empty-source': null, 'no-empty-source': null,
'named-grid-areas-no-invalid': null, 'named-grid-areas-no-invalid': null,
@ -28,12 +41,12 @@ module.exports = {
'font-family-no-missing-generic-family-keyword': null, 'font-family-no-missing-generic-family-keyword': null,
'declaration-colon-space-after': 'always-single-line', 'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never', 'declaration-colon-space-before': 'never',
'declaration-block-trailing-semicolon': ['always', { ignore: ['single-declaration'] }], // 'declaration-block-trailing-semicolon': 'always',
'rule-empty-line-before': [ 'rule-empty-line-before': [
'always', 'always',
{ {
ignore: ['after-comment', 'first-nested'] ignore: ['after-comment', 'first-nested'],
} },
], ],
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }], 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
'order/order': [ 'order/order': [
@ -44,165 +57,37 @@ module.exports = {
'declarations', 'declarations',
{ {
type: 'at-rule', type: 'at-rule',
name: 'supports' name: 'supports',
}, },
{ {
type: 'at-rule', 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, "removeComments": false,
"strictFunctionTypes": false, "strictFunctionTypes": false,
"baseUrl": ".", "baseUrl": ".",
"types": ["vite/client", "node"], "types": [
"typeRoots": ["./node_modules/@types/", "./types"], "vite/client",
"node",
"element-plus/global"
],
"typeRoots": [
"./node_modules/@types/",
"./types"
],
"paths": { "paths": {
"@/*": ["src/*"] "@/*": [
"src/*"
]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"] "lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
@ -43,5 +57,8 @@
"preview/**/*.tsx", "preview/**/*.tsx",
"preview/**/*.vue" "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' { declare module '*.bmp' {
const src: string const src: string;
export default src export default src;
} }
declare module '*.gif' { declare module '*.gif' {
const src: string const src: string;
export default src export default src;
} }
declare module '*.jpg' { declare module '*.jpg' {
const src: string const src: string;
export default src export default src;
} }
declare module '*.jpeg' { declare module '*.jpeg' {
const src: string const src: string;
export default src export default src;
} }
declare module '*.png' { declare module '*.png' {
const src: string const src: string;
export default src export default src;
} }
declare module '*.webp' { declare module '*.webp' {
const src: string const src: string;
export default src export default src;
} }
declare module '*.module.css' { declare module '*.module.css' {
const classes: { readonly [key: string]: string } const classes: { readonly [key: string]: string };
export default classes export default classes;
} }
declare module '*.module.scss' { declare module '*.module.scss' {
const classes: { readonly [key: string]: string } const classes: { readonly [key: string]: string };
export default classes export default classes;
} }
declare module '*.module.less' { declare module '*.module.less' {
const classes: { readonly [key: string]: string } const classes: { readonly [key: string]: string };
export default classes export default classes;
} }
declare module '*.module.sass' { declare module '*.module.sass' {
const classes: { readonly [key: string]: string } const classes: { readonly [key: string]: string };
export default classes export default classes;
} }
declare module 'moment/locale/*' { declare module 'moment/locale/*' {
const LocaleMessage: { [key: string]: any } const LocaleMessage: { [key: string]: any };
export default LocaleMessage export default LocaleMessage;
} }
declare module 'ant-design-vue/es/locale-provider/*' { declare module 'ant-design-vue/es/locale-provider/*' {
const LocaleMessage: { [key: string]: any } const LocaleMessage: { [key: string]: any };
export default LocaleMessage export default LocaleMessage;
} }
// ant-design-vue/es/base // ant-design-vue/es/base
declare module 'ant-design-vue/es/base' { declare module 'ant-design-vue/es/base' {
class 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' { declare module '*.vue' {
import { defineComponent } from 'vue' import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent> const component: ReturnType<typeof defineComponent>;
export default component export default component;
} }
// declare module '*.vue' { // declare module '*.vue' {
@ -10,15 +10,16 @@ declare module '*.vue' {
// } // }
// declare module '@vue/runtime-core' { // declare module '@vue/runtime-core' {
// interface ComponentCustomProperties { // export interface ComponentCustomProperties {
// $createLoading: () => any // $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> = { 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 { ComponentCustomProperties } from 'vue';
import { Store } from 'vuex' import { Store } from 'vuex';
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
// declare your own store states // declare your own store states
interface State { interface State {
count: number count: number;
} }
// provide typings for `this.$store` // provide typings for `this.$store`
interface ComponentCustomProperties { interface ComponentCustomProperties {
$store: Store<State> $store: Store<State>;
} }
} }
export {}

@ -1,59 +1,91 @@
import { ConfigEnv, loadEnv, UserConfig } from 'vite' import { ConfigEnv, loadEnv, UserConfig } from 'vite';
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx';
import legacy from '@vitejs/plugin-legacy' import legacy from '@vitejs/plugin-legacy';
import { resolve } from 'path' import { resolve } from 'path';
import ViteComponents, { ElementPlusResolver, VantResolver } from 'vite-plugin-components' import { ElementPlusResolver, VantResolver } from 'unplugin-vue-components/resolvers';
import WindiCSS from 'vite-plugin-windicss' import AutoImport from 'unplugin-auto-import/vite';
import VitePluginElementPlus from 'vite-plugin-element-plus' 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/ // https://cn.vitejs.dev/config/
export default ({ mode }: ConfigEnv): UserConfig => { export default ({ mode }: ConfigEnv): UserConfig => {
// 环境变量 // 环境变量
const { VITE_BASE_URL } = loadEnv(mode, CWD) const { VITE_BASE_URL } = loadEnv(mode, CWD);
return { return {
base: VITE_BASE_URL, // 设置打包路径 base: VITE_BASE_URL, // 设置打包路径
css: { css: {
modules: { 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: [ plugins: [
vue(), vue(),
vueJsx(), vueJsx(),
WindiCSS(), WindiCSS(),
legacy({ legacy({
targets: ['defaults', 'not IE 11'] targets: ['defaults', 'not IE 11'],
}),
AutoImport({
resolvers: [ElementPlusResolver(), VantResolver()],
}), }),
ViteComponents({ Components({
globalComponentsDeclaration: true, resolvers: [ElementPlusResolver(), VantResolver()],
// 自动导入组件(还不够完善,可能会有样式丢失)
// valid file extensions for components.
extensions: ['vue', 'tsx', 'js'],
customComponentResolvers: [ElementPlusResolver(), VantResolver()]
}), }),
VitePluginElementPlus({
// 如果你需要使用 [component name].scss 源文件,你需要把下面的注释取消掉。
// 对于所有的 API 你可以参考 https://github.com/element-plus/vite-plugin-element-plus
// 的文档注释
// useSource: true
format: mode === 'development' ? 'esm' : 'cjs'
})
], ],
resolve: { resolve: {
alias: { alias: [
'@': resolve(__dirname, 'src') // 设置 `@` 指向 `src` 目录 {
} find: '@',
replacement: resolve(__dirname, './src'),
},
],
}, },
build: { 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: { rollupOptions: {
input: { input: {
main: resolve(__dirname, 'index.html'), main: resolve(__dirname, 'index.html'),
preview: resolve(__dirname, 'preview/index.html') preview: resolve(__dirname, 'preview/index.html'),
}, },
output: { output: {
manualChunks: { manualChunks: {
@ -61,22 +93,13 @@ export default ({ mode }: ConfigEnv): UserConfig => {
cssWorker: [`${prefix}/language/css/css.worker`], cssWorker: [`${prefix}/language/css/css.worker`],
htmlWorker: [`${prefix}/language/html/html.worker`], htmlWorker: [`${prefix}/language/html/html.worker`],
tsWorker: [`${prefix}/language/typescript/ts.worker`], tsWorker: [`${prefix}/language/typescript/ts.worker`],
editorWorker: [`${prefix}/editor/editor.worker`] editorWorker: [`${prefix}/editor/editor.worker`],
} },
} },
} },
}, },
optimizeDeps: { optimizeDeps: {
include: [ include: ['@vueuse/core', 'element-plus', 'vant', 'lodash', 'vuedraggable'],
'vue',
'vue-router',
'@vueuse/core',
'element-plus',
'vant',
'lodash',
'vuedraggable'
],
exclude: ['vue-demi']
}, },
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
@ -90,9 +113,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
target: 'http://29135jo738.zicp.vip/api/v1', target: 'http://29135jo738.zicp.vip/api/v1',
changeOrigin: true, changeOrigin: true,
secure: false, 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