ソースを参照

初始化

develop
コミット
892c7cd66b
100個のファイルの変更5217行の追加0行の削除
  1. +18
    -0
      .env
  2. +8
    -0
      .env.development
  3. +8
    -0
      .env.test
  4. +63
    -0
      .gitattributes
  5. +20
    -0
      .gitignore
  6. +4
    -0
      .prettierrc.json
  7. +28
    -0
      .project
  8. +21
    -0
      LICENSE
  9. +53
    -0
      README.en-US.md
  10. +13
    -0
      babel.config.js
  11. +101
    -0
      package.json
  12. バイナリ
      public/favicon.ico
  13. +33
    -0
      public/index.html
  14. バイナリ
      public/static/cnc.xlsx
  15. バイナリ
      public/static/font/HarmonyOS_Sans_SC_Regular.ttf
  16. +1
    -0
      public/static/iconfont.js
  17. バイナリ
      public/static/lingjian.xlsx
  18. バイナリ
      public/static/peijian.xlsx
  19. バイナリ
      public/static/template/PCB模板.xlsx
  20. バイナリ
      public/static/template/元器件模板.xlsx
  21. バイナリ
      public/static/template/包材件模板.xlsx
  22. バイナリ
      public/static/template/模具模板.xlsx
  23. バイナリ
      public/static/template/结构件模板.xlsx
  24. バイナリ
      public/static/template/辅材件模板.xlsx
  25. +117
    -0
      src/App.vue
  26. バイナリ
      src/assets/img/bomIcon/3d.png
  27. バイナリ
      src/assets/img/bomIcon/accessories.png
  28. バイナリ
      src/assets/img/bomIcon/assembling.png
  29. バイナリ
      src/assets/img/bomIcon/cnc.png
  30. バイナリ
      src/assets/img/bomIcon/electronicsBOM.png
  31. バイナリ
      src/assets/img/bomIcon/injectMould.png
  32. バイナリ
      src/assets/img/bomIcon/mould.png
  33. バイナリ
      src/assets/img/bomIcon/packingMaterial.png
  34. バイナリ
      src/assets/img/bomIcon/pcb.png
  35. バイナリ
      src/assets/img/bomIcon/pcba.png
  36. バイナリ
      src/assets/img/bomIcon/smt.png
  37. バイナリ
      src/assets/img/bomIcon/structure.png
  38. +1
    -0
      src/assets/img/cancel.svg
  39. バイナリ
      src/assets/img/detailIcon/dom.png
  40. バイナリ
      src/assets/img/detailIcon/icon1.png
  41. バイナリ
      src/assets/img/detailIcon/icon2.png
  42. バイナリ
      src/assets/img/developing.png
  43. バイナリ
      src/assets/img/icon-date.png
  44. バイナリ
      src/assets/img/icon-notice.png
  45. バイナリ
      src/assets/img/icon-reupload.png
  46. バイナリ
      src/assets/img/icon-upload.png
  47. バイナリ
      src/assets/img/icon-wait.png
  48. バイナリ
      src/assets/img/loginbg.jpg
  49. バイナリ
      src/assets/img/loginbg.png
  50. バイナリ
      src/assets/img/loginlogo.png
  51. バイナリ
      src/assets/img/logo.png
  52. バイナリ
      src/assets/img/logo1.png
  53. バイナリ
      src/assets/img/logo2.png
  54. バイナリ
      src/assets/img/preview-nine.png
  55. バイナリ
      src/assets/img/preview.png
  56. +25
    -0
      src/bootstrap.js
  57. +118
    -0
      src/components/address/AddressAdd.vue
  58. +193
    -0
      src/components/address/AddressList.vue
  59. +172
    -0
      src/components/cache/AKeepAlive.js
  60. +78
    -0
      src/components/card/ChartCard.vue
  61. +59
    -0
      src/components/chart/Bar.vue
  62. +67
    -0
      src/components/chart/MiniArea.vue
  63. +59
    -0
      src/components/chart/MiniBar.vue
  64. +56
    -0
      src/components/chart/MiniProgress.vue
  65. +80
    -0
      src/components/chart/Radar.vue
  66. +59
    -0
      src/components/chart/RankingList.vue
  67. +79
    -0
      src/components/chart/Trend.vue
  68. +9
    -0
      src/components/chart/index.less
  69. +157
    -0
      src/components/checkbox/ColorCheckbox.vue
  70. +161
    -0
      src/components/checkbox/ImgCheckbox.vue
  71. +7
    -0
      src/components/checkbox/index.js
  72. +46
    -0
      src/components/customer/AreaSelect.vue
  73. +64
    -0
      src/components/customer/ButtonBox.vue
  74. +107
    -0
      src/components/customer/CustomerBusinessModal.vue
  75. +70
    -0
      src/components/customer/CustomerIntroduce.vue
  76. +173
    -0
      src/components/customer/FileList.vue
  77. +216
    -0
      src/components/customer/OfferFileList.vue
  78. +100
    -0
      src/components/customer/OpenCloseBox.vue
  79. +65
    -0
      src/components/customer/StringSubstr.vue
  80. +175
    -0
      src/components/dragModal/index.vue
  81. +30
    -0
      src/components/dragModal/props.js
  82. +190
    -0
      src/components/drawer/CustomDrawer.vue
  83. +159
    -0
      src/components/drawer/FileListDrawer.vue
  84. +69
    -0
      src/components/exception/ExceptionPage.vue
  85. +19
    -0
      src/components/exception/typeConfig.js
  86. +130
    -0
      src/components/form-builder/Form.vue
  87. +495
    -0
      src/components/form-builder/FormBuilder.vue
  88. +96
    -0
      src/components/form-builder/form-builder-modal/FormConfigData.vue
  89. +121
    -0
      src/components/form-builder/form-builder-modal/GlobalSetting.vue
  90. +152
    -0
      src/components/form-builder/form-builder-modal/PreviewModal.vue
  91. +251
    -0
      src/components/form-builder/form-editor/FormGridEditor.vue
  92. +55
    -0
      src/components/form-builder/form-editor/FormItemEditor.vue
  93. +0
    -0
      src/components/form-builder/form-item/button/Button.vue
  94. +93
    -0
      src/components/form-builder/form-item/checkbox/Checkbox.vue
  95. +118
    -0
      src/components/form-builder/form-item/checkbox/config.js
  96. +55
    -0
      src/components/form-builder/form-item/common.config.js
  97. +55
    -0
      src/components/form-builder/form-item/date-range/DateRange.vue
  98. +98
    -0
      src/components/form-builder/form-item/date-range/config.js
  99. +49
    -0
      src/components/form-builder/form-item/date-time/DateTime.vue
  100. +98
    -0
      src/components/form-builder/form-item/date-time/config.js

+ 18
- 0
.env ファイルの表示

@@ -0,0 +1,18 @@
VUE_APP_PUBLIC_PATH=/
VUE_APP_NAME=捷配管理后台
VUE_APP_ROUTES_KEY=admin.routes
VUE_APP_PERMISSIONS_KEY=admin.permissions
VUE_APP_ROLES_KEY=admin.roles
VUE_APP_USER_KEY=admin.user
VUE_APP_SETTING_KEY=admin.setting
VUE_APP_TBAS_KEY=admin.tabs
VUE_APP_TBAS_TITLES_KEY=admin.tabs.titles

# api服务的baseUrls
VUE_APP_API_IDS=https://ids.justfit.com
VUE_APP_API_BASE_URL_API=https://agentapi.justfit.com
VUE_APP_API_COMMON_BASE_URL=https://api.allpcb.com
VUE_APP_API_OSS_BASE_URL=https://member.justfit.com
VUE_APP_API_ALLPCB_BASE_URL=https://www.allpcb.com

VUE_APP_YFZX_CSZ=115ecffc-261a-464d-c9c4-3a0343848dd6

+ 8
- 0
.env.development ファイルの表示

@@ -0,0 +1,8 @@
# webpack devServer的proxy配置
VUE_APP_API_DEV_SERVER_PROXY_ENABLED=true
VUE_APP_API_DEV_SERVER_PROXY_BASE=/proxy

VUE_APP_API_IDS=/ids
VUE_APP_API_BASE_URL_API=/
VUE_APP_API_OFFER=/
VUE_APP_YFZX_CSZ=de89fee0-d722-9d95-9520-3a00f6ff7b35

+ 8
- 0
.env.test ファイルの表示

@@ -0,0 +1,8 @@
NODE_ENV=production

# api服务的baseUrls
VUE_APP_API_IDS=http://192.168.19.4:44330
VUE_APP_API_BASE_URL_API=http://192.168.19.4:44332


VUE_APP_YFZX_CSZ=de89fee0-d722-9d95-9520-3a00f6ff7b35

+ 63
- 0
.gitattributes ファイルの表示

@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary

###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

+ 20
- 0
.gitignore ファイルの表示

@@ -0,0 +1,20 @@
.DS_Store
node_modules/
dist/
admindb/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test/unit/coverage/
/test/e2e/reports/
selenium-debug.log

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
package-lock.json
.env.production.local

+ 4
- 0
.prettierrc.json ファイルの表示

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"semi": false
}

+ 28
- 0
.project ファイルの表示

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>html</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.aptana.ide.core.unifiedBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.aptana.projects.webnature</nature>
</natures>
<filteredResources>
<filter>
<id>1586918228372</id>
<name></name>
<type>26</type>
<matcher>
<id>org.eclipse.ui.ide.multiFilter</id>
<arguments>1.0-name-matches-false-false-node_modules</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

+ 21
- 0
LICENSE ファイルの表示

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 iczer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 53
- 0
README.en-US.md ファイルの表示

@@ -0,0 +1,53 @@
[简体中文](./README.md) | English
<h1 align="center">Vue Antd Admin</h1>

<div align="center">
[Ant Design Pro](https://github.com/ant-design/ant-design-pro)'s implementation with Vue.
An out-of-box UI solution for enterprise applications as a React boilerplate.

[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)
[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)
![image](./src/assets/img/preview.png)

Multiple theme modes available:
![image](./src/assets/img/preview-nine.png)
</div>

- Preview:https://iczer.gitee.io/vue-antd-admin
- Documentation:https://iczer.gitee.io/vue-antd-admin-docs
- FAQ:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html
- Mirror Repo in China:https://gitee.com/iczer/vue-antd-admin

## Browsers support
Modern browsers and IE10.

| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / 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/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |

## Usage
### clone
```bash
$ git clone https://github.com/iczer/vue-antd-admin.git
```
### yarn
```bash
$ yarn install
$ yarn serve
```
### or npm
```
$ npm install
$ npm run serve
```
More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs).

## Contributing
Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2::
- Use Vue Antd Admin in your daily work.
- Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions.
- Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code.
- Join the community and share your experiences with us. QQ Group: 812277510、610090280(已满)

+ 13
- 0
babel.config.js ファイルの表示

@@ -0,0 +1,13 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)

const plugins = []
if (IS_PROD) {
plugins.push('transform-remove-console')
}

module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins
}

+ 101
- 0
package.json ファイルの表示

@@ -0,0 +1,101 @@
{
"name": "vue-antd-admin",
"version": "0.7.2",
"homepage": "https://iczer.github.io/vue-antd-admin",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --dest dist_捷配后台_生产",
"build:test": "vue-cli-service build --mode test --dest dist_捷配后台_测试",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@antv/data-set": "^0.11.4",
"@antv/x6": "^1.29.0",
"animate.css": "^4.1.0",
"ant-design-vue": "1.7.2",
"axios": "^0.21.1",
"clipboard": "^2.0.6",
"codemirror": "^5.55.0",
"core-js": "^3.6.5",
"date-fns": "^2.14.0",
"echarts": "^5.3.2",
"enquire.js": "^2.1.6",
"highlight.js": "^10.2.1",
"js-cookie": "^2.2.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"plupload": "^2.3.7",
"qrcodejs2": "^0.0.2",
"register-service-worker": "^1.7.1",
"smooth-scrollbar": "^8.5.2",
"sortablejs": "^1.10.2",
"uuid": "^8.3.2",
"v-viewer": "^1.6.4",
"viser-vue": "^2.4.8",
"vue": "^2.6.11",
"vue-clipboard2": "^0.3.3",
"vue-count-to": "^1.0.13",
"vue-i18n": "^8.18.2",
"vue-quill-editor": "^3.0.6",
"vue-router": "^3.3.4",
"vuedraggable": "^2.23.2",
"vuex": "^3.4.0"
},
"devDependencies": {
"@ant-design/colors": "^4.0.1",
"@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-service": "^4.4.0",
"@vuepress/plugin-back-to-top": "^1.5.2",
"babel-eslint": "^8.0.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"babel-polyfill": "^6.26.0",
"compression-webpack-plugin": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"fast-deep-equal": "^3.1.3",
"gh-pages": "^3.1.0",
"less-loader": "^6.1.1",
"style-resources-loader": "^1.3.2",
"vue-cli-plugin-style-resources-loader": "^0.1.4",
"vue-template-compiler": "^2.6.11",
"vuepress": "^1.5.2",
"webpack-theme-color-replacer": "^1.3.12",
"whatwg-fetch": "^3.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"no-unused-vars": 0,
"no-undef": 1,
"no-irregular-whitespace": 1,
"no-multiple-empty-lines": [
1,
{
"max": 1
}
],
"indent": [
2,
2
]
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
}

バイナリ
public/favicon.ico ファイルの表示

変更前 変更後
幅: 64  |  高さ: 64  |  サイズ: 1.2 KiB

+ 33
- 0
public/index.html ファイルの表示

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en" class="beauty-scroll">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= process.env.VUE_APP_NAME %>
</title>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
<script src="./static/iconfont.js"></script>
</head>

<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="popContainer" class="beauty-scroll" style="height: 100vh; overflow-y: scroll;background-color: #f0f2f5;">
<div id="app"></div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>

</html>

バイナリ
public/static/cnc.xlsx ファイルの表示


バイナリ
public/static/font/HarmonyOS_Sans_SC_Regular.ttf ファイルの表示


+ 1
- 0
public/static/iconfont.js
ファイル差分が大きすぎるため省略します
ファイルの表示


バイナリ
public/static/lingjian.xlsx ファイルの表示


バイナリ
public/static/peijian.xlsx ファイルの表示


バイナリ
public/static/template/PCB模板.xlsx ファイルの表示


バイナリ
public/static/template/元器件模板.xlsx ファイルの表示


バイナリ
public/static/template/包材件模板.xlsx ファイルの表示


バイナリ
public/static/template/模具模板.xlsx ファイルの表示


バイナリ
public/static/template/结构件模板.xlsx ファイルの表示


バイナリ
public/static/template/辅材件模板.xlsx ファイルの表示


+ 117
- 0
src/App.vue ファイルの表示

@@ -0,0 +1,117 @@
<template>
<a-config-provider :locale="locale" :get-popup-container="popContainer">
<router-view/>
</a-config-provider>
</template>

<script>
import { enquireScreen } from './utils/util'
import {mapState, mapMutations} from 'vuex'
import themeUtil from '@/utils/themeUtil';
import {getI18nKey} from '@/utils/routerUtil'
import {checkAuthorization} from '@/utils/request';

export default {
name: 'App',
data() {
return {
locale: {}
}
},
created () {
this.setHtmlTitle()
this.setLanguage(this.lang)
enquireScreen(isMobile => this.setDevice(isMobile))
window.SYS_CONFIG = {
OSS_Extranet_URL: {
Name: 'oss外网访问地址',
Code: 'OSS_Extranet_URL',
Value: 'http://192.168.16.100:9015'
},
}
},
mounted() {
this.setWeekModeTheme(this.weekMode)
/* if (checkAuthorization()) {
this.$store.dispatch('account/refreshPermissions');
if (this.$route.path == '/loginjmp') {
location.href = '/#/main';
}
} else {
this.$router.push('/loginOA');
}*/
window.addEventListener('scroll', this.handleScroll, true);
},
watch: {
weekMode(val) {
this.setWeekModeTheme(val)
},
lang(val) {
this.setLanguage(val)
this.setHtmlTitle()
},
$route() {
this.setHtmlTitle()
},
'theme.mode': function(val) {
let closeMessage = this.$message.loading(`您选择了主题模式 ${val}, 正在切换...`)
themeUtil.changeThemeColor(this.theme.color, val).then(closeMessage)
},
'theme.color': function(val) {
let closeMessage = this.$message.loading(`您选择了主题色 ${val}, 正在切换...`)
themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage)
},
'layout': function() {
window.dispatchEvent(new Event('resize'))
}
},
computed: {
...mapState('setting', ['layout', 'theme', 'weekMode', 'lang'])
},
methods: {
...mapMutations('setting', ['setDevice']),
handleScroll() {
const dom = document.getElementById('popContainer');
let scrollTop = dom.scrollTop;
this.$bus.$emit('scrollTop', scrollTop);
},
setWeekModeTheme(weekMode) {
if (weekMode) {
document.body.classList.add('week-mode')
} else {
document.body.classList.remove('week-mode')
}
},
setLanguage(lang) {
this.$i18n.locale = lang
switch (lang) {
case 'CN':
this.locale = require('ant-design-vue/es/locale-provider/zh_CN').default
break
case 'HK':
this.locale = require('ant-design-vue/es/locale-provider/zh_TW').default
break
case 'US':
default:
this.locale = require('ant-design-vue/es/locale-provider/en_US').default
break
}
},
setHtmlTitle() {
const route = this.$route
const key = route.path === '/' ? 'home.name' : getI18nKey(route.matched[route.matched.length - 1].path)
document.title = process.env.VUE_APP_NAME + ' | ' + this.$t(key)
},
popContainer() {
return document.getElementById('popContainer')
}
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll)
}
}
</script>

<style lang="less" scoped>

</style>

バイナリ
src/assets/img/bomIcon/3d.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 9.7 KiB

バイナリ
src/assets/img/bomIcon/accessories.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 12 KiB

バイナリ
src/assets/img/bomIcon/assembling.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 31 KiB

バイナリ
src/assets/img/bomIcon/cnc.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 9.7 KiB

バイナリ
src/assets/img/bomIcon/electronicsBOM.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 7.3 KiB

バイナリ
src/assets/img/bomIcon/injectMould.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 11 KiB

バイナリ
src/assets/img/bomIcon/mould.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 10 KiB

バイナリ
src/assets/img/bomIcon/packingMaterial.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 4.3 KiB

バイナリ
src/assets/img/bomIcon/pcb.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 20 KiB

バイナリ
src/assets/img/bomIcon/pcba.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 14 KiB

バイナリ
src/assets/img/bomIcon/smt.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 34 KiB

バイナリ
src/assets/img/bomIcon/structure.png ファイルの表示

変更前 変更後
幅: 120  |  高さ: 120  |  サイズ: 13 KiB

+ 1
- 0
src/assets/img/cancel.svg
ファイル差分が大きすぎるため省略します
ファイルの表示


バイナリ
src/assets/img/detailIcon/dom.png ファイルの表示

変更前 変更後
幅: 55  |  高さ: 49  |  サイズ: 608 B

バイナリ
src/assets/img/detailIcon/icon1.png ファイルの表示

変更前 変更後
幅: 56  |  高さ: 56  |  サイズ: 1.5 KiB

バイナリ
src/assets/img/detailIcon/icon2.png ファイルの表示

変更前 変更後
幅: 56  |  高さ: 56  |  サイズ: 1.6 KiB

バイナリ
src/assets/img/developing.png ファイルの表示

変更前 変更後
幅: 449  |  高さ: 462  |  サイズ: 65 KiB

バイナリ
src/assets/img/icon-date.png ファイルの表示

変更前 変更後
幅: 32  |  高さ: 32  |  サイズ: 990 B

バイナリ
src/assets/img/icon-notice.png ファイルの表示

変更前 変更後
幅: 32  |  高さ: 32  |  サイズ: 865 B

バイナリ
src/assets/img/icon-reupload.png ファイルの表示

変更前 変更後
幅: 200  |  高さ: 200  |  サイズ: 5.2 KiB

バイナリ
src/assets/img/icon-upload.png ファイルの表示

変更前 変更後
幅: 216  |  高さ: 200  |  サイズ: 3.8 KiB

バイナリ
src/assets/img/icon-wait.png ファイルの表示

変更前 変更後
幅: 32  |  高さ: 32  |  サイズ: 888 B

バイナリ
src/assets/img/loginbg.jpg ファイルの表示

変更前 変更後
幅: 1920  |  高さ: 1080  |  サイズ: 1.1 MiB

バイナリ
src/assets/img/loginbg.png ファイルの表示

変更前 変更後
幅: 1920  |  高さ: 1080  |  サイズ: 47 KiB

バイナリ
src/assets/img/loginlogo.png ファイルの表示

変更前 変更後
幅: 178  |  高さ: 48  |  サイズ: 8.3 KiB

バイナリ
src/assets/img/logo.png ファイルの表示

変更前 変更後
幅: 296  |  高さ: 28  |  サイズ: 4.0 KiB

バイナリ
src/assets/img/logo1.png ファイルの表示

変更前 変更後
幅: 400  |  高さ: 345  |  サイズ: 26 KiB

バイナリ
src/assets/img/logo2.png ファイルの表示

変更前 変更後
幅: 166  |  高さ: 26  |  サイズ: 3.4 KiB

バイナリ
src/assets/img/preview-nine.png ファイルの表示

変更前 変更後
幅: 1882  |  高さ: 1112  |  サイズ: 131 KiB

バイナリ
src/assets/img/preview.png ファイルの表示

変更前 変更後
幅: 1882  |  高さ: 1112  |  サイズ: 70 KiB

+ 25
- 0
src/bootstrap.js ファイルの表示

@@ -0,0 +1,25 @@
import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil'
import {loadInterceptors} from '@/utils/request'
import guards from '@/router/guards'
import interceptors from '@/utils/axios-interceptors'

/**
* 启动引导方法
* 应用启动时需要执行的操作放在这里
* @param router 应用的路由实例
* @param store 应用的 vuex.store 实例
* @param i18n 应用的 vue-i18n 实例
* @param i18n 应用的 message 实例
*/
function bootstrap({router, store, i18n, message}) {
// 设置应用配置
setAppOptions({router, store, i18n})
// 加载 axios 拦截器
loadInterceptors(interceptors, {router, store, i18n, message})
// 加载路由
loadRoutes()
// 加载路由守卫
loadGuards(guards, {router, store, i18n, message})
}

export default bootstrap

+ 118
- 0
src/components/address/AddressAdd.vue ファイルの表示

@@ -0,0 +1,118 @@
<template>
<CustomDrawer ref="addPayType" title="新增收货地址" :destroyOnClose="true">
<a-form-model ref="ruleForm" :model="form" :rules="rules" layout="vertical">
<a-form-model-item label="联系人" prop="linkName">
<a-input v-model="form.linkName" />
</a-form-model-item>
<a-form-model-item label="联系电话" prop="linkPhone">
<a-input v-model="form.linkPhone" />
</a-form-model-item>
<a-form-model-item label="收货地址" prop="address">
<AreaSelect :areaData="form.address" ref="AreaSelect" @ok="getAddress" />
</a-form-model-item>
<a-form-model-item label="详细地址" prop="detailAddress">
<a-textarea
v-model="form.detailAddress"
placeholder="请输入详细地址"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-model-item>
<a-form-model-item label="默认使用">
<a-checkbox v-model="form.isEnable">默认使用</a-checkbox>
</a-form-model-item>
</a-form-model>
<a-space :size="10">
<a-button type="primary" :loading="loading" @click="handleSave()">确认</a-button>
<a-button @click="$refs.addPayType.hideDrawer()">取消</a-button>
</a-space>
</CustomDrawer>
</template>

<script>
import { getProjectPaymentType } from '@/services/dropDown';
import { addAddress } from '@/services/fileManagement/address';
import AreaSelect from '@/components/customer/AreaSelect';
export default {
components: {
AreaSelect
},
data() {
return {
form: {},
rules: {
linkName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
linkPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
address: [{ required: true, message: '请选择收货地址', trigger: 'change' }],
detailAddress: [{ required: true, message: '请输入详细地址', trigger: 'blur' }],
},
paymentTypes: [],
companyId: '',
loading: false,
selectedOptions: []
};
},
methods: {
showDrawer(companyId) {
this.companyId = companyId;
this.form = {
isEnable: true,
address: []
};
this.getDropDown();
this.$refs.addPayType.showDrawer();
},
getDropDown() {
getProjectPaymentType()
.then(res => {
const arr = [];
for (let i in res) {
arr.push({
type: Number(i),
name: res[i]
})
}
this.paymentTypes = arr;
})
},
getAddress(value, selectedOptions) {
this.form.address = value;
this.selectedOptions = selectedOptions;
},
handleSave() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
const params = {
companyId: this.companyId,
linkName: this.form.linkName,
linkPhone: this.form.linkPhone,
provinceCode: this.selectedOptions[0].code,
cityCode: this.selectedOptions[1].code,
province: this.selectedOptions[0].name,
city: this.selectedOptions[1].name,
detailAddress: this.form.detailAddress,
isDefault: this.form.isEnable
}
this.loading = true;
addAddress(params)
.then(res => {
const data = {
update: false,
id: res.id
};
if (this.form.isEnable) {
data.update = true
}
this.$emit('ok', data);
this.$refs.addPayType.hideDrawer();
})
.finally(() => {
this.loading = false;
});
}
});
},
},
};
</script>

<style></style>

+ 193
- 0
src/components/address/AddressList.vue ファイルの表示

@@ -0,0 +1,193 @@
<template>
<CustomDrawer ref="addressList" title="选择收货地址" :destroyOnClose="true">
<a-card
v-for="(type, index) in addressList"
:key="index"
:class="type.isDefault && model == 1 ? 'cardCls active' : 'cardCls'"
>
<a-row class="rowCls">
<a-col :span="12">
<a-row>
<a-col :span="10">联系人:</a-col>
<a-col :span="12" class="conCls">{{ type.linkName }}</a-col>
</a-row>
</a-col>
<a-col :span="12">
<a-row>
<a-col :span="10">联系电话:</a-col>
<a-col :span="12" class="conCls">{{ type.linkPhone }}</a-col>
</a-row>
</a-col>
<a-col :span="12">
<a-row>
<a-col :span="10">收货地址:</a-col>
<a-col :span="12" class="conCls">{{ type.province }}-{{ type.city }}</a-col>
</a-row>
</a-col>
<a-col :span="12">
<a-row>
<a-col :span="10">详细地址:</a-col>
<a-col :span="12" class="conCls">{{ type.detailAddress }}</a-col>
</a-row>
</a-col>
</a-row>
<div class="btnCls" v-if="!type.isDefault && model == 1">
<span @click="handleUse(type)">使用</span>
<a-popconfirm
placement="topRight"
title="确认删除该收货地址?"
ok-text="确认"
cancel-text="取消"
@confirm="handleDel(type)"
>
<span>删除</span>
</a-popconfirm>
</div>
<div class="btnCls" v-if="model == 2">
<span @click="handleSelect(type)">使用</span>
<a-popconfirm
placement="topRight"
title="确认删除该收货地址?"
ok-text="确认"
cancel-text="取消"
@confirm="handleDel(type)"
>
<span>删除</span>
</a-popconfirm>
</div>
<div class="activeCls" v-if="type.isDefault && model == 1"></div>
</a-card>
<a-button type="dashed" @click="addPayType()" icon="plus">新增</a-button>
<AddressAdd ref="AddressAdd" @ok="getList" />
</CustomDrawer>
</template>

<script>
import AddressAdd from './AddressAdd';
import { getAddressList, deleteAddress, updateAddress } from '@/services/fileManagement/address';
export default {
props: {
model: {
type: Number,// 1:收货地址管理,2:选择收货地址
default: 1
}
},
components: { AddressAdd },
data() {
return {
form: {},
rules: {},
payTypeArr: [],
monthArr: [],
addressList: [],
id: '',
};
},
methods: {
//使用
handleUse(row) {
updateAddress(row.id, {
...row,
companyId: this.id,
isDefault: true
})
.then(res => {
this.$message.success('操作成功');
this.getList();
})
},
// 选择
handleSelect(row) {
this.$emit('ok', row);
this.$refs.addressList.hideDrawer();
},
//删除
handleDel(row) {
deleteAddress(row.id, this.id)
.then(res => {
this.$message.success('删除成功');
this.getList();
})
},
showDrawer({id}) {
this.id = id;
this.getList();
this.$refs.addressList.showDrawer();
},
// 获取列表
getList() {
getAddressList(this.id)
.then(res => {
this.addressList = res;
})
},
// 新增
addPayType() {
this.$refs.AddressAdd.showDrawer(this.id);
},
},
};
</script>

<style scoped lang="less">
.rowCls {
line-height: 40px;
color: #999999;
}
.conCls {
font-size: 14px;
color: #333333;
}
.cardCls {
border: 1px solid #dddddd;
border-radius: 4px;
margin-bottom: 10px;
position: relative;
&.active {
border-color: #f90;
}
.btnCls {
display: none;
position: absolute;
right: 0;
bottom: 5px;
span {
padding: 10px;
color: #f90;
cursor: pointer;
}
}
&:hover {
border-color: #f90;
.btnCls {
display: block;
}
}
}
.activeCls {
position: absolute;
right: 0;
bottom: 0;
&:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
border: 12px solid #f90;
border-top-color: transparent;
border-left-color: transparent;
}
&:after {
content: "";
width: 5px;
height: 10px;
position: absolute;
right: 4px;
bottom: 5px;
border: 1px solid #fff;
border-top-color: transparent;
border-left-color: transparent;
transform: rotate(45deg);
}
}
</style>

+ 172
- 0
src/components/cache/AKeepAlive.js ファイルの表示

@@ -0,0 +1,172 @@
import {isDef, isRegExp, remove} from '@/utils/util'

const patternTypes = [String, RegExp, Array]

function matches (pattern, name) {
if (Array.isArray(pattern)) {
if (pattern.indexOf(name) > -1) {
return true
} else {
for (let item of pattern) {
if (isRegExp(item) && item.test(name)) {
return true
}
}
return false
}
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}

function getComponentName (opts) {
return opts && (opts.Ctor.options.name || opts.tag)
}

function getComponentKey (vnode) {
const {componentOptions, key} = vnode
return key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: key + componentOptions.Ctor.cid
}

function getFirstComponentChild (children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || c.isAsyncPlaceholder)) {
return c
}
}
}
}

function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
const componentKey = getComponentKey(cachedNode)
if (name && !filter(name, componentKey)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}

function pruneCacheEntry2(cache, key, keys) {
const cached = cache[key]
if (cached) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}

function pruneCacheEntry (cache, key, keys, current) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}

export default {
name: 'AKeepAlive',
abstract: true,
model: {
prop: 'clearCaches',
event: 'clear',
},
props: {
include: patternTypes,
exclude: patternTypes,
excludeKeys: patternTypes,
max: [String, Number],
clearCaches: Array
},
watch: {
clearCaches: function(val) {
if (val && val.length > 0) {
const {cache, keys} = this
val.forEach(key => {
pruneCacheEntry2(cache, key, keys)
})
this.$emit('clear', [])
}
}
},

created() {
this.cache = Object.create(null)
this.keys = []
},

destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},

mounted () {
this.$watch('include', val => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, (name) => !matches(val, name))
})
this.$watch('excludeKeys', val => {
pruneCache(this, (name, key) => !matches(val, key))
})
},

render () {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name = getComponentName(componentOptions)
const componentKey = getComponentKey(vnode)
const { include, exclude, excludeKeys } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name)) ||
(excludeKeys && componentKey && matches(excludeKeys, componentKey))
) {
return vnode
}

const { cache, keys } = this
const key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key + componentOptions.Ctor.cid
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}

vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}

+ 78
- 0
src/components/card/ChartCard.vue ファイルの表示

@@ -0,0 +1,78 @@
<template>
<a-card :loading="loading" :body-style="{padding: '20px 24px 8px'}" :bordered="false">
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">{{title}}</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total"><span>{{total}}</span></div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot></slot>
</div>
</div>
<div class="chart-card-footer">
<slot name="footer"></slot>
</div>
</a-card>
</template>

<script>
export default {
name: 'ChartCard',
props: ['title', 'total', 'loading']
}
</script>

<style scoped lang="less">
.chart-card-header{
position: relative;
overflow: hidden;
width: 100%;
}
.chart-card-header .meta{
position: relative;
overflow: hidden;
width: 100%;
color: @text-color-second;
font-size: 14px;
line-height: 22px;
}
.chart-card-action{
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
.chart-card-footer{
border-top: 1px solid @border-color-base;
padding-top: 9px;
margin-top: 8px;
}
.chart-card-content{
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
}
.chart-card-content .content-fix{
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
</style>

+ 59
- 0
src/components/chart/Bar.vue ファイルの表示

@@ -0,0 +1,59 @@
<template>
<div class="bar">
<h4>{{title}}</h4>
<div class="chart">
<v-chart :force-fit="true" height="312" :data="data" :padding="[24, 0, 0, 0]">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</div>
</template>

<script>

const data = []
for (let i = 0; i < 12; i += 1) {
data.push({
x: `${i + 1}月`,
y: Math.floor(Math.random() * 1000) + 200
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]

const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'Bar',
props: ['title'],
data () {
return {
data,
scale,
tooltip
}
}
}
</script>

<style scoped lang="less">
.bar{
position: relative;
.chart{
}
}
</style>

+ 67
- 0
src/components/chart/MiniArea.vue ファイルの表示

@@ -0,0 +1,67 @@
<template>
<div class="mini-chart">
<div class="chart-content" :style="{height: 46}">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>

<script>
import {format} from 'date-fns'

const data = []
const beginDay = new Date().getTime()

const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]
for (let i = 0; i < fakeY.length; i += 1) {
data.push({
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
y: fakeY[i]
})
}

const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]

const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]

export default {
name: 'MiniArea',
data () {
return {
data,
scale,
tooltip,
height: 100
}
}
}
</script>

<style scoped>
.mini-chart {
position: relative;
width: 100%
}
.mini-chart .chart-content{
position: absolute;
bottom: -28px;
width: 100%;
}
</style>

+ 59
- 0
src/components/chart/MiniBar.vue ファイルの表示

@@ -0,0 +1,59 @@
<template>
<div class="mini-chart">
<div class="chart-content" :style="{height: 46}">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-bar position="x*y" />
</v-chart>
</div>
</div>
</template>

<script>
import {format} from 'date-fns'

const data = []
const beginDay = new Date().getTime()

const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]
for (let i = 0; i < fakeY.length; i += 1) {
data.push({
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
y: fakeY[i]
})
}

const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]

const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]

export default {
name: 'MiniBar',
data () {
return {
data,
scale,
tooltip,
height: 100
}
}
}
</script>

<style lang="less" scoped>
@import "index.less";
</style>

+ 56
- 0
src/components/chart/MiniProgress.vue ファイルの表示

@@ -0,0 +1,56 @@
<template>
<div class="mini-progress">
<a-tooltip :title="'目标值:' + target + '%'">
<div class="target" :style="{left: target + '%'}">
<span :style="{backgroundColor: color}" />
<span :style="{backgroundColor: color}" />
</div>
</a-tooltip>
<div class="wrap">
<div class="progress" :style="{backgroundColor: color, width: percent + '%', height: height}" />
</div>
</div>
</template>

<script>
export default {
name: 'MiniProgress',
props: ['target', 'color', 'percent', 'height']
}
</script>

<style lang="less" scoped>
.mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.wrap {
background-color: @layout-bg-color;
position: relative;
}
.progress {
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: #13C2C2;
width: 0;
height: 100%;
}
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
}
span:last-child {
top: auto;
bottom: 0;
}
}
}
</style>

+ 80
- 0
src/components/chart/Radar.vue ファイルの表示

@@ -0,0 +1,80 @@
<template>
<v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip />
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
<v-legend dataKey="user" marker="circle" :offset="30" />
<v-coord type="polar" radius="0.8" />
<v-line position="item*score" color="user" :size="2" />
<v-point position="item*score" color="user" :size="4" shape="circle" />
</v-chart>
</template>

<script>
const DataSet = require('@antv/data-set')

const sourceData = [
{item: '引用', a: 70, b: 30, c: 40},
{item: '口碑', a: 60, b: 70, c: 40},
{item: '产量', a: 50, b: 60, c: 40},
{item: '贡献', a: 40, b: 50, c: 40},
{item: '热度', a: 60, b: 70, c: 40},
{item: '引用', a: 70, b: 50, c: 40}
]

const dv = new DataSet.View().source(sourceData)
dv.transform({
type: 'fold',
fields: ['a', 'b', 'c'],
key: 'user',
value: 'score'
})

const scale = [{
dataKey: 'score',
min: 0,
max: 80
}]

const data = dv.rows

const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}

export default {
name: 'Radar',
data () {
return {
sourceData,
data,
axis1Opts,
axis2Opts,
scale
}
}
}
</script>

<style scoped>

</style>

+ 59
- 0
src/components/chart/RankingList.vue ファイルの表示

@@ -0,0 +1,59 @@
<template>
<div class="rank">
<h4 class="title">{{title}}</h4>
<ul class="list">
<li :key="index" v-for="(item, index) in list">
<span :class="index < 3 ? 'active' : null">{{index + 1}}</span>
<span >{{item.name}}</span>
<span >{{item.total}}</span>
</li>
</ul>
</div>
</template>

<script>
export default {
name: 'RankingList',
props: ['title', 'list']
}
</script>

<style lang="less" scoped>
.rank{
padding: 0 32px 32px 72px;
.title{
}
.list{
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
margin-top: 16px;
span {
color: @text-color-second;
font-size: 14px;
line-height: 22px;
}
span:first-child {
background-color: @layout-bg-color;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
span.active {
background-color: #314659 !important;
color: @text-color-inverse !important;
}
span:last-child {
float: right;
}
}
}
}
</style>

+ 79
- 0
src/components/chart/Trend.vue ファイルの表示

@@ -0,0 +1,79 @@
<template>
<div class="chart-trend">
{{term}}
<span>{{rate}}%</span>
<span :class="['chart-trend-icon', trend]" style=""><a-icon :type="'caret-' + trend" /></span>
</div>
</template>

<script>
export default {
name: 'Trend',
props: {
term: {
type: String,
required: true
},
target: {
type: Number,
required: false,
default: 0
},
value: {
type: Number,
required: false,
default: 0
},
isIncrease: {
type: Boolean,
required: false,
default: null
},
percent: {
type: Number,
required: false,
default: null
},
scale: {
type: Number,
required: false,
default: 2
}
},
data () {
return {
trend: this.isIncrease ? 'up' : 'down',
rate: this.percent
}
},
created () {
this.trend = this.caulateTrend()
this.rate = this.caulateRate()
},
methods: {
caulateRate () {
return (this.percent === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percent).toFixed(this.scale)
},
caulateTrend () {
let isIncrease = this.isIncrease === null ? this.value >= this.target : this.isIncrease
return isIncrease ? 'up' : 'down'
}
}
}
</script>

<style lang="less" scoped>
.chart-trend{
display: inline-block;
font-size: 14px;
.chart-trend-icon{
font-size: 12px;
&.up{
color: @red-6;
}
&.down{
color: @green-6;
}
}
}
</style>

+ 9
- 0
src/components/chart/index.less ファイルの表示

@@ -0,0 +1,9 @@
.mini-chart{
position: relative;
width: 100%;
.chart-content{
position: absolute;
bottom: -28px;
width: 100%;
}
}

+ 157
- 0
src/components/checkbox/ColorCheckbox.vue ファイルの表示

@@ -0,0 +1,157 @@
<template>
<div class="theme-color" :style="{backgroundColor: color}" @click="toggle">
<a-icon v-if="sChecked" type="check" />
</div>
</template>

<script>
const Group = {
name: 'ColorCheckboxGroup',
props: {
defaultValues: {
type: Array,
required: false,
default: () => []
},
multiple: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
values: [],
options: []
}
},
computed: {
colors () {
let colors = []
this.options.forEach(item => {
if (item.sChecked) {
colors.push(item.color)
}
})
return colors
}
},
provide () {
return {
groupContext: this
}
},
watch: {
values(value) {
this.$emit('change', value, this.colors)
}
},
methods: {
handleChange (option) {
if (!option.checked) {
if (this.values.indexOf(option.value) > -1) {
this.values = this.values.filter(item => item != option.value)
}
} else {
if (!this.multiple) {
this.values = [option.value]
this.options.forEach(item => {
if (item.value != option.value) {
item.sChecked = false
}
})
} else {
this.values.push(option.value)
}
}
}
},
render (h) {
const clear = h('div', {attrs: {style: 'clear: both'}})
return h(
'div',
{},
[this.$slots.default, clear]
)
}
}

export default {
name: 'ColorCheckbox',
Group: Group,
props: {
color: {
type: String,
required: true
},
value: {
type: [String, Number],
required: true
},
checked: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
sChecked: this.initChecked()
}
},
computed: {
},
inject: ['groupContext'],
watch: {
'sChecked': function () {
const value = {
value: this.value,
color: this.color,
checked: this.sChecked
}
this.$emit('change', value)
const groupContext = this.groupContext
if (groupContext) {
groupContext.handleChange(value)
}
}
},
created () {
const groupContext = this.groupContext
if (groupContext) {
groupContext.options.push(this)
}
},
methods: {
toggle () {
if (this.groupContext.multiple || !this.sChecked) {
this.sChecked = !this.sChecked
}
},
initChecked() {
let groupContext = this.groupContext
if (!groupContext) {
return this.checked
}else if (groupContext.multiple) {
return groupContext.defaultValues.indexOf(this.value) > -1
} else {
return groupContext.defaultValues[0] == this.value
}
}
}
}
</script>

<style lang="less" scoped>
.theme-color{
float: left;
width: 20px;
height: 20px;
border-radius: 2px;
cursor: pointer;
margin-right: 8px;
text-align: center;
color: @base-bg-color;
font-weight: bold;
}
</style>

+ 161
- 0
src/components/checkbox/ImgCheckbox.vue ファイルの表示

@@ -0,0 +1,161 @@
<template>
<a-tooltip :title="title" :overlayStyle="{zIndex: 2001}">
<div class="img-check-box" @click="toggle">
<img :src="img" />
<div v-if="sChecked" class="check-item">
<a-icon type="check" />
</div>
</div>
</a-tooltip>
</template>

<script>
const Group = {
name: 'ImgCheckboxGroup',
props: {
multiple: {
type: Boolean,
required: false,
default: false
},
defaultValues: {
type: Array,
required: false,
default: () => []
}
},
data () {
return {
values: [],
options: []
}
},
provide () {
return {
groupContext: this
}
},
watch: {
'values': function (value) {
this.$emit('change', value)
// // 此条件是为解决单选时,触发两次chang事件问题
// if (!(newVal.length === 1 && oldVal.length === 1 && newVal[0] === oldVal[0])) {
// this.$emit('change', this.values)
// }
}
},
methods: {
handleChange (option) {
if (!option.checked) {
if (this.values.indexOf(option.value) > -1) {
this.values = this.values.filter(item => item != option.value)
}
} else {
if (!this.multiple) {
this.values = [option.value]
this.options.forEach(item => {
if (item.value != option.value) {
item.sChecked = false
}
})
} else {
this.values.push(option.value)
}
}
}
},
render (h) {
return h(
'div',
{
attrs: {style: 'display: flex'}
},
[this.$slots.default]
)
}
}

export default {
name: 'ImgCheckbox',
Group,
props: {
checked: {
type: Boolean,
required: false,
default: false
},
img: {
type: String,
required: true
},
value: {
required: true
},
title: String
},
data () {
return {
sChecked: this.initChecked()
}
},
inject: ['groupContext'],
watch: {
'sChecked': function () {
const option = {
value: this.value,
checked: this.sChecked
}
this.$emit('change', option)
const groupContext = this.groupContext
if (groupContext) {
groupContext.handleChange(option)
}
}
},
created () {
const groupContext = this.groupContext
if (groupContext) {
this.sChecked = groupContext.defaultValues.length > 0 ? groupContext.defaultValues.indexOf(this.value) >= 0 : this.sChecked
groupContext.options.push(this)
}
},
methods: {
toggle () {
if (this.groupContext.multiple || !this.sChecked) {
this.sChecked = !this.sChecked
}
},
initChecked() {
let groupContext = this.groupContext
if (!groupContext) {
return this.checked
}else if (groupContext.multiple) {
return groupContext.defaultValues.indexOf(this.value) > -1
} else {
return groupContext.defaultValues[0] == this.value
}
}
}
}
</script>

<style lang="less" scoped>
.img-check-box{
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
.check-item{
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: @primary-color;
font-size: 14px;
font-weight: bold;
}
}
</style>

+ 7
- 0
src/components/checkbox/index.js ファイルの表示

@@ -0,0 +1,7 @@
import ColorCheckbox from '@/components/checkbox/ColorCheckbox'
import ImgCheckbox from '@/components/checkbox/ImgCheckbox'

export {
ColorCheckbox,
ImgCheckbox
}

+ 46
- 0
src/components/customer/AreaSelect.vue ファイルの表示

@@ -0,0 +1,46 @@
<template>
<a-cascader :options="options" :fieldNames="fieldNames" v-model="area" placeholder="请选择省市" @change="onChange" />
</template>
<script>
import { getAreaList } from '@/services/dropDown';
export default {
props: {
areaData: {
type: Array,
default: function() {
return [];
}
}
},
watch: {
areaData(n) {
this.area = n;
}
},
data() {
return {
area: [],
fieldNames: {
label: 'name',
value: 'code',
children: 'child'
},
options: [],
};
},
mounted() {
this.getList();
},
methods: {
onChange(value, selectedOptions) {
this.$emit('ok', value, selectedOptions)
},
getList() {
getAreaList()
.then(res => {
this.options = res;
})
}
},
};
</script>

+ 64
- 0
src/components/customer/ButtonBox.vue ファイルの表示

@@ -0,0 +1,64 @@
<template>
<div :style="`height: ${height}px`">
<a-card ref="buttonBoxSelf" :class="{ 'button-box-fixed': isFixed }">
<slot></slot>
</a-card>
</div>
</template>

<script>
export default {
name: 'ButtonBox',
data() {
return {
height: 0,
isFixed: false,
timer: null
}
},
mounted() {
const dom = this.$refs.buttonBoxSelf.$el;
this.height = dom.offsetHeight;
let offsetTop = dom.offsetTop;
if (dom.offsetTop == 0) {
const parent = this.$refs.buttonBoxSelf.$parent.$parent.$el;
offsetTop = parent.offsetTop;
}
this.$bus.$on('scrollTop', scrollTop => {
if(this.timer){
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
if (offsetTop == 0) {
// 滚动距离为包裹层的高度
if (scrollTop >= 20) {
this.isFixed = true;
} else {
this.isFixed = false;
}
return;
}
// 滚动距离为包裹层距离内容层的高度
if (scrollTop >= offsetTop) {
this.isFixed = true;
} else {
this.isFixed = false;
}
}, 10)
});
},
methods: {
}
}
</script>

<style scoped>
.button-box-fixed {
position: fixed;
top: 152px;
z-index: 3;
width: calc(100% - 120px);
box-shadow: 0 6px 12px 0 rgb(0 0 0 / 5%);
}
</style>

+ 107
- 0
src/components/customer/CustomerBusinessModal.vue ファイルの表示

@@ -0,0 +1,107 @@
<template>
<div class="business-box" :class="{'left': position == 'left', 'right': position == 'right',}" ref="business" @click.stop="stopPop">
<div class="business-info">
<div class="business-label">角色</div>
<div class="business-value">{{getCustomerType(type)}}</div>
<div class="business-label">姓名</div>
<div class="business-value">{{fullName|empty}} <a v-if="type != 1 && showBtn" href="javascript:;" @click.stop="changeBusiness">更换</a></div>
<div class="business-label">工号</div>
<div class="business-value">{{employeeNo|empty}}</div>
<div class="business-label">联系方式</div>
<div class="business-value">{{info.phoneNumber|empty}}</div>
</div>
</div>
</template>

<script>
export default {
props: {
info: {
type: Object,
default: function() {
return {}
}
},
type: {
type: Number,
default: 2
},
position: {
type: String,
default: 'left'
},
showBtn: {
type: Boolean,
default: true
}
},
computed: {
fullName() {
const surName = this.info.surname || '';
const name = this.info.name || '';
return `${surName}${name}`;
},
employeeNo() {
return this.info.extraProperties?.EmployeeNo || '';
},
},
methods: {
stopPop() {
// 阻止冒泡
},
changeBusiness() {
this.$emit('change');
},
getCustomerType(){
if(this.type==1)
return '商务人员'
if(this.type==2)
return '项目经理'
if(this.type==3)
return '投单人'
if(this.type==4)
return '审核人'
},
}
}
</script>

<style lang="less" scoped>
.business-box {
position: absolute;
top: 0;
display: none;
&.left {
padding-right: 7px;
right: 40px;
}
&.right {
padding-left: 7px;
left: 40px;
}
}
.business-info {
width: 140px;
height: 266px;
padding: 20px;
box-sizing: border-box;
background: #fff;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.16);
border-radius: 4px;
.business-label {
font-size: 14px;
line-height: 20px;
color: #999;
margin-bottom: 6px;
}
.business-value {
font-size: 14px;
line-height: 20px;
color: #333;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

+ 70
- 0
src/components/customer/CustomerIntroduce.vue ファイルの表示

@@ -0,0 +1,70 @@
<template>
<a-card>
<div class="customer-header">
<div class="customer-header-left">
<img
:src="basicInfo.logo"
v-if="basicInfo.logo"
alt=""
width="64"
height="64"
/>
<SvgIcon iconClass="icon-liebiaomorenwu" v-else />
</div>
<div class="customer-header-right">
<div class="company-name">{{ basicInfo.name }}</div>
<div class="company-info">
<span>客户编号:{{ basicInfo.code }}</span>
<span
>创建时间:{{
basicInfo.creationTime | moment("YYYY年MM月DD日")
}}</span
>
</div>
</div>
</div>
</a-card>
</template>

<script>
export default {
name: 'CustomerIntroduce',
props: {
basicInfo: {}
}
}
</script>

<style lang="less" scoped>
.customer-header {
display: flex;
.customer-header-left {
margin-right: 20px;
font-size: 64px;
line-height: 64px;
height: 64px;
display: flex;
align-items: center;
border-radius: 4px;
overflow: hidden;
img {
object-fit: cover;
}
}
.company-name {
font-size: 20px;
line-height: 26px;
color: #333;
font-weight: 500;
margin-bottom: 10px;
}
.company-info {
font-size: 14px;
line-height: 20px;
color: #333;
}
.company-info span {
margin-right: 25px;
}
}
</style>

+ 173
- 0
src/components/customer/FileList.vue ファイルの表示

@@ -0,0 +1,173 @@
<template>
<div>
<p class="mb-10 titleCls">{{ title }}</p>
<a-card class="fileCls">
<div class="file">
<div
class="fileItem"
v-for="(file, index) in structureOptions"
:key="index"
@click="showList(file)"
>
<div>
<SvgIcon
v-show="file.fileCount > 0"
iconClass="icon-wenjian"
></SvgIcon>
<SvgIcon
v-show="file.fileCount == 0"
iconClass="icon-zanwuwenjian"
></SvgIcon>
</div>
<p>{{ file.name || file.displayText }}</p>
</div>
</div>
</a-card>
<FileListDrawer
ref="fileListDrawer"
:title="fileListTitle"
:fileArr="fileArr"
:uploadFlag="uploadFlag"
@ok="refresh"
/>
</div>
</template>

<script>
import { getFileStructure } from '@/services/fileManagement/product'
import FileListDrawer from '@/components/drawer/FileListDrawer'
export default {
name: 'FileList',
components: {
FileListDrawer,
},
props: {
title: {
type: String,
default: '文件清单',
},
code: {
type: String,
},
dictCode: {
type: String,
},
dictionaryCode: {
type: String,
},
},
data() {
return {
fileListTitle: '',
fileTypeOptions: [],
structureOptions: [],
fileArr: [],
fileCenters: [],
fileCode: null,
fileDictCode: null,
uploadFlag: true,
}
},
mounted() {
this.getStructure()
},
methods: {
//报价详情中异步获取文件列表
showModal(code, dictCode, flag) {
console.log(flag)
this.uploadFlag = flag || false
this.fileCode = code
this.fileDictCode = dictCode
this.getStructure()
},
//刷新列表
refresh() {
this.getStructure()
},
//展示文件列表
showList(row) {
if (row.fileCount == 0) {
return
}
this.fileListTitle = row.name
this.$refs.fileListDrawer.showDrawer(this.code || this.fileCode, row.type)
},
// 获取文件夹结构
getStructure() {
if (
(this.code && this.dictCode) ||
(this.fileCode && this.fileDictCode)
) {
getFileStructure({
dictCode: this.dictCode || this.fileDictCode,
code: this.code || this.fileCode,
}).then((res) => {
this.structureOptions = res
})
}
},
},
}
</script>

<style lang="less" scoped>
.titleCls {
font-size: 16px;
color: #333333;
}
.fileCls {
padding: 12px;
box-sizing: border-box;

p {
font-size: 16px;
line-height: 20px;
color: #333333;
}

.file {
display: flex;
align-items: center;
flex-wrap: wrap;
}

.fileItem {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// margin-right: 50px;
// width: 10%;
flex-shrink: 0;
// margin-bottom: 20px;
width: 140px;
padding: 10px;
box-sizing: border-box;
cursor: pointer;

p {
font-size: 14px;
color: #333333;
margin: 12px 0 0;
padding: 0;
}

div {
width: 55px;
height: 50px;
overflow: hidden;

.icon {
width: 50px;
height: 50px;
}

img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
}
</style>

+ 216
- 0
src/components/customer/OfferFileList.vue ファイルの表示

@@ -0,0 +1,216 @@
<template>
<div v-show="structureOptions.length">
<p class="mb-10 titleCls">{{ title }}</p>
<a-card class="fileCls">
<div class="file">
<div
class="fileItem"
v-for="(file, index) in structureOptions"
:key="index"
@click="showList(file)"
>
<div>
<SvgIcon
v-show="file.fileCount > 0"
iconClass="icon-wenjian"
></SvgIcon>
<SvgIcon
v-show="file.fileCount == 0"
iconClass="icon-zanwuwenjian"
></SvgIcon>
</div>
<p>{{ file.name || file.displayText }}</p>
</div>
</div>
</a-card>
<FileListDrawer
ref="fileListDrawer"
:title="fileListTitle"
:fileArr="fileArr"
:uploadFlag="uploadFlag"
@ok="refresh"
/>
</div>
</template>

<script>
import { getFileStructure } from '@/services/fileManagement/product'
import FileListDrawer from '@/components/drawer/FileListDrawer'
export default {
name: 'FileList',
components: {
FileListDrawer,
},
props: {
title: {
type: String,
default: '文件清单',
},
code: {
type: String,
},
dictCode: {
type: String,
},
dictionaryCode: {
type: String,
},
},
data() {
return {
fileListTitle: '',
fileTypeOptions: [],
structureOptions: [],
fileArr: [],
fileCenters: [],
fileCode: null,
fileDictCode: null,
uploadFlag: true,
type:null,
priceFileArr:[],
}
},
mounted() {
this.getStructure()
},
methods: {
//报价详情中异步获取文件列表
showModal(code, dictCode,arr) {
this.uploadFlag = false
this.fileCode = code
this.fileCenters = arr
this.fileDictCode = dictCode
this.getStructure()
},
//刷新列表
refresh() {
this.getStructure()
},
//对象数组去重
removeDuplicateObj(arr){
let obj = {};
arr = arr.reduce((newArr, next) => {
obj[next.type] ? '' : (obj[next.type] = true && newArr.push(next));
return newArr;
}, []);
return arr;
},
filterData(){
let arr = this.fileCenters.map(item=>{
return {
type:item.type,
typeValue:item.typeValue
}
})
arr = this.removeDuplicateObj(arr);
this.priceFileArr = [];
arr.forEach(item=>{
let list = this.fileCenters.filter(data=>data.type==item.type);
this.priceFileArr.push({
type:item.type,
typeValue:item.typeValue,
children:list
})
})
let typeArr = this.structureOptions.map(item=>item.type);
this.priceFileArr.forEach(item=>{
let index = typeArr.indexOf(item.type);
if(index>-1){
this.structureOptions[index]['fileCount'] = item.children.length;
this.$forceUpdate();
}
})
},
//展示文件列表
showList(row) {
if (row.fileCount == 0) {
return
}
this.fileListTitle = row.name
let arr = this.fileCenters.filter(item=>item.type==row.type);
this.$refs.fileListDrawer.showDrawer(null,null,arr)
},
// 获取文件夹结构
getStructure() {
if (
(this.code && this.dictCode) ||
(this.fileCode && this.fileDictCode)
) {
getFileStructure({
dictCode: this.dictCode || this.fileDictCode,
code: this.code || this.fileCode,
}).then((res) => {
let data = [...res]
this.structureOptions = data.map(item=>{
item.fileCount = 0
return item
})
this.filterData();
})
}
},
},
}
</script>

<style lang="less" scoped>
.titleCls {
font-size: 16px;
color: #333333;
}
.fileCls {
padding: 12px;
box-sizing: border-box;

p {
font-size: 16px;
line-height: 20px;
color: #333333;
}

.file {
display: flex;
align-items: center;
flex-wrap: wrap;
}

.fileItem {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// margin-right: 50px;
// width: 10%;
flex-shrink: 0;
// margin-bottom: 20px;
width: 140px;
padding: 10px;
box-sizing: border-box;
cursor: pointer;

p {
font-size: 14px;
color: #333333;
margin: 12px 0 0;
padding: 0;
}

div {
width: 55px;
height: 50px;
overflow: hidden;

.icon {
width: 50px;
height: 50px;
}

img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
}
</style>

+ 100
- 0
src/components/customer/OpenCloseBox.vue ファイルの表示

@@ -0,0 +1,100 @@
<template>
<div
class="openclose-box"
:class="{
'openclose-card-open': isOpen && card,
'openclose-card-close': !isOpen && card,
'openclose-box-open': isOpen && !card,
'openclose-box-close': !isOpen && !card
}"
>
<div class="openclose-btn" :class="{'openclose-btn-box': !card}" @click="isOpen = !isOpen">
<a-space :size="5">
<SvgIcon class="openclose-icon" iconClass="icon-zhankai" v-if="!isOpen" />
<SvgIcon class="openclose-icon" iconClass="icon-shouqi" v-else />
<span>{{ isOpen ? '收起' : '展开' }}</span>
</a-space>
</div>
<a-card v-if="card">
<slot></slot>
</a-card>
<slot v-else></slot>
</div>
</template>

<script>
export default {
name: 'OpenCloseBox',
props: {
open: {
type: Boolean,
default: false
},
card: {
type: Boolean,
default: true
}
},
data() {
return {
isOpen: this.open
}
}
}
</script>

<style lang="less" scoped>
.openclose-box {
position: relative;
/deep/ .ant-card-body {
padding: 20px 18px;
}
.openclose-btn {
font-size: 14px;
line-height: 16px;
color: #333;
width: 100%;
height: 56px;
position: absolute;
top: 0;
right: 0;
padding-right: 18px;
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 1;
user-select: none;
cursor: pointer;
.openclose-icon {
color: #999;
}
&:hover {
color: #f90;
.openclose-icon {
color: #f90;
}
}
}
.openclose-btn-box {
height: 48px;
}
}
.openclose-card-open {
/deep/ .ant-card-body {
height: auto;
}
}
.openclose-card-close {
/deep/ .ant-card-body {
height: 56px;
overflow: hidden;
}
}
.openclose-box-open {
height: auto;
}
.openclose-box-close {
height: 48px;
overflow: hidden;
}
</style>

+ 65
- 0
src/components/customer/StringSubstr.vue ファイルの表示

@@ -0,0 +1,65 @@
<template>
<a-tooltip
placement="topLeft"
:title="content"
:get-popup-container="getPopupContainer"
>
{{ text }}
</a-tooltip>
</template>

<script>
export default {
name: 'StringSubstr',
props: {
content: {
type: String,
default: '',
},
length: {
type: Number,
default: 30,
},
},
data() {
return {
text: '',
}
},
watch: {
content() {
this.getText()
},
},
mounted() {
this.getText()
},
methods: {
getText() {
if (
typeof this.content == 'string' &&
this.content.length > this.length
) {
this.text = `${this.content.substring(0, this.length)}...`
} else {
this.text = this.content
}
},
// 气泡框不可见时自动调整位置
getPopupContainer(trigger) {
return trigger.parentElement
},
},
}
</script>

<style>
.ant-tooltip-arrow::before {
display: none;
}
.ant-tooltip-inner {
background-color: #fff !important;
color: #333 !important;
border-color: #dddddd !important;
}
</style>

+ 175
- 0
src/components/dragModal/index.vue ファイルの表示

@@ -0,0 +1,175 @@
<template>
<a-modal
:class="[modalClass, simpleClass]"
:visible="visible"
v-bind="$props"
:footer="footer"
:bodyStyle="{padding:0}"
@ok="handleOk"
@cancel="handleCancel">
<div class="ant-modal-body" :style="bodyStyle">
<slot></slot>
</div>
<div class="ant-modal-footer relative" v-if="footer === null">
<slot name="footer"></slot>
</div>
<div v-if="!title && title !== ''" slot="title">
<slot name="title"></slot>
</div>
</a-modal>
</template>

<script>
import props from './props.js'
var mouseDownX = 0
var mouseDownY = 0
var deltaX = 0
var deltaY = 0
var sumX = 0
var sumY = 0

var header = null
var contain = null
var modalContent = null

var onmousedown = false
export default {
name: 'DragModal',
mixins: [props],
props: {
// 容器的类名
modalClass: {
type: String,
default: () => {
return 'modal-box'
}
},
visible: {
type: Boolean,
default: () => {
return false
}
},
title: {
type: String,
default: () => {
return undefined
}
},
width: {
type: [String, Number],
default: () => {
return '50%'
}
},
footer: {
type: Boolean,
default: () => {
return undefined
}
}
},
data () {
return {
}
},
computed: {
simpleClass () {
return Math.random().toString(36).substring(2)
}
},
watch: {
visible () {
this.$nextTick(() => {
this.initialEvent(this.visible)
})
}
},
mounted () {
this.$nextTick(() => {
this.initialEvent(this.visible)
})
},
created () {},
beforeDestroy () {
this.removeMove()
window.removeEventListener('mouseup', this.removeUp, false)
},
methods: {
handleOk (e) {
this.resetNum()
this.$emit('ok', e)
},
handleCancel (e) {
this.resetNum()
this.$emit('cancel', e)
},
resetNum () {
mouseDownX = 0
mouseDownY = 0
deltaX = 0
deltaY = 0
sumX = 0
sumY = 0
},
handleMove (event) {
const delta1X = event.pageX - mouseDownX
const delta1Y = event.pageY - mouseDownY

deltaX = delta1X
deltaY = delta1Y
// console.log('delta1X:' + delta1X, 'sumX:' + sumX, 'delta1Y:' + delta1Y, 'sumY:' + sumY)
modalContent.style.transform = `translate(${delta1X + sumX}px, ${delta1Y + sumY}px)`
},
initialEvent (visible) {
// console.log('--------- 初始化')
// console.log('simpleClass===>', this.simpleClass)
// console.log('document===>', document)
if (visible) {
setTimeout(() => {
window.removeEventListener('mouseup', this.removeUp, false)
contain = document.getElementsByClassName(this.simpleClass)[0]
header = contain.getElementsByClassName('ant-modal-header')[0]
modalContent = contain.getElementsByClassName('ant-modal-content')[0]

modalContent.style.left = 0
modalContent.style.transform = 'translate(0px,0px)'

// console.log('初始化-header:', header)
// console.log('初始化-contain:', contain)
// console.log('初始化-modalContent:', modalContent)

header.style.cursor = 'all-scroll'

// contain.onmousedown = (e) => {
header.onmousedown = (e) => {
onmousedown = true
mouseDownX = e.pageX
mouseDownY = e.pageY
document.body.onselectstart = () => false
window.addEventListener('mousemove', this.handleMove, false)
}

window.addEventListener('mouseup', this.removeUp, false)
}, 0)
}
},
removeMove () {
window.removeEventListener('mousemove', this.handleMove, false)
},
removeUp (e) {
// console.log('removeUp')
document.body.onselectstart = () => true

if (onmousedown && !(e.pageX === mouseDownX && e.pageY === mouseDownY)) {
onmousedown = false
sumX = sumX + deltaX
sumY = sumY + deltaY
// console.log('sumX:' + sumX, 'sumY:' + sumY)
}

this.removeMove()
}
}
}
</script>

+ 30
- 0
src/components/dragModal/props.js ファイルの表示

@@ -0,0 +1,30 @@
export default {
props: [
'afterClose', // Modal 完全关闭后的回调 function 无
'bodyStyle', // Modal body 样式 object {}
'cancelText', // 取消按钮文字 string| slot 取消
'centered', // 垂直居中展示 Modal Boolean false
'closable', // 是否显示右上角的关闭按钮 boolean true
'closeIcon', // 自定义关闭图标 VNode | slot - 1.5.0
'confirmLoading', // 确定按钮 loading boolean 无
'destroyOnClose', // 关闭时销毁 Modal 里的子元素 boolean false
// 'footer', // 底部内容,当不需要默认底部按钮时,可以设为 :footer="null" string|slot 确定取消按钮
'forceRender', // 强制渲染 Modal boolean false
'getContainer', // 指定 Modal 挂载的 HTML 节点 (instance): HTMLElement () => document.body
'keyboard', // 是否支持键盘 esc 关闭 boolean true
'mask', // 是否展示遮罩 Boolean true
'maskClosable', // 点击蒙层是否允许关闭 boolean true
'maskStyle', // 遮罩样式 object {}
'okText', // 确认按钮文字 string|slot 确定
'okType', // 确认按钮类型 string primary
'okButtonProps', // ok 按钮 props, 遵循 jsx规范 {props: ButtonProps, on: {}} -
'cancelButtonProps', // cancel 按钮 props, 遵循 jsx规范 {props: ButtonProps, on: {}} -
'title', // 标题 string|slot 无
'visible', // (v-model) 对话框是否可见 boolean 无
'width', // 宽度 string|number 520
'wrapClassName', // 对话框外层容器的类名 string -
'zIndex', // 设置 Modal 的 z-index Number 1000
'dialogStyle', // 可用于设置浮层的样式,调整浮层位置等 object - 1.6.1
'dialogClass' // 可用于设置浮层的类名 string
]
}

+ 190
- 0
src/components/drawer/CustomDrawer.vue ファイルの表示

@@ -0,0 +1,190 @@
<template>
<a-drawer
:placement="placement"
:closable="closable"
:destroyOnClose="destroyOnClose"
:getContainer="getContainer"
:maskClosable="maskClosable"
:mask="mask"
:maskStyle="{ ...defaultMaskStyle, ...maskStyle }"
:wrapClassName="wrapClassName"
:wrapStyle="wrapStyle"
:drawerStyle="drawerStyle"
:headerStyle="{ ...defaultHeaderStyle, ...headerStyle }"
:bodyStyle="{ ...defaultBodyStyle, ...bodyStyle }"
:visible="visible"
:after-visible-change="afterVisibleChange"
:width="width"
:height="height"
:zIndex="zIndex"
:keyboard="keyboard"
@close="hideDrawer"
>
<div slot="title" class="title-wrap">
<span v-if="title!='自定义'">{{ title }}</span>
<slot v-else name="title"></slot>
<div class="drawer-close" @click="hideDrawer">
<SvgIcon iconClass="icon-guanbi" />
</div>
</div>
<slot></slot>
</a-drawer>
</template>

<script>
export default {
name: 'CustomDrawer',
props: {
// 是否显示右上角的关闭按钮
closable: {
type: Boolean,
default: false,
},
// 关闭时销毁 Drawer 里的子元素
destroyOnClose: {
type: Boolean,
default: true,
},
// 指定 Drawer 挂载的 HTML 节点
getContainer: {
type: [HTMLElement, () => HTMLElement, String],
default: 'body',
},
// 点击蒙层是否允许关闭
maskClosable: {
type: Boolean,
default: true,
},
// 是否展示遮罩
mask: {
type: Boolean,
default: true,
},
// 遮罩样式
maskStyle: {
type: Object,
default: function () {
return {};
},
},
// 标题
title: {
type: String,
default: '标题',
},
// 对话框外层容器的类名
wrapClassName: {
type: String,
default: '',
},
// 可用于设置 Drawer 最外层容器的样式,和 drawerStyle 的区别是作用节点包括 mask
wrapStyle: {
type: Object,
default: function () {
return {};
},
},
// 用于设置 Drawer 弹出层的样式
drawerStyle: {
type: Object,
default: function () {
return {};
},
},
// 用于设置 Drawer 头部的样式
headerStyle: {
type: Object,
default: function () {
return {};
},
},
// 可用于设置 Drawer 内容部分的样式
bodyStyle: {
type: Object,
default: function () {
return {};
},
},
// 宽度
width: {
type: [Number, String],
default: 600,
},
// 高度, 在 placement 为 top 或 bottom 时使用
height: {
type: [Number, String],
default: 256,
},
// 设置 Drawer 的 z-index
zIndex: {
type: Number,
default: 1000,
},
// 抽屉的方向 'top' | 'right' | 'bottom' | 'left'
placement: {
type: String,
default: 'right',
},
// 是否支持键盘 esc 关闭
keyboard: {
type: Boolean,
default: true,
},
},
data() {
return {
visible: false,
defaultMaskStyle: {
background: 'rgba(0,0,0,0.2)',
},
defaultHeaderStyle: {
fontSize: '16px',
fontWeight: '500',
color: '#333333',
padding: '40px 30px 25px',
borderBottom: 'none',
},
defaultBodyStyle: {
padding: '0 30px 40px',
},
};
},
methods: {
// 切换抽屉时动画结束后的回调
afterVisibleChange(val) {},
showDrawer() {
this.visible = true;
},
// 点击遮罩层或右上角叉或取消按钮的回调
hideDrawer() {
this.visible = false;
},
},
};
</script>

<style lang="less" scoped>
.title-wrap {
display: flex;
justify-content: space-between;
align-items: center;
height: 28px;
}
.drawer-close {
width: 28px;
height: 28px;
color: #666;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 40px;
right: 30px;
z-index: 99;
&:hover {
background: #E8E9E9;
}
}
</style>

+ 159
- 0
src/components/drawer/FileListDrawer.vue ファイルの表示

@@ -0,0 +1,159 @@
<template>
<CustomDrawer ref="assembleDrawer" :title="title">
<template v-if="code || type">
<div class="drawerCls" v-for="(file, index) in fileList" :key="index">
<div class="imgCls">
<SvgIcon iconClass="icon-wenjianyangshi"></SvgIcon>
</div>
<div class="conCls">
<div>
<UploadFile
v-if="uploadFlag"
:id="'fileId' + index"
@ok="getFile($event, file)"
:iconFlag="true"
style="display: inline-block; margin-right: 10px"
/>
<a
class="nameCls"
href="javascript:;"
@click="downLoad(file.url)"
>{{ file.name }}</a
>
</div>
<div class="conText"><span>创建人</span>{{ file.creator }}</div>
<div class="conText">
<span>创建时间</span>{{ file.creationTime | moment }}
</div>
</div>
<div class="rightCls">
<div @click="handleDel(file, index)" style="cursor: pointer" v-if="checkPermission('Integrations.FileCenterManagement.Delete')">
<SvgIcon iconClass="icon-shanchu" />
</div>
<div></div>
<!-- <span>更新时间</span>{{file.lastModificationTime|moment}} -->
</div>
</div>
</template>
<template v-else>
<div class="drawerCls" v-for="(file, index) in fileArr" :key="index">
<div class="imgCls">
<SvgIcon iconClass="icon-wenjianyangshi"></SvgIcon>
</div>
<div class="conCls">
<div>
<UploadFile
v-if="uploadFlag"
:id="'fileId' + index"
@ok="getFile($event, file)"
:iconFlag="true"
style="display: inline-block; margin-right: 10px"
/>
<a
class="nameCls"
href="javascript:;"
@click="downLoad(file.url)"
>{{ file.name }}</a
>
</div>
<div class="conText"><span>创建人</span>{{ file.creator }}</div>
<div class="conText">
<span>创建时间</span>{{ file.creationTime | moment }}
</div>
</div>
<div class="rightCls">
<div></div>
<div></div>
<!-- <span>更新时间</span>{{file.lastModificationTime|moment}} -->
</div>
</div>
</template>
</CustomDrawer>
</template>

<script>
import { checkPermission } from '@/utils/abp';
import {
getFileCenter,
putFileCenter,
delFileCenter,
} from '@/services/fileManagement/product'
import UploadFile from '@/components/upload/UploadFileSingle'
export default {
components: { UploadFile },
props: {
uploadFlag: {
type: Boolean,
default: true,
},
title: {
type: String,
default: '文件列表',
},
},
data() {
return {
fileList: [],
type: null,
code: null,
fileArr:[]
}
},
methods: {
checkPermission,
getFile(row, file) {
var obj = {
type: this.type,
name: row.name,
url: row.path,
}
//如果有id,则更新
putFileCenter(file.id, obj).then(() => {
this.$message.success('文件更新成功')
this.getFileList(this.code, this.type)
})
},
//下载文件
downLoad(url) {
url && window.open(url)
},
showDrawer(code, type,arr) {
this.type = type
this.code = code
if (code || type) {
this.getFileList(code, type)
}else{
this.fileArr = arr;
}
this.$refs.assembleDrawer.showDrawer()
},
getFileList(code, type) {
getFileCenter(code, type).then((res) => {
this.fileList = res
})
},
hideDrawer() {
this.$refs.assembleDrawer.hideDrawer()
},
handleDel(file, index) {
this.$confirm({
title: '确定删除该文件?',
onOk: () => {
delFileCenter(file.id).then(() => {
this.$message.success('删除成功')
this.fileList.splice(index, 1)
this.$emit('ok')
})
},
})
},
},
}
</script>

<style lang="less" scoped>
@import '@/pages/customerManagement/product/style/product.less';
.nameCls {
margin-left: 0 !important;
}
</style>

+ 69
- 0
src/components/exception/ExceptionPage.vue ファイルの表示

@@ -0,0 +1,69 @@
<template>
<div class="exception-page">
<div class="img">
<img :src="config[type].img" />
</div>
<div class="content">
<h1>{{config[type].title}}</h1>
<div class="desc">{{config[type].desc}}</div>
<div class="action">
<a-button type="primary" @click="backHome">返回首页</a-button>
</div>
</div>
</div>
</template>

<script>
import Config from './typeConfig'

export default {
name: 'ExceptionPage',
props: ['type', 'homeRoute'],
data () {
return {
config: Config
}
},
methods: {
backHome() {
if (this.homeRoute) {
this.$router.push(this.homeRoute)
}
this.$emit('backHome', this.type)
}
}
}
</script>

<style lang="less" scoped>
.exception-page{
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background-color: @base-bg-color;
.img{
padding-right: 52px;
zoom: 1;
img{
max-width: 430px;
}
}
.content{
h1{
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc{
color: @text-color-second;
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
}
}

</style>

+ 19
- 0
src/components/exception/typeConfig.js ファイルの表示

@@ -0,0 +1,19 @@
const config = {
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
title: '403',
desc: '抱歉,你无权访问该页面'
},
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '404',
desc: '抱歉,你访问的页面不存在或仍在开发中'
},
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
title: '500',
desc: '抱歉,服务器出错了'
}
}

export default config

+ 130
- 0
src/components/form-builder/Form.vue ファイルの表示

@@ -0,0 +1,130 @@
<template>
<a-form
:form="form"
class="form-container"
ref="sortTable"
:style="{ width: banner ? banner + 'px' : '100%', margin: '0 auto' }"
:class="config.tableStyle ? 'table' : ''"
>
<template v-for="(item, index) in data">
<FormGridRender
v-if="item.type === 'Grid'"
:mode="mode"
:key="index"
:formItem="item"
:form="form"
:config="{ ...config, gateway, userModel }"
:banner="!!banner"
:mock="mock"
/>
<FormItemRender
v-else
:mode="mode"
:key="index"
:formItem="item"
:form="form"
:config="{ ...config, gateway, userModel }"
/>
</template>
</a-form>
</template>

<script>
import FormGridRender from './form-render/FormGridRender';
import FormItemRender from './form-render/FormItemRender';
export default {
components: { FormGridRender, FormItemRender },
props: [
'uiConf',
'formData',
'gateway',
'userModel',
'mode',
'banner',
'mock',
],
data() {
return {
data: [],
config: {},
};
},
watch: {
uiConf: {
handler(v) {
if (v) {
this.data = v.data;
this.config = v.config;
} else {
this.data = [];
this.config = {};
}
},
immediate: true,
deep: true,
},
},
mounted() {
this.$emit('renderFinish');
},
beforeCreate() {
console.log('beforeCreate');
this.form = this.$form.createForm(this);
this.$emit('getform', this.form);
},
};
</script>

<style lang="less">
@table-border: 1px solid @border-color-base;
.form-container {
&.table {
border: @table-border;
border-right: 0;
.form-item-render {
border-bottom: @table-border;
border-right: @table-border;
&:last-child {
border-bottom: 0;
}
.ant-form-item {
margin-bottom: 0;
margin-top: 5px;
.ant-form-explain {
font-size: 12px;
min-height: 16px;
}
.ant-form-item-control {
padding-bottom: 16px;
}
.ant-form-explain {
position: absolute;
bottom: 0;
}
}
}
.form-grid-render {
border-bottom: @table-border;
border-right: @table-border;
&:last-child {
border-bottom: 0;
}
.col-item {
align-self: stretch;
min-height: 40px;
border-right: @table-border;
.form-item-render {
border-right: 0;
}
&:last-child {
border-right: 0;
}
}
.form-grid-render {
border-right: 0;
border-bottom: @table-border;
}
}
}
}
</style>

+ 495
- 0
src/components/form-builder/FormBuilder.vue ファイルの表示

@@ -0,0 +1,495 @@
<template>
<div class="form-builder-container">
<div class="builder-container header">
<div class="list header-item">
<span>表单控件仓库</span>
<span class="tips">拖拽控件到中间</span>
</div>
<div class="builder header-item">
<span>
<a-button type="link">普通布局</a-button>
<!-- <a-button type="link" disabled>移动端布局</a-button> -->
</span>
<span>
<a-input placeholder="请输入属性名称" v-model="propName" />
</span>
<span>
<a-button type="link" @click="onFormSave">保存</a-button>
<a-button type="link" @click="onFormConfig">表单配置数据</a-button>
<a-button type="link" @click="onGlobalSettingClick">全局设置</a-button>
<a-button type="link" @click="onPreviewClick">预览</a-button>
</span>
</div>
<div class="config header-item">
<span>控件设置</span>
<span>{{activeItem ? activeItem.name : '未选中表单控件'}}</span>
</div>
</div>
<div class="builder-container main">
<div class="list" ref="listArea">
<FormList @drag="onDrag" />
</div>
<div class="builder" ref="builderArea" :class="[draging ? 'draging' : '']">
<a-form :form="form" class="form-container" ref="sortTable">
<template v-for="(item,index) in data">
<FormGridEditor
v-if="item.type === 'Grid'"
:key="renderKey+'_'+index"
:index="index"
:formItem="item"
:form="form"
:config="{...config,gateway,userModel}"
:activeKey="activeKey"
@active="onActive"
@copy="onGridCopy"
@delete="onDelete"
/>
<FormItemEditor
v-else
:activeKey="activeKey"
:key="renderKey+'_'+index"
:index="index"
:formItem="item"
:form="form"
:config="{...config,gateway,userModel}"
@active="onActive"
@copy="onItemCopy"
@delete="onDelete"
/>
</template>
</a-form>
</div>
<div class="config" ref="configArea">
<FormSetting :activeItem="activeItem" />
</div>
</div>

<previewModal
v-if="previewModalVisible"
:visible="previewModalVisible"
:uiConf="{config: config,data:data}"
:gateway="gateway"
:userModel="userModel"
@close="previewModalVisible = false"
/>
<GlobalSettingModal
v-if="globalSettingVisible"
v-model="config"
:visible="globalSettingVisible"
@close="globalSettingVisible = false"
/>
<FormConfigData
v-if="formConfigVisible"
:visible="formConfigVisible"
:uiConf="{config: config,data:data}"
:dbConf="dbConf"
@close="formConfigVisible = false"
/>
</div>
</template>

<script>
import Sortable from 'sortablejs';
import Scrollbar from 'smooth-scrollbar';
import FormList from './form-list/FormList';
import formItemList from './form-item/form-item.config';
import FormItemEditor from './form-editor/FormItemEditor';
import FormGridEditor from './form-editor/FormGridEditor';
import FormSetting from './form-setting/FormSetting';
import previewModal from './form-builder-modal/PreviewModal';
import GlobalSettingModal from './form-builder-modal/GlobalSetting';
import FormConfigData from './form-builder-modal/FormConfigData';

import {
getFormConfig,
recursiveReplace,
arrayObject2ArrayElement,
getFormList
} from './utils/form-config.util';

const defaultConfig = {
labelCol: 5,
wraperCol: 12,
labelAlign: 'right',
labelVertical: 'top',
tableStyle: false
};
export default {
components: {
FormList,
FormItemEditor,
FormSetting,
FormGridEditor,
previewModal,
GlobalSettingModal,
FormConfigData
},
props: {
value: {
type: Object,
default: () => ({ config: { ...defaultConfig }, list: [] })
},
gateway: {
type: String,
default: '',
required: true
},
userModel: {
type: Object,
default: () => ({}),
required: true
}
},
watch: {
value: {
handler(val) {
console.log(val, 'builder');
if (val) {
this.data = val.list || [];

this.config = { ...defaultConfig, ...val.config };
} else {
this.data = [];
this.config = { ...defaultConfig };
}
},
immediate: true
},
data: {
handler(v) {
this.dbConf = getFormList(v);
this.$emit('change', { config: this.config, data: v });
},
deep: true
},
config: {
handler(v) {
this.$emit('change', { config: v, data: this.data });
},
deep: true
}
},
data() {
return {
formConfigVisible: false,
globalSettingVisible: false,
previewModalVisible: false,
data: [],
config: { ...defaultConfig },
scroll: null,
draging: false,
activeItem: null,
activeKey: null,
previewForm: null,
renderKey: 0,
dbConf: [],
propName: ''
};
},
beforeCreate() {
this.form = this.$form.createForm(this);
},
mounted() {
Scrollbar.init(this.$refs.listArea);
Scrollbar.init(this.$refs.builderArea);
Scrollbar.init(this.$refs.configArea);

let isAdd = false;
new Sortable(this.$refs.sortTable.$el, {
group: { name: 'component' },
animation: 200,
onStart: e => {},
onEnd: e => {},
onSort: e => {
//添加也会触发onSort, 用个变量去来区分
if (!isAdd) {
console.log('排序 进入', 'e');
const tempData = this.data[e.oldIndex];
const newData = this.data[e.newIndex];
console.log(tempData, newData, e.newIndex, e.oldIndex);
if (!tempData.key || !newData.key) return;
if (e.newIndex !== e.oldIndex) {
this.data.splice(e.oldIndex, 1);
this.data.splice(e.newIndex, 0, tempData);
}
this.emitChange();
console.log('排序 进入', this.data);
}

isAdd = false;
},
onAdd: e => {
console.log('builder onAdd');
isAdd = true;
let defaultConfig = null;

if (e.item.dataset.source == 'formList') {
//左侧新增的
defaultConfig = getFormConfig(e.item.dataset.id);
} else {
//不是从左侧进来的 数据
defaultConfig = e.item.__vue__.formItem;
}
console.log(defaultConfig, 'eeeeeeeee');
this.data.splice(e.newIndex, 0, defaultConfig);
this.activeKey = defaultConfig.key;
this.activeItem = defaultConfig;
e.item.parentNode.removeChild(e.item);
this.emitChange();
console.log(this.data, 'ddddddddddddata');
},
onRemove: e => {
console.log('builder remove');
//拖动时候的remove事件,不等于删除,删除时任何地方都没有了,remove的时候有个地方会add
// 做移除的时候,add的地方会先触发,如果不设定时器,add的地方会取不到e.item.__vue__
isAdd = true
setTimeout(() => {
this.data.splice(e.oldIndex, 1);
console.log(this.data, 'datatatatata');
this.emitChange();
}, 100);
}
});
},
methods: {
onFormSave() {
this.$emit('getData', {config: this.config, data: this.data})
},
onFormConfig() {
this.formConfigVisible = true;
},
onGlobalSettingClick() {
this.globalSettingVisible = true;
},
onPreviewClick() {
this.previewModalVisible = true;
},
emitChange() {
this.renderKey += 1;
this.$emit('change', {
config: this.config,
data: this.data
});
},
onDelete(e) {
this.data.splice(e.index, 1);
this.activeItem = {};
this.activeKey = null;
this.emitChange();
},
onItemCopy(e) {
let item = JSON.parse(JSON.stringify(e.data));

let key = Date.now();
item['key'] = key;
item['model'] = item.type + '_' + key;
this.data.splice(e.index + 1, 0, item);
this.activeItem = item;
this.activeKey = key;
this.emitChange();
},
onGridCopy(e) {
//栅格复制,需要更新内部所有的key
let item = JSON.parse(JSON.stringify(e.data));
// 替换替换栅格里的所有key
let key = Date.now();
recursiveReplace(item, key, 0);
this.data.splice(e.index + 1, 0, item);
this.activeItem = item;
this.activeKey = key;
this.emitChange();
},
onDrag(bool) {
this.draging = bool;
},
onActive(e) {
if (e) {
this.activeItem = e;
this.activeKey = e.key;
} else {
this.activeItem = {};
this.activeKey = null;
}
}
}
};
</script>
<style lang="less" scoped>
@border: 1px solid @border-color-base;
/deep/ .preview-modal {
height: 100%;

.ant-modal {
height: 100%;
top: 0;
padding-bottom: 0;
.ant-modal-content {
height: 100%;
overflow: hidden;
.ant-modal-body {
padding: 0;
height: calc(100% - 55px);
.top-toolbox {
padding: @padding-md;
height: 50px;
border-bottom: 1px solid @border-color-base;
display: flex;
justify-content: center;
align-items: center;
background-color: #f1f1f1;
.item {
padding: 0 @padding-md * 2;
/deep/ button {
margin-right: @padding-md;
}
}
}
.preview-content {
height: calc(100% - 66px);
padding: @padding-md / 2;
overflow: auto;
}
}
}
}
}

.form-builder-container {
height: 100%;
.builder-container {
display: flex;
&.header {
height: 45px;
border-bottom: @border;

.header-item {
align-items: center;
display: flex;
justify-content: space-between;
&:not(.builder) {
padding: 0 @padding-md;
}
span:nth-child(1) {
font-weight: bold;
}
}
}
.list,
.config {
flex: 0 0 290px;
.tips {
font-size: 12px;
color: #d1d1d1;
}
}
.builder {
flex: 1;
border-left: @border;
border-right: @border;
}

&.main {
height: calc(100% - 45px);
.builder {
background-image: linear-gradient(
90deg,
rgba(0, 0, 0, 0.02) 10%,
transparent 0
),
linear-gradient(rgba(0, 0, 0, 0.02) 10%, transparent 0);
background-size: 10px 10px;

/deep/ .scroll-content {
height: 100%;
.form-container {
min-height: 100%;
padding-bottom: 50px;
}
}
&.draging {
background-image: linear-gradient(
90deg,
@primary-1 10%,
transparent 0
),
linear-gradient(@primary-1 10%, transparent 0);
padding-top: 20px;
}

/deep/ .component-item {
width: 100%;
height: 5px;
background-color: @primary-color;
margin-left: 0;
.component-content {
display: none;
}
}
}

/deep/ .form-item-editor {
position: relative;
padding: 0 5px;
.overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
cursor: move;
z-index: 19;
}
.form-item-copy {
position: absolute;
right: 16px;
bottom: -4px;
width: 20px;
height: 20px;
border: 1px solid @primary-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
color: @primary-color;
z-index: 20;

cursor: pointer;
i,
svg {
font-size: 12px;
}
&:hover {
background-color: @primary-color;
color: #ffffff;
}
}
.form-item-delete {
position: absolute;
right: 40px;
bottom: -4px;
width: 20px;
height: 20px;
border: 1px solid @error-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
cursor: pointer;
color: @error-color;
z-index: 20;
i,
svg {
font-size: 12px;
}
&:hover {
background-color: @error-color;
color: #ffffff;
}
}
}
}
}
}
</style>

+ 96
- 0
src/components/form-builder/form-builder-modal/FormConfigData.vue ファイルの表示

@@ -0,0 +1,96 @@
<template>
<a-modal
:width="width"
title="表单配置"
wrapClassName="preview-modal"
v-model="modalVisible"
:footer="null"
:maskClosable="false"
@cancel="$emit('close',false)"
>
<a-tabs @change="onChange">
<a-tab-pane key="1" tab="表单界面配置数据">
<div :style="{height:height+'px'}">
<textarea ref="uiDom" :value="strUiConf"></textarea>
</div>
</a-tab-pane>
<a-tab-pane key="2" tab="表单字段配置数据">
<div :style="{height:height+'px'}">
<textarea ref="dbDom" :value="strDbConf"></textarea>
</div>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>

<script>
import * as CodeMirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/javascript/javascript.js';

export default {
props: ['visible', 'uiConf', 'dbConf'],
data() {
return {
width: window.innerWidth,
modalVisible: false,
CodeMirrorEditorUi: null,
height: window.innerHeight - 120,
CodeMirrorEditorDb: null
};
},
watch: {
visible: {
handler(v) {
this.modalVisible = v;
},
immediate: true
}
},
computed: {
strUiConf() {
return JSON.stringify(this.uiConf,null,'\t');
},
strDbConf() {
return JSON.stringify(this.dbConf,null,'\t');
}
},

mounted() {
this.$nextTick(() => {
this.CodeMirrorEditorUi = CodeMirror.fromTextArea(this.$refs.uiDom, {
lineNumbers: true, //是否显示每一行的行数
cursorHeight: 1,
mode: { name: 'text/javascript' },
theme: 'darcula',
styleActiveLine: true, // 当前行背景高亮
smartIndent: true
});
});
},
methods: {
onChange(e) {
if (e == 2 && !this.CodeMirrorEditorDb) {
this.$nextTick(() => {
this.CodeMirrorEditorDb = CodeMirror.fromTextArea(this.$refs.dbDom, {
lineNumbers: true, //是否显示每一行的行数
cursorHeight: 1,
mode: { name: 'text/javascript' },
theme: 'darcula',
styleActiveLine: true, // 当前行背景高亮
smartIndent: true
});
});
}
}
}
};
</script>

<style lang="less" scoped>

@import "~codemirror/lib/codemirror.css";
@import "~codemirror/theme/darcula.css";
/deep/ .CodeMirror {
height: 100%;
}
</style>

+ 121
- 0
src/components/form-builder/form-builder-modal/GlobalSetting.vue ファイルの表示

@@ -0,0 +1,121 @@
<template>
<a-modal
:width="660"
title="全局设置"
wrapClassName="global-setting-modal"
v-model="modalVisible"
:maskClosable="false"
@cancel="onCancel"
@ok="onOk"
>
<div class="global-setting-list">
<div class="global-setting-item">
<div class="label">标签宽度</div>
<div class="control">
<a-input-number v-model="globalConfig.labelCol" :min="0" :max="24" />
<span>与控件宽度之和不超过24</span>
</div>
</div>
<div class="global-setting-item">
<div class="label">控件宽度</div>
<div class="control">
<a-input-number v-model="globalConfig.wraperCol" :min="0" :max="24" />
<span>与标签宽度之和不超过24</span>
</div>
</div>
<div class="global-setting-item">
<div class="label">标签横向对齐方式</div>
<div class="control">
<a-radio-group v-model="globalConfig.labelAlign" buttonStyle="solid">
<a-radio-button value="left">左对齐</a-radio-button>
<a-radio-button value="center">居中</a-radio-button>
<a-radio-button value="right">右对齐</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="global-setting-item">
<div class="label">标签竖向对齐方式</div>
<div class="control">
<a-radio-group v-model="globalConfig.labelVertical" buttonStyle="solid">
<a-radio-button value="top">顶部对齐</a-radio-button>
<a-radio-button value="middle">居中</a-radio-button>
<a-radio-button value="bottom">底部对齐</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="global-setting-item">
<div class="label">表格模式</div>
<div class="control">
<a-switch v-model="globalConfig.tableStyle" />
<span>如果开启,响应式设置将会失效</span>
</div>
</div>
</div>
</a-modal>
</template>

<script>
export default {
model: {
prop: 'value',
event: 'change'
},
props: ['visible', 'value'],
data() {
return {
modalVisible: false,
globalConfig: {}
};
},
watch: {
visible: {
handler(v) {
this.modalVisible = v;
},
immediate: true
},
value: {
handler(v) {
if (v) {
this.globalConfig = JSON.parse(JSON.stringify(v));
} else {
this.globalConfig = {};
}
},
immediate: true,
deep: true
}
},
methods: {
onCancel() {
this.$emit('close', false);
},
onOk() {
this.$emit('change', this.globalConfig);
this.$emit('close', false);
}
}
};
</script>

<style lang="less" scoped>

.global-setting-list {
display: flex;
flex-wrap: wrap;
.global-setting-item {
width: 50%;
height: 90px;
display: flex;
flex-direction: column;
justify-content: center;
span {
font-size: 12px;
padding-left: @padding-md / 2;
}
.label {
margin-bottom: @padding-md / 2;
}
}
}
</style>

+ 152
- 0
src/components/form-builder/form-builder-modal/PreviewModal.vue ファイルの表示

@@ -0,0 +1,152 @@
<template>
<a-modal
:width="width"
title="预览"
:wrapClassName="'preview-modal preview-mock ' + layout"
v-model="previewModalVisible"
:footer="null"
:maskClosable="false"
@cancel="$emit('close',false)"
>
<div class="top-toolbox">
<span class="item">
<span>布局:</span>
<a-select v-model="layout" style="width: 160px" >
<a-select-option value="none">无</a-select-option>
<a-select-option value="banner">banner (1200px)</a-select-option>
<a-select-option value="xs" :disabled="uiConf && uiConf.config && uiConf.config.tableStyle">xs (&lt;576px)</a-select-option>
<a-select-option value="sm" :disabled="uiConf && uiConf.config && uiConf.config.tableStyle">sm (≥576px)</a-select-option>
<a-select-option value="md" :disabled="uiConf && uiConf.config && uiConf.config.tableStyle">md (≥768px)</a-select-option>
<a-select-option value="lg" :disabled="uiConf && uiConf.config && uiConf.config.tableStyle">lg (≥992px)</a-select-option>
<a-select-option value="xl" :disabled="uiConf && uiConf.config && uiConf.config.tableStyle">xl (≥1200px)</a-select-option>
<a-select-option value="xxl" :disabled="uiConf && uiConf.config && uiConf.config.tableStyle">xxl (≥1600px)</a-select-option>
</a-select>
</span>
<span class="item">
<span>模式:</span>
<a-select v-model="formMode" style="width: 160px">
<a-select-option value="edit">编辑模式</a-select-option>
<a-select-option value="detail">详情模式</a-select-option>
</a-select>
</span>
<span class="item" v-if="formMode == 'edit'">
<a-button type="primary" @click="onFormSubmit">提交表单</a-button>
<a-button type="danger" ghost @click="onFormReset">重置</a-button>
</span>
</div>
<div class="preview-content" :class="['mock-' + layout]">
<Form
:uiConf="uiConf"
:gateway="gateway"
:userModel="userModel"
@getform="getPreviewForm"
:mode="formMode"
:banner="layout == 'banner' ? 1200 : false"
:mock="layout"
/>
</div>
<a-modal title="表单验证结果" v-model="resultModalVisible" :footer="null">
<div class="result-item header">{{results?results:'表单验证不通过'}}</div>
</a-modal>
</a-modal>
</template>

<script>
import Form from '../Form';
import message from 'ant-design-vue/es/message';

export default {
props: ['uiConf', 'visible', 'gateway', 'userModel'],
components: { Form },
data() {
return {
previewModalVisible: false,
resultModalVisible: false,

layout: 'none',
formMode: 'edit',
width: window.innerWidth,
results: []
};
},
watch: {
visible: {
handler(v) {
this.previewModalVisible = v;
this.layout = 'none';
this.formMode = 'edit';
},
immediate: true
}
},
methods: {
onGlobalSettingClick() {},
onFormSubmit() {
if (this.previewForm) {
this.previewForm.validateFields((err, values) => {
if (err) {
message.error('表单验证失败');
this.results = null;
} else {
message.success('表单验证通过');
this.results = values;
}
this.resultModalVisible = true;
});
}
},
onFormReset() {
if (this.previewForm) {
this.$confirm({
title: '提示',
content: '确认要重置表单的值吗?',
onOk: () => {
this.previewForm.resetFields();
}
});
}
},
getPreviewForm(form) {
this.previewForm = form;
}
}
};
</script>
<style lang="less" scoped>

/deep/ .preview-mock {
.ant-modal-body {
background-color: rgba(0, 0, 0, 0.5);
}
&.none {
.ant-modal-body {
background-color: transparent;
}
}

.preview-content {
background-color: #fff;
margin: 0 auto;
padding: 0;
height: calc(100% - 50px) !important;
&.mock-banner {
width: 1260px;
}
&.mock-xs {
width: 576px;
}
&.mock-sm {
width: 760px;
}
&.mock-md {
width: 880px;
}
&.mock-lg {
width: 1000px;
}
&.mock-xl {
width: 1400px;
}
}
}
</style>

+ 251
- 0
src/components/form-builder/form-editor/FormGridEditor.vue ファイルの表示

@@ -0,0 +1,251 @@
<template>
<div
class="form-grid-editor form-item-editor"
:class="[activeKey === formItem.key ?'form-item-active':'']"
@click.stop.prevent="onItemActive"
>
<a-row
type="flex"
:align="formItem.align || 'top'"
:justify="formItem.justify || 'start'"
class="form-grid-editor-row"
>
<a-col
v-for="(colItem,index) in formItem.columns"
class="col-item"
:offset="colItem.offset"
:span="colItem.span"
:pull="colItem.pull"
:push="colItem.push"
:xs="colItem.isResponvive?colItem.xs:colItem.span"
:sm="colItem.isResponvive?colItem.sm:colItem.span"
:md="colItem.isResponvive?colItem.md:colItem.span"
:lg="colItem.isResponvive?colItem.lg:colItem.span"
:xl="colItem.isResponvive?colItem.xl:colItem.span"
:xxl="colItem.isResponvive?colItem.xxl:colItem.span"
:key="index"
>
<div class="col-border">
<div class="col-grid-list" :data-id="colItem.key" :data-index="index" ref="colbody">
<template v-for="(colListItem,cIndex) in colItem.list">
<FormGridEditor
v-if="colListItem.type == 'Grid'"
:key="renderKey+'_'+cIndex"
:formItem="colListItem"
:activeKey="activeKey"
:form="form"
:index="cIndex"
:config="config"
@active="onSubAcitve"
@copy="onGridCopy($event,index)"
@delete="onSubDelete($event,index)"
/>
<FormItemEditor
v-else
:key="renderKey+'_'+cIndex"
:formItem="colListItem"
:form="form"
:activeKey="activeKey"
:config="config"
:index="cIndex"
@active="onSubAcitve"
@copy="onItemCopy($event,index)"
@delete="onSubDelete($event,index)"
/>
</template>
</div>
</div>
</a-col>
<template v-if="activeKey === formItem.key">
<div class="active-bar"></div>
<div class="form-item-copy" @click.stop.prevent="onCopy" title="复制">
<a-icon type="copy" />
</div>
<div class="form-item-delete" @click.stop.prevent="onDelete" title="删除">
<a-icon type="minus" />
</div>
</template>
</a-row>
</div>
</template>

<script>
import FormItemEditor from './FormItemEditor';
import Sortable from 'sortablejs';
import { getFormConfig, recursiveReplace } from '../utils/form-config.util';

export default {
name: 'FormGridEditor',
props: ['formItem', 'form', 'config', 'activeKey', 'index'],
components: { FormItemEditor },
data() {
return {
sortInstance: [],
renderKey: 0
};
},
mounted() {},
watch: {
formItem: {
handler(v) {
this.sortableInit();
},
immediate: true,
deep: true
}
},
beforeDestroy() {
this.destorySortInstance();
},
methods: {
onItemCopy(e, colIndex) {
let item = JSON.parse(JSON.stringify(e.data));
let key = Date.now();
item['key'] = key;
item['model'] = item.type + '_' + key;
this.formItem.columns[colIndex].list.splice(e.index + 1, 0, item);
this.$emit('change', this.formItem);
this.$emit('active', item);
},
onSubDelete(e, colIndex) {
console.log(this.formItem, '前');
this.formItem.columns[colIndex].list.splice(e.index, 1);
console.log(this.formItem, '后');
this.$emit('change', this.formItem);
this.$emit('active', null);
},
onGridCopy(e, colIndex) {
let item = JSON.parse(JSON.stringify(e.data));
let key = Date.now();
recursiveReplace(item, key, 0);
this.formItem.columns[colIndex].list.splice(e.index + 1, 0, item);
this.$emit('change', this.formItem);
this.$emit('active', item);
},

onCopy() {
this.$emit('copy', { data: this.formItem, index: this.index });
},
onDelete() {
this.$emit('delete', { data: this.formItem, index: this.index });
},
onItemActive() {
console.log('我是在 grid里');
this.$emit('active', this.formItem);
},
onSubAcitve(e) {
console.log('我是在item里 grid');

this.$emit('active', e);
},
destorySortInstance() {
this.sortInstance.forEach(item => {
if (item.destory) {
item.destory();
}
});
},
sortableInit() {
this.$nextTick(() => {
this.destorySortInstance(); //节约内存,摧毁已经实例过的desotry
this.$refs.colbody.forEach(item => {
let isAdd = false;
let sortinstance = new Sortable(item, {
group: { name: 'component' },
animation: 200,
onSort: e => {
if (!isAdd) {
const tempData = this.formItem.columns[
Number(e.target.dataset.index)
].list[e.oldIndex];

const newData = this.formItem.columns[
Number(e.target.dataset.index)
].list[e.newIndex];
if (!newData) {
//如果没有newData,则表示是从栅格里面拖出来的,拖到最后面去了
this.formItem.columns[
Number(e.target.dataset.index)
].list.splice(e.oldIndex, 1);
} else {
if (!tempData.key) return;
if (e.newIndex !== e.oldIndex) {
this.formItem.columns[
Number(e.target.dataset.index)
].list.splice(e.oldIndex, 1);
this.formItem.columns[
Number(e.target.dataset.index)
].list.splice(e.newIndex, 0, tempData);
}
}
this.renderKey += 1;
this.$emit('change', this.formItem);
}
isAdd = false;
},
onAdd: e => {
isAdd = true;
console.log(e, 'eeee');
let defaultConfig = null;
if (e.item.dataset.source == 'formList') {
//左侧进入的
defaultConfig = getFormConfig(e.item.dataset.id);
} else {
//是从现有的控件拖过来的
console.log(e.item.__vue__.formItem, 'eeee');
defaultConfig = e.item.__vue__.formItem;
}

this.formItem.columns[Number(e.target.dataset.index)].list.splice(
e.newIndex,
0,
defaultConfig
);

this.$emit('active', defaultConfig);
this.$emit('change', this.formItem);
e.item.parentNode.removeChild(e.item);
},
onRemove: e => {
// 做移除的时候,add的地方会先触发,如果不设定时器,add的地方会取不到e.item.__vue__
setTimeout(() => {
this.formItem.columns[
Number(e.target.dataset.index)
].list.splice(e.newIndex, 1);
this.$emit('change', this.formItem);
}, 100);
}
});
this.sortInstance.push(sortinstance);
});
});
}
}
};
</script>

<style lang="less" scoped>

@table-border: 1px solid @border-color-base;
.form-grid-editor {
background-color: #f4f6fc;
.col-border {
border: 1px dashed @border-color-base;
.col-grid-list {
padding-bottom: 50px;
}
}
&.form-item-active {
background-color: @primary-1;
.active-bar {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 5px;
background-color: @primary-color;
}
}
}
</style>

+ 55
- 0
src/components/form-builder/form-editor/FormItemEditor.vue ファイルの表示

@@ -0,0 +1,55 @@
<template>
<div class="form-item-editor" :class="[activeKey === formItem.key ?'form-item-active':'']">
<FormItemRender :formItem="formItem" :form="form" :config="config" mode="edit" />
<div class="overlay" @click.stop.prevent="onItemActive"></div>
<template v-if="activeKey === formItem.key">
<div class="active-bar"></div>
<div class="form-item-copy" @click.stop.prevent="onCopy" title="复制">
<a-icon type="copy" />
</div>
<div class="form-item-delete" @click.stop.prevent="onDelete" title="删除">
<a-icon type="minus" />
</div>
</template>
</div>
</template>

<script>
import FormItemRender from '../form-render/FormItemRender';

export default {
props: ['formItem', 'form', 'config', 'activeKey', 'index'],
components: { FormItemRender },
methods: {
onItemActive() {
console.log('我是在item里');
this.$emit('active', this.formItem);
},
onCopy() {
this.$emit('copy', { data: this.formItem, index: this.index });
},
onDelete() {
console.log(1)
this.$emit('delete', { data: this.formItem, index: this.index });
},
}
};
</script>

<style lang="less" scoped>

.form-item-editor {
background-color: #f4f6fc;
&.form-item-active {
background-color: @primary-1;
.active-bar {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 5px;
background-color: @primary-color;
}
}
}
</style>

+ 0
- 0
src/components/form-builder/form-item/button/Button.vue ファイルの表示


+ 93
- 0
src/components/form-builder/form-item/checkbox/Checkbox.vue ファイルの表示

@@ -0,0 +1,93 @@
<template>
<span class="hb-checkbox" :class="[formItem.orientation]">
<a-checkbox-group :options="options" :value="data" @change="onChange" v-if="mode !== 'detail'">
<a-checkbox
v-for="(option,index) in options"
:key="index"
:value="option.value"
>{{option.label}}</a-checkbox>
</a-checkbox-group>
<template v-else>{{ detailText }}</template>
</span>
</template>

<script>
import message from 'ant-design-vue/es/message';

export default {
model: {
prop: 'value',
event: 'change'
},
props: ['formItem', 'form', 'config', 'value', 'mode'],
data() {
return {
options: [],
data: [],
radioStyle: {}
};
},
computed:{
detailText(){
let names = [];
this.options.forEach(item =>{
if(this.data.includes(item.value)){
names.push(item.label)
}
})
return names.join(',');
}
},
watch: {
value: {
handler(v) {
if (v && typeof v === 'string') {
this.data = v.split(',');
} else {
this.data = [];
}
},
immediate: true
},
formItem: {
handler(v) {
if (v && Array.isArray(v.options)) {
this.options = v.options;
} else {
this.options = [];
}
},
immediate: true,
deep: true
}
},
methods: {
onChange(e) {
if (
this.formItem &&
this.formItem.maxCount >= 1 &&
e.length > this.formItem.maxCount
) {
//超出最大个数,提示,报错
message.error(`超出当前允许最大选择个数: ${this.formItem.maxCount}`);
return;
}

this.$emit('change', e.join(','));
}
}
};
</script>

<style lang="less" scoped>

.hb-checkbox {
&.vertical {
/deep/ .ant-checkbox-wrapper {
display: block;
height: 30px;
line-height: 30px;
}
}
}
</style>

+ 118
- 0
src/components/form-builder/form-item/checkbox/config.js ファイルの表示

@@ -0,0 +1,118 @@
import {
ALIGN_OPTIONS,
VERTICAL_OPTIONS,
DEMO_OPTIONS,
} from '../common.config'

export default {
name: '多选按钮',
type: 'Checkbox',
realForm: true,
default_config: {
title: '多选按钮',
hideTitle: false,
labelOverflow: 'ellipsis',
defaultValue: '',
labelBlock: false,
labelAlign: null,
labelVertical: null,
labelCol: null,
wraperCol: null,
rules: [],
placeholder: '请选择',
options: JSON.parse(JSON.stringify(DEMO_OPTIONS)),
orientation: 'horizontal',
maxCount: 0
},
config_render_list: [{
fields: 'title',
type: 'Text',
title: '标题',
placeholder: '字段别名'
},
{
fields: 'hideTitle',
type: 'Checkbox',
title: '隐藏标题'
},
{
fields: 'labelOverflow',
type: 'Radio',
options: [{
label: '省略号',
value: 'ellipsis'
},
{
label: '换行',
value: 'break'
},
],
title: '标题溢出处理方式'
},
{
fields: 'defaultValue',
type: 'Text',
title: '默认值'
},
{
fields: 'maxCount',
type: 'Number',
title: '允许最多选择个数(大于1生效)'
},
{
fields: 'options',
type: 'EditOption',
title: '选项设置'
},
{
fields: 'orientation',
type: 'Radio',
title: '选项排列方向',
options: [{
label: '横向',
value: 'horizontal'
}, {
label: '竖向',
value: 'vertical'
}]
},
{
fields: 'placeholder',
type: 'Text',
title: '提示文字',
placeholder: '请输入提示文字'
},
{
fields: 'rules',
type: 'Rules',
title: '校验规则'
},
{
fields: 'labelCol',
type: 'Number',
title: '标题宽度'
},
{
fields: 'wraperCol',
type: 'Number',
title: '控件宽度'
},
{
fields: 'labelBlock',
type: 'Checkbox',
title: '标题独占一行'
},
{
fields: 'labelAlign',
type: 'Radio',
options: ALIGN_OPTIONS,
title: '标题对齐方式'
},
{
fields: 'labelVertical',
type: 'Radio',
options: VERTICAL_OPTIONS,
title: '标题竖向对齐方式'
},
]
}

+ 55
- 0
src/components/form-builder/form-item/common.config.js ファイルの表示

@@ -0,0 +1,55 @@
export const ALIGN_OPTIONS = [{
label: '左对齐',
value: 'left'
},
{
label: '居中',
value: 'center'
},
{
label: '右对齐',
value: 'right'
}
]

export const VERTICAL_OPTIONS = [{
label: '顶部对齐',
value: 'top'
},
{
label: '居中对齐',
value: 'middle'
},
{
label: '底部对齐',
value: 'bottom'
}
]

export const DEMO_OPTIONS = [{
label: '选项1',
value: 'option1'
},
{
label: '选项2',
value: 'option2'
},
{
label: '选项3',
value: 'option3'
}
]

export const DEMO_TAG_OPTIONS = ['标签1', '标签2', '标签3']

export const INPUTFIX_OPTIONS = [{
label: '文本',
value: 'text'
},
{
label: '图标',
value: 'icon'
}
]

export const hbConfigKey = ['title', 'hideTitle', 'labelOverflow', 'labelBlock', 'labelAlign', 'labelCol', 'wraperCol', 'rules']

+ 55
- 0
src/components/form-builder/form-item/date-range/DateRange.vue ファイルの表示

@@ -0,0 +1,55 @@
<template>
<span>
<a-range-picker
v-if="mode !== 'detail'"
v-model="data"
:showTime="!!formItem.showTime"
@change="onChange"
:placeholder="[formItem?formItem.placeholder:undefined,formItem?formItem.placeholder:undefined]"
/>
<template v-else>{{ detailText }}</template>
</span>
</template>

<script>
import moment from 'moment';
export default {
model: {
prop: 'value',
event: 'change'
},
props: ['formItem', 'form', 'config', 'value', 'mode'],
data() {
return {
data: []
};
},
computed:{
detailText(){
return (this.value || []).join('~');
}
},
watch: {
value: {
handler(v) {
if (v && v.length >= 2) {
this.data = [moment(v[0]), moment(v[1])];
} else {
this.data = [];
}
},
immediate: true
}
},
methods: {
onChange(e) {
let format =
this.formItem && this.formItem.showTime
? 'YYYY-MM-DD HH:mm:ss'
: 'YYYY-MM-DD';
this.$emit('change', [e[0].format(format), e[1].format(format)]);
}
}
};
</script>

+ 98
- 0
src/components/form-builder/form-item/date-range/config.js ファイルの表示

@@ -0,0 +1,98 @@
import {
ALIGN_OPTIONS,
VERTICAL_OPTIONS,
} from '../common.config'
export default {
name: '日期区间',
type: 'DateRange',
realForm: true,
default_config: {
title: '日期区间',
hideTitle: false,
labelOverflow: 'ellipsis',
defaultValue: undefined,
labelBlock: false,
labelAlign: null,
labelVertical: null,
labelCol: null,
wraperCol: undefined,
rules: [],
placeholder: '请选择',
showTime: false,
},
config_render_list: [{
fields: 'title',
type: 'Text',
title: '标题',
placeholder: '字段别名'
},
{
fields: 'hideTitle',
type: 'Checkbox',
title: '隐藏标题'
},
{
fields: 'labelOverflow',
type: 'Radio',
options: [{
label: '省略号',
value: 'ellipsis'
},
{
label: '换行',
value: 'break'
},
],
title: '标题溢出处理方式'
},
{
fields: 'defaultValue',
type: 'DateRange',
title: '默认值'
},
{
fields: 'showTime',
type: 'Checkbox',
title: '是否显示时间'
},
{
fields: 'placeholder',
type: 'Text',
title: '提示文字',
placeholder: '请输入提示文字'
},
{
fields: 'rules',
type: 'Rules',
title: '校验规则'
},
{
fields: 'labelCol',
type: 'Number',
title: '标题宽度'
},
{
fields: 'wraperCol',
type: 'Number',
title: '控件宽度'
},
{
fields: 'labelBlock',
type: 'Checkbox',
title: '标题独占一行'
},
{
fields: 'labelAlign',
type: 'Radio',
options: ALIGN_OPTIONS,
title: '标题对齐方式'
},
{
fields: 'labelVertical',
type: 'Radio',
options: VERTICAL_OPTIONS,
title: '标题竖向对齐方式'
},

]
}

+ 49
- 0
src/components/form-builder/form-item/date-time/DateTime.vue ファイルの表示

@@ -0,0 +1,49 @@
<template>
<span>
<a-date-picker
v-if="mode !== 'detail'"
v-model="data"
:showTime="!!formItem.showTime"
@change="onChange"
:placeholder="formItem?formItem.placeholder:undefined"
/>
<template v-else>{{ value }}</template>
</span>
</template>

<script>
import moment from 'moment';
export default {
model: {
prop: 'value',
event: 'change'
},
props: ['formItem', 'form', 'config', 'value','mode'],
data() {
return {
data: undefined
};
},
watch: {
value: {
handler(v) {
if (v) {
this.data = moment(v);
} else {
this.data = undefined;
}
},
immediate: true
}
},
methods: {
onChange(e) {
let format =
this.formItem && this.formItem.showTime
? 'YYYY-MM-DD HH:mm:ss'
: 'YYYY-MM-DD';
this.$emit('change', e.format(format));
}
}
};
</script>

+ 98
- 0
src/components/form-builder/form-item/date-time/config.js ファイルの表示

@@ -0,0 +1,98 @@
import {
ALIGN_OPTIONS,
VERTICAL_OPTIONS,
} from '../common.config'
export default {
name: '日期时间',
type: 'Datetime',
realForm: true,
default_config: {
title: '日期时间',
hideTitle: false,
labelOverflow: 'ellipsis',
defaultValue: undefined,
labelBlock: false,
labelAlign: null,
labelVertical: null,
labelCol: null,
wraperCol: undefined,
rules: [],
placeholder: '请选择',
showTime: false
},
config_render_list: [{
fields: 'title',
type: 'Text',
title: '标题',
placeholder: '字段别名'
},
{
fields: 'hideTitle',
type: 'Checkbox',
title: '隐藏标题'
},
{
fields: 'labelOverflow',
type: 'Radio',
options: [{
label: '省略号',
value: 'ellipsis'
},
{
label: '换行',
value: 'break'
},
],
title: '标题溢出处理方式'
},
{
fields: 'defaultValue',
type: 'Datetime',
title: '默认值'
},
{
fields: 'showTime',
type: 'Checkbox',
title: '是否显示时间'
},
{
fields: 'placeholder',
type: 'Text',
title: '提示文字',
placeholder: '请输入提示文字'
},
{
fields: 'rules',
type: 'Rules',
title: '校验规则'
},
{
fields: 'labelCol',
type: 'Number',
title: '标题宽度'
},
{
fields: 'wraperCol',
type: 'Number',
title: '控件宽度'
},
{
fields: 'labelBlock',
type: 'Checkbox',
title: '标题独占一行'
},
{
fields: 'labelAlign',
type: 'Radio',
options: ALIGN_OPTIONS,
title: '标题对齐方式'
},
{
fields: 'labelVertical',
type: 'Radio',
options: VERTICAL_OPTIONS,
title: '标题竖向对齐方式'
},

]
}

変更されたファイルが多すぎるため、一部のファイルは表示されません

読み込み中…
キャンセル
保存