持续集成分支
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -27,3 +27,6 @@ yarn-error.log* | |||||||
| /project/*/dist | /project/*/dist | ||||||
| /ui/package-lock.json | /ui/package-lock.json | ||||||
| /examples/modules.json | /examples/modules.json | ||||||
|  | /examples/router/apps.js | ||||||
|  | /src/apps/ | ||||||
|  | /src/config.json | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								.npmrc
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								.npmrc
									
									
									
									
									
								
							| @@ -1,5 +1,4 @@ | |||||||
| registry=http://192.168.1.87:4873/ |  | ||||||
| email=aixianling@sinoecare.com | email=aixianling@sinoecare.com | ||||||
| always-auth=true |  | ||||||
| package-lock=false | package-lock=false | ||||||
| //192.168.1.87:4873/:_auth="YWRtaW46YWRtaW4xMjM=" | registry=http://registry.npmmirror.com | ||||||
|  | legacy-peer-deps=true | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								bin/scanApps.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								bin/scanApps.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | const {chalkTag, findApp, fs, fsExtra} = require("./tools"); | ||||||
|  | const compiler = require('vue-template-compiler') | ||||||
|  | const getAppInfo = (file, apps) => { | ||||||
|  |   if (/[\\\/](App[A-Z][^\\\/]+)\.vue$/g.test(file)) { | ||||||
|  |     const name = file.replace(/.+[\\\/](App[^\\\/]+)\.vue$/, '$1'), | ||||||
|  |       source = fs.readFileSync(file).toString(), | ||||||
|  |       parsed = compiler.parseComponent(source), | ||||||
|  |       script = parsed.script?.content || "", | ||||||
|  |       label = script.match(/label:[^,]+/)?.[0]?.replace(/.+["']([^"']+).+/, '$1') | ||||||
|  |     const paths = file.split(/[\\\/]/) | ||||||
|  |     apps.push({ | ||||||
|  |       id: file.replace(/\.vue$/, '').replace(/[\\\/]/g, '_'), | ||||||
|  |       label: label || name, | ||||||
|  |       path: `/${file.replace(/\.vue$/, '').replace(/[\\\/]/g, '/')}`, | ||||||
|  |       workspace: paths.at(0), | ||||||
|  |       esm: file.replace(/[\\\/]/g, '/'), | ||||||
|  |       name | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const start = () => { | ||||||
|  |   chalkTag.info("开始扫描库工程...") | ||||||
|  |   const list = [] | ||||||
|  |   Promise.all([ | ||||||
|  |     findApp('packages', app => getAppInfo(app, list)), | ||||||
|  |     findApp('project', app => getAppInfo(app, list)), | ||||||
|  |   ]).then(() => { | ||||||
|  |     fsExtra.outputFile('examples/router/apps.js', `export default [${list.map(e => { | ||||||
|  |       const {name, label, path, esm} = e | ||||||
|  |       return `{name:"${name}",label:"${label}",path:"${path}",component:()=>import("@${esm}")}` | ||||||
|  |     }).join(',\n')}]`) | ||||||
|  |     chalkTag.done("扫描完毕") | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | start() | ||||||
| @@ -39,10 +39,10 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapState(['user', 'apps']), |     ...mapState(['user']), | ||||||
|     navs() { |     navs() { | ||||||
|       let reg = new RegExp(`.*${this.searchApp?.replace(/-/g,'')||''}.*`, 'gi') |       let reg = new RegExp(`.*${this.searchApp?.replace(/-/g,'')||''}.*`, 'gi') | ||||||
|       return (this.apps || []).filter(e => !this.searchApp || reg?.test(e.name) || reg?.test(e.label)).map(e => { |       return (this.$apps || []).filter(e => !this.searchApp || reg?.test(e.name) || reg?.test(e.label)).map(e => { | ||||||
|         if (/\/project\//.test(e.path)) { |         if (/\/project\//.test(e.path)) { | ||||||
|           e.project = process.env.VUE_APP_SCOPE || e.path.replace(/.*project\/([^\/]+)\/.+/, '$1') |           e.project = process.env.VUE_APP_SCOPE || e.path.replace(/.*project\/([^\/]+)\/.+/, '$1') | ||||||
|         } else if (/\/core\//.test(e.path)) { |         } else if (/\/core\//.test(e.path)) { | ||||||
| @@ -52,9 +52,9 @@ export default { | |||||||
|       }) |       }) | ||||||
|     }, |     }, | ||||||
|     menuPath() { |     menuPath() { | ||||||
|       let paths = [], current = this.apps?.find(e => e.name == this.$route.name) |       let paths = [], current = this.$apps?.find(e => e.name == this.$route.name) | ||||||
|       const findParent = name => { |       const findParent = name => { | ||||||
|         let menu = this.apps?.find(e => e.name == name) |         let menu = this.$apps?.find(e => e.name == name) | ||||||
|         if (menu) { |         if (menu) { | ||||||
|           paths.push(menu.name) |           paths.push(menu.name) | ||||||
|           if (!!menu.parentId) findParent(menu.parentId) |           if (!!menu.parentId) findParent(menu.parentId) | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import App from './App.vue'; | import App from './App.vue'; | ||||||
| import ui from 'element-ui'; | import ui from 'element-ui'; | ||||||
| import router from './router/router'; | import router from './router'; | ||||||
| import axios from './router/axios'; | import axios from './router/axios'; | ||||||
| import utils from './utils'; | import utils from './utils'; | ||||||
| import dui from 'dui'; | import dui from 'dui'; | ||||||
| @@ -22,6 +22,7 @@ const app = new Vue({ | |||||||
|   store, |   store, | ||||||
|   render: h => h(App) |   render: h => h(App) | ||||||
| }); | }); | ||||||
|  |  | ||||||
| let theme = null | let theme = null | ||||||
| store.dispatch('getSystem').then(res => { | store.dispatch('getSystem').then(res => { | ||||||
|   theme = JSON.parse(res?.colorScheme || null) |   theme = JSON.parse(res?.colorScheme || null) | ||||||
|   | |||||||
| @@ -1,81 +0,0 @@ | |||||||
| import store from "../store"; |  | ||||||
| import {waiting} from "../utils"; |  | ||||||
| import appEntry from "../views/appEntry"; |  | ||||||
| import router from "./router"; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   routes: () => store.state.apps, |  | ||||||
|   init() { |  | ||||||
|     //约束正则式 |  | ||||||
|     store.commit("cleanApps") |  | ||||||
|     // 自动化本工程应用 |  | ||||||
|     waiting.init({innerHTML: '应用加载中..'}) |  | ||||||
|     let startTime = new Date().getTime() |  | ||||||
|     /** |  | ||||||
|      * require.context 的路径变量范式只能为静态字符串 |  | ||||||
|      */ |  | ||||||
|     switch (process.env.VUE_APP_SCOPE) { |  | ||||||
|       case 'dv': |  | ||||||
|         this.esm = { |  | ||||||
|           packages: require.context('../../packages/bigscreen', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy') |  | ||||||
|         } |  | ||||||
|         break |  | ||||||
|       case 'fengdu': |  | ||||||
|         this.esm = { |  | ||||||
|           project: require.context('../../project/fengdu', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy') |  | ||||||
|         } |  | ||||||
|         break |  | ||||||
|       case 'ai': |  | ||||||
|         this.esm = { |  | ||||||
|           biaopin: require.context('../../project/biaopin/AppCopilotConfig', true, /\.\/App[A-Z][^\/]+\.vue$/, 'lazy'), |  | ||||||
|           project: require.context('../../project/ai', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy') |  | ||||||
|         } |  | ||||||
|         break |  | ||||||
|       case 'oms': |  | ||||||
|         this.esm = { |  | ||||||
|           project: require.context('../../project/oms', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy') |  | ||||||
|         } |  | ||||||
|         break |  | ||||||
|       default: |  | ||||||
|         this.esm = { |  | ||||||
|           packages: require.context('../../packages/', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy'), |  | ||||||
|           project: require.context('../../project/', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, 'lazy') |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     console.log('模块引用用了%s秒', (new Date().getTime() - startTime) / 1000) |  | ||||||
|     startTime = new Date().getTime() |  | ||||||
|     this.loadApps().finally(() => { |  | ||||||
|       console.log('模块加载用了%s秒', (new Date().getTime() - startTime) / 1000) |  | ||||||
|       waiting.close() |  | ||||||
|     }) |  | ||||||
|   }, |  | ||||||
|   loadMods() { |  | ||||||
|     // return Promise.all(mods.apps.map(e => { |  | ||||||
|     //   Vue.component(e.name, this.esm[e.workspace](e.esm)) |  | ||||||
|     //   const addApp = {...e, component: appEntry} |  | ||||||
|     //   waiting.setContent(`加载${e.name}...`) |  | ||||||
|     //   //命名规范入口文件必须以App开头 |  | ||||||
|     //   return store.commit("addApp", addApp) |  | ||||||
|     // })) |  | ||||||
|   }, |  | ||||||
|   loadApps() { |  | ||||||
|     //新App的自动化格式 |  | ||||||
|     const promise = (mods, base) => Promise.all(mods.keys().map(path => mods(path).then(file => { |  | ||||||
|       if (file.default) { |  | ||||||
|         const {name, label} = file.default |  | ||||||
|         const addApp = { |  | ||||||
|           name: [base, path.replace(/\.\/?(vue)?/g, '')?.split("/")].flat().join("_"), |  | ||||||
|           label: label || name, |  | ||||||
|           path: `/${base}${path.replace(/\.(\/.+\/App.+)\.vue$/, '$1')}`, |  | ||||||
|           component: appEntry, |  | ||||||
|           esm: file.default |  | ||||||
|         } |  | ||||||
|         waiting.setContent(`加载${name}...`) |  | ||||||
|         router.addRoute(addApp) |  | ||||||
|         //命名规范入口文件必须以App开头 |  | ||||||
|         return store.commit("addApp", addApp) |  | ||||||
|       } else return 0 |  | ||||||
|     }).catch(err => console.log(err)))) |  | ||||||
|     return Promise.all(Object.entries(this.esm).map(([root, mods]) => promise(mods, root))).catch(console.error) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,13 +1,13 @@ | |||||||
| import Vue from 'vue' | import Vue from 'vue' | ||||||
| import VueRouter from 'vue-router' | import VueRouter from 'vue-router' | ||||||
| import autoRoutes from './autoRoutes' | import apps from "./apps"; | ||||||
| 
 | 
 | ||||||
| Vue.use(VueRouter) | Vue.use(VueRouter) | ||||||
| autoRoutes.init() | Vue.prototype.$apps = apps | ||||||
| export default new VueRouter({ | export default new VueRouter({ | ||||||
|   mode: 'history', |   mode: 'history', | ||||||
|   hashbang: false, |   hashbang: false, | ||||||
|   routes: autoRoutes.routes(), |   routes: apps, | ||||||
|   scrollBehavior(to) { |   scrollBehavior(to) { | ||||||
|     if (to.hash) { |     if (to.hash) { | ||||||
|       return { |       return { | ||||||
| @@ -5,22 +5,8 @@ import * as modules from "dui/lib/js/modules" | |||||||
| import xsActions from "../../project/xiushan/actions" | import xsActions from "../../project/xiushan/actions" | ||||||
|  |  | ||||||
| Vue.use(Vuex) | Vue.use(Vuex) | ||||||
|  |  | ||||||
| export default new Vuex.Store({ | export default new Vuex.Store({ | ||||||
|   state: { |   actions: {...xsActions}, | ||||||
|     apps: [] |  | ||||||
|   }, |  | ||||||
|   mutations: { |  | ||||||
|     addApp(state, app) { |  | ||||||
|       state.apps.push(app) |  | ||||||
|     }, |  | ||||||
|     cleanApps(state) { |  | ||||||
|       state.apps = [] |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   actions: { |  | ||||||
|     ...xsActions |  | ||||||
|   }, |  | ||||||
|   modules, |   modules, | ||||||
|   plugins: [preState()] |   plugins: [preState()] | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -15,7 +15,8 @@ | |||||||
|     "preui": "npm publish -ws||(npm unpublish -f -ws&&npm publish -ws)", |     "preui": "npm publish -ws||(npm unpublish -f -ws&&npm publish -ws)", | ||||||
|     "ui": "npm i dui@latest @dui/dv@latest", |     "ui": "npm i dui@latest @dui/dv@latest", | ||||||
|     "sync": "node bin/appsSync.js", |     "sync": "node bin/appsSync.js", | ||||||
|     "preview": "vue-cli-service serve" |     "preview": "vue-cli-service serve", | ||||||
|  |     "predev": "node bin/scanApps.js" | ||||||
|   }, |   }, | ||||||
|   "workspaces": [ |   "workspaces": [ | ||||||
|     "ui", |     "ui", | ||||||
| @@ -33,6 +34,7 @@ | |||||||
|     "bin-ace-editor": "^3.2.0", |     "bin-ace-editor": "^3.2.0", | ||||||
|     "dayjs": "^1.8.35", |     "dayjs": "^1.8.35", | ||||||
|     "dui": "^2.0.0", |     "dui": "^2.0.0", | ||||||
|  |     "echarts": "^5.5.1", | ||||||
|     "echarts-wordcloud": "^2.0.0", |     "echarts-wordcloud": "^2.0.0", | ||||||
|     "hash.js": "^1.1.7", |     "hash.js": "^1.1.7", | ||||||
|     "html2canvas": "^1.4.1", |     "html2canvas": "^1.4.1", | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | <template> | ||||||
|  |   <div id="app" :class="{greyFilter,[`theme-${$theme}`]:true}"> | ||||||
|  |     <router-view/> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapState} from "vuex"; | ||||||
|  | import customConfig from "./config.json"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'app', | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['sys']), | ||||||
|  |     greyFilter: v => v.sys?.theme?.enableGreyFilter == 1, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     initFavicon(icon) { | ||||||
|  |       const linkList = document.head.querySelectorAll('link') || {}, | ||||||
|  |           url = `${this.$cdn}/favicon/${icon || "favicon"}.ico` | ||||||
|  |       if (Object.values(linkList).findIndex(e => e.href == url) == -1) { | ||||||
|  |         let favicon = document.createElement("link") | ||||||
|  |         favicon.rel = "icon" | ||||||
|  |         favicon.href = url | ||||||
|  |         document.head.appendChild(favicon) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.initFavicon(customConfig.sysInfo?.favicon) | ||||||
|  |     document.title = this.sys.info.fullTitle | ||||||
|  |     customConfig?.hmt && this.$injectLib("https://hm.baidu.com/hm.js?4e5dd7c5512e5da68c025c3b956fbd5d") | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss"> | ||||||
|  | html, body { | ||||||
|  |   height: 100%; | ||||||
|  |   width: 100%; | ||||||
|  |   padding: 0; | ||||||
|  |   margin: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, fieldset, input, p, blockquote, th, td { | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #app { | ||||||
|  |   font-family: Helvetica, Arial, sans-serif; | ||||||
|  |   -webkit-font-smoothing: antialiased; | ||||||
|  |   -moz-osx-font-smoothing: grayscale; | ||||||
|  |   overflow: hidden; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | li { | ||||||
|  |   list-style-type: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .greyFilter { | ||||||
|  |   filter: grayscale(100%); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .amap-logo, .amap-copyright { | ||||||
|  |   display: none !important; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/bg_prolife.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/bg_prolife.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 94 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/building.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/building.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/loginLeft.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/loginLeft.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 616 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/loginRightBottom.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/loginRightBottom.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/loginRightTop.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/loginRightTop.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/nav_bg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/nav_bg.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 75 KiB | 
							
								
								
									
										125
									
								
								src/components/AiAreaPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/components/AiAreaPicker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="AiAreaPicker"> | ||||||
|  |     <div @click="handleOpenDialog"> | ||||||
|  |       <slot v-if="$scopedSlots.default" :selected="selected"/> | ||||||
|  |       <el-button v-else type="text">选择地区</el-button> | ||||||
|  |     </div> | ||||||
|  |     <ai-dialog :visible.sync="dialog" title="选择地区" @onConfirm="submit" @close="selecting=[],init()" destroy-on-close> | ||||||
|  |       <el-breadcrumb separator-class="el-icon-arrow-right"> | ||||||
|  |         <el-breadcrumb-item v-for="(item,i) in path" :key="item.id"> | ||||||
|  |           <el-button type="text" @click.native="handlePathClick(i)">{{ item.name }}</el-button> | ||||||
|  |         </el-breadcrumb-item> | ||||||
|  |       </el-breadcrumb> | ||||||
|  |       <ai-table-select class="mar-t16" v-model="selecting" :instance="instance" :action="action" :isShowPagination="false" extra="hidden" search-key="name" | ||||||
|  |                        multiple valueObj> | ||||||
|  |         <template slot="extra" slot-scope="{row}"> | ||||||
|  |           <el-button v-if="row.id!=root" type="text" icon="el-icon-arrow-right" @click.stop="getChildren(row)"/> | ||||||
|  |         </template> | ||||||
|  |       </ai-table-select> | ||||||
|  |     </ai-dialog> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "AiAreaPicker", | ||||||
|  |   model: { | ||||||
|  |     prop: "value", | ||||||
|  |     event: "change" | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     value: {default: ""}, | ||||||
|  |     meta: {default: null}, | ||||||
|  |     root: {default: ""}, | ||||||
|  |     instance: {type: Function, required: true}, | ||||||
|  |     multiple: Boolean | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     action() { | ||||||
|  |       let currentParent = this.path.slice(-1)?.[0]?.id | ||||||
|  |       return !!currentParent && /[^0]0{0,2}$/.test(currentParent) ? `/admin/appresident/queryAreaIdGroup?currentAreaId=${currentParent}` : | ||||||
|  |           `/admin/area/queryAreaByParentIdSelf?self=${currentParent == this.root ? 1 : ''}&id=${currentParent}` | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     value(v) { | ||||||
|  |       this.dispatch('ElFormItem', 'el.form.change', [v]); | ||||||
|  |     }, | ||||||
|  |     selected(v) { | ||||||
|  |       this.$emit("update:meta", v) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       dialog: false, | ||||||
|  |       options: [], | ||||||
|  |       selecting: [], | ||||||
|  |       path: [], | ||||||
|  |       selected: [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     dispatch(componentName, eventName, params) { | ||||||
|  |       let parent = this.$parent || this.$root; | ||||||
|  |       let name = parent.$options.componentName; | ||||||
|  |       while (parent && (!name || name !== componentName)) { | ||||||
|  |         parent = parent.$parent; | ||||||
|  |         if (parent) { | ||||||
|  |           name = parent.$options.componentName; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (parent) { | ||||||
|  |         parent.$emit.apply(parent, [eventName].concat(params)); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     getChildren(row) { | ||||||
|  |       let area = this.path.find(e => e.id == row.id) | ||||||
|  |       if (!area) this.path.push(row) | ||||||
|  |     }, | ||||||
|  |     handlePathClick(index) { | ||||||
|  |       this.path.splice(index + 1, 10) | ||||||
|  |     }, | ||||||
|  |     submit() { | ||||||
|  |       this.$emit("change", this.selecting.map(e => e.id)) | ||||||
|  |       this.selected = this.$copy(this.selecting) | ||||||
|  |       this.$emit("select", this.selecting) | ||||||
|  |       this.dialog = false | ||||||
|  |     }, | ||||||
|  |     init() { | ||||||
|  |       this.path = [{id: this.root, name: "可选范围"}] | ||||||
|  |     }, | ||||||
|  |     handleOpenDialog() { | ||||||
|  |       let areas = this.value.filter(e => /^\d{12}$/.test(e)) | ||||||
|  |       this.instance.post("/admin/area/getAreaNameByids", null, { | ||||||
|  |         params: {ids: areas?.toString()} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.selecting = this.value.map(id => ({id, name: res.data?.[id] || id})) | ||||||
|  |           this.dialog = true | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     getAreaById(id) { | ||||||
|  |       return this.instance.post("/admin/area/queryAreaByAreaid", null, { | ||||||
|  |         params: {id} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           return res.data | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.init() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .AiAreaPicker { | ||||||
|  |   .mar-t16 { | ||||||
|  |     margin-top: 16px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										181
									
								
								src/components/AppLicence.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/components/AppLicence.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="AppLicence" v-if="showLicence"> | ||||||
|  |     <ai-list> | ||||||
|  |       <template #content> | ||||||
|  |         <div class="licence-content"> | ||||||
|  |           <img class="left-img" src="https://cdn.cunwuyun.cn/dvcp/key.png" alt="" /> | ||||||
|  |           <div class="content-right"> | ||||||
|  |             <h3 class="title">产品许可信息</h3> | ||||||
|  |             <p class="mini-title">您当前的版本为Saas专业版,非常感谢您对我们产品的认可与支持!</p> | ||||||
|  |             <div class="info"> | ||||||
|  |               <span class="label">过期时间</span> | ||||||
|  |               <span class="value color-f46" v-if="info.isExpired == 1">{{info.expireDate ? info.expireDate+'(已过期)' : '未激活'}}</span> | ||||||
|  |               <span class="value color-26f" v-else>{{info.expireDate}}</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="info"> | ||||||
|  |               <span class="label">主板序列号</span> | ||||||
|  |               <span class="value">{{info.mainBoard}}</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="info"> | ||||||
|  |               <span class="label">CPU</span> | ||||||
|  |               <span class="value">{{info.cpu}}</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="info"> | ||||||
|  |               <span class="label">MAC地址</span> | ||||||
|  |               <span class="value">{{info.mac}}</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="info mar-b32"> | ||||||
|  |               <span class="label">IP地址</span> | ||||||
|  |               <span class="value">{{info.ip}}</span> | ||||||
|  |             </div> | ||||||
|  |             <el-upload | ||||||
|  |               class="upload-demo mar-r16" | ||||||
|  |               action | ||||||
|  |               multiple | ||||||
|  |               accept=".lic" | ||||||
|  |               :http-request="uploadFile"> | ||||||
|  |               <div class="btn">上传许可</div> | ||||||
|  |             </el-upload> | ||||||
|  |             <div class="btn" @click="showLicence = false">返回登录</div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |     </ai-list> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "AppLicence", | ||||||
|  |   label: "产品许可", | ||||||
|  |   props: { | ||||||
|  |     instance: Function, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       files: [], | ||||||
|  |       info: {}, | ||||||
|  |       showLicence: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |  | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     show() { | ||||||
|  |       this.showLicence = true | ||||||
|  |       this.getDetail() | ||||||
|  |     }, | ||||||
|  |     hide() { | ||||||
|  |       this.showLicence = false | ||||||
|  |     }, | ||||||
|  |     getDetail() { | ||||||
|  |       this.instance.post(`/admin/license/detail`).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.info = res.data | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     uploadFile: function (file) { | ||||||
|  |       let formData = new FormData(); | ||||||
|  |       formData.append("file", file.file); | ||||||
|  |       this.instance.post(`/admin/license/save`, formData, {withoutToken: false}).then(res => { | ||||||
|  |         if (res && res.code == 0) { | ||||||
|  |           this.$message.success("证书上传成功!"); | ||||||
|  |           this.getDetail() | ||||||
|  |         } | ||||||
|  |       }).catch((err) => { | ||||||
|  |         this.$message.error(err); | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .AppLicence { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   position: fixed; | ||||||
|  |   top: 0; | ||||||
|  |   left: 0; | ||||||
|  |   z-index: 99; | ||||||
|  |   :deep( .ai-list){ | ||||||
|  |     background-color: #F5F6F9; | ||||||
|  |   } | ||||||
|  |   :deep( .ai-list .ai-list__content--right .ai-list__content--right-wrapper){ | ||||||
|  |     background-color: #F5F6F9; | ||||||
|  |     box-shadow: 0 0 0 0; | ||||||
|  |     margin: 0!important; | ||||||
|  |     padding: 0!important; | ||||||
|  |   } | ||||||
|  |   :deep( .el-upload-list){ | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  |   .licence-content{ | ||||||
|  |     display: flex; | ||||||
|  |     width: 1000px; | ||||||
|  |     margin: 200px auto 0; | ||||||
|  |     .left-img{ | ||||||
|  |       width: 200px; | ||||||
|  |       height: 200px; | ||||||
|  |     } | ||||||
|  |     .content-right{ | ||||||
|  |       width: 800px; | ||||||
|  |       .title{ | ||||||
|  |         font-size: 24px; | ||||||
|  |         font-weight: bold; | ||||||
|  |         color: #222; | ||||||
|  |         line-height: 24px; | ||||||
|  |         margin-bottom: 8px; | ||||||
|  |       } | ||||||
|  |       .mini-title{ | ||||||
|  |         font-size: 14px; | ||||||
|  |         color: #555; | ||||||
|  |         line-height: 22px; | ||||||
|  |         margin-bottom: 20px; | ||||||
|  |       } | ||||||
|  |       .info{ | ||||||
|  |         font-size: 14px; | ||||||
|  |         line-height: 22px; | ||||||
|  |         margin-bottom: 8px; | ||||||
|  |         display: flex; | ||||||
|  |         color: #222; | ||||||
|  |         .label{ | ||||||
|  |           width: 164px; | ||||||
|  |         } | ||||||
|  |         .value{ | ||||||
|  |           width: calc(100% - 164px); | ||||||
|  |         } | ||||||
|  |         .color-26f{ | ||||||
|  |           color: #26f; | ||||||
|  |         } | ||||||
|  |         .color-f46{ | ||||||
|  |           color: #f46; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       .mar-b32{ | ||||||
|  |         margin-bottom: 32px; | ||||||
|  |       } | ||||||
|  |       .upload-demo{ | ||||||
|  |         display: inline-block; | ||||||
|  |       } | ||||||
|  |       .btn{ | ||||||
|  |         display: inline-block; | ||||||
|  |         width: 88px; | ||||||
|  |         height: 32px; | ||||||
|  |         line-height: 32px; | ||||||
|  |         text-align: center; | ||||||
|  |         background: linear-gradient(90deg, #299FFF 0%, #0C61FF 100%); | ||||||
|  |         border-radius: 2px; | ||||||
|  |         cursor: pointer; | ||||||
|  |         color: #fff; | ||||||
|  |         font-size: 14px; | ||||||
|  |       } | ||||||
|  |       .mar-r16{ | ||||||
|  |         margin-right: 16px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										255
									
								
								src/components/headerNav.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/components/headerNav.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="headerNav navBg"> | ||||||
|  |     <div style="position: relative"> | ||||||
|  |       <ai-icon type="logo" :icon="system.logo||'iconcunwei'"/> | ||||||
|  |       <ai-icon type="logo" :icon="system.logo||'iconcunwei'" class="textShadow"/> | ||||||
|  |     </div> | ||||||
|  |     <span class="headerTitle">{{ system.fullTitle }}<div class="textShadow" v-html="system.fullTitle"/></span> | ||||||
|  |     <el-row type="flex" align="middle" class="toolbar"> | ||||||
|  |       <!--下载中心--> | ||||||
|  |       <downloan-center-btn v-if="extra.downloadCenter"/> | ||||||
|  |       <!--大屏按钮--> | ||||||
|  |       <dv-btn v-if="extra.dv"/> | ||||||
|  |       <div class="btn" v-if="extra.showTool" @click="home.showTool=false">隐藏工具栏</div> | ||||||
|  |       <div class="btn" v-if="extra.helpDoc" @click="openHelp">帮助文档</div> | ||||||
|  |       <app-qrcode v-if="extra.appQRCode"/> | ||||||
|  |       <div class="btn" v-if="extra.customerService" @click.native="openAiService"> | ||||||
|  |         <div class="iconfont iconCustomer_Service"></div> | ||||||
|  |         <div>智能客服</div> | ||||||
|  |       </div> | ||||||
|  |       <!--推荐链接--> | ||||||
|  |       <link-btn/> | ||||||
|  |     </el-row> | ||||||
|  |     <el-dropdown @visible-change="v=>isClick=v" @command="doMenu" class="rightDropdown"> | ||||||
|  |       <el-row type="flex" align="middle"> | ||||||
|  |         <el-avatar :src="user.info.avatar"> | ||||||
|  |           {{ defaultAvatar }} | ||||||
|  |         </el-avatar> | ||||||
|  |         <span>{{ [user.info.name, user.info.roleName].join(" - ") }}</span> | ||||||
|  |         <i :class="dropdownIcon"/> | ||||||
|  |       </el-row> | ||||||
|  |       <el-dropdown-menu> | ||||||
|  |         <el-dropdown-item command="user">用户中心</el-dropdown-item> | ||||||
|  |         <el-dropdown-item command="signOut">退出</el-dropdown-item> | ||||||
|  |       </el-dropdown-menu> | ||||||
|  |     </el-dropdown> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapState} from "vuex"; | ||||||
|  | import DvBtn from "./headerTools/dvBtn"; | ||||||
|  | import DownloanCenterBtn from "./headerTools/downloanCenterBtn"; | ||||||
|  | import extra from "../config.json" | ||||||
|  | import AppQrcode from "./headerTools/appQrcode"; | ||||||
|  | import LinkBtn from "./headerTools/linkBtn"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'headerNav', | ||||||
|  |   components: {LinkBtn, AppQrcode, DownloanCenterBtn, DvBtn}, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       extra, | ||||||
|  |       isClick: false, | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['user', 'sys']), | ||||||
|  |     dropdownIcon() { | ||||||
|  |       return this.isClick ? 'el-icon-caret-top' : 'el-icon-caret-bottom' | ||||||
|  |     }, | ||||||
|  |     defaultAvatar() { | ||||||
|  |       return this.user.info.name?.slice(-2) || "游客" | ||||||
|  |     }, | ||||||
|  |     system: v => v.sys?.info || {} | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.$dict.load('fileFrom'); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     // 获取最新的安卓、ios下载二维码 | ||||||
|  |     doMenu(comm) { | ||||||
|  |       switch (comm) { | ||||||
|  |         case 'signOut': | ||||||
|  |           //登出 | ||||||
|  |           this.$confirm("是否要登出?", {type: "warning"}).then(() => { | ||||||
|  |             this.$store.commit("signOut") | ||||||
|  |           }).catch(() => { | ||||||
|  |           }) | ||||||
|  |           break; | ||||||
|  |         case 'user': | ||||||
|  |           this.$router.push({name: "个人中心"}) | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     openAiService() { | ||||||
|  |       window.open('http://v3.faqrobot.org/robot/chat1.html?sysNum=153543696570625098') | ||||||
|  |     }, | ||||||
|  |     openHelp() { | ||||||
|  |       window.open('https://www.yuque.com/books/share/eeaaa5e3-a528-42eb-872e-20d661f3d0e2') | ||||||
|  |     }, | ||||||
|  |     changeStatus(id) { | ||||||
|  |       this.$request.post(`/app/sysuserdownload/addOrUpdate`, { | ||||||
|  |         id, status: "2" | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.code == 0) { | ||||||
|  |           this.getFiles(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .headerNav { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   width: 100%; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   background-size: 100% 48px; | ||||||
|  |   position: fixed; | ||||||
|  |   z-index: 99; | ||||||
|  |   height: 48px; | ||||||
|  |   padding-left: 24px; | ||||||
|  |   box-sizing: border-box; | ||||||
|  |   top: 0; | ||||||
|  |   color: white; | ||||||
|  |   font-size: 14px; | ||||||
|  |  | ||||||
|  |   .AiIcon { | ||||||
|  |     font-size: 38px; | ||||||
|  |     width: auto; | ||||||
|  |     height: auto; | ||||||
|  |     background: linear-gradient(180deg, #FFFFFF 0%, #CCDBF6 100%); | ||||||
|  |     -webkit-background-clip: text; | ||||||
|  |     -webkit-text-fill-color: transparent; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       color: white; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .headerTitle { | ||||||
|  |     flex: 1; | ||||||
|  |     min-width: 0; | ||||||
|  |     font-size: 24px; | ||||||
|  |     color: #FFF; | ||||||
|  |     line-height: 28px; | ||||||
|  |     background: linear-gradient(180deg, #FFFFFF 0%, #CCDBF6 100%); | ||||||
|  |     -webkit-background-clip: text; | ||||||
|  |     -webkit-text-fill-color: transparent; | ||||||
|  |     font-weight: bold; | ||||||
|  |     margin-left: 8px; | ||||||
|  |     position: relative; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :deep(.toolbar) { | ||||||
|  |     gap: 12px; | ||||||
|  |     margin-right: 32px; | ||||||
|  |  | ||||||
|  |     .btn { | ||||||
|  |       padding: 0 12px; | ||||||
|  |       color: white; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         cursor: pointer; | ||||||
|  |         color: rgba(#fff, .8); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .el-dropdown { | ||||||
|  |     height: 48px; | ||||||
|  |     line-height: 48px; | ||||||
|  |     color: #fff; | ||||||
|  |     padding: 0 12px; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       background-color: rgba(46, 51, 68, .15); | ||||||
|  |       color: white; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .el-image { | ||||||
|  |     margin: 12px 0 12px 16px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .download-wrapper { | ||||||
|  |     position: relative; | ||||||
|  |  | ||||||
|  |     &:hover .download { | ||||||
|  |       display: flex; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .download { | ||||||
|  |       display: none; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 100%; | ||||||
|  |       left: 12px; | ||||||
|  |       transform: translateX(-90%); | ||||||
|  |       width: auto; | ||||||
|  |       height: auto; | ||||||
|  |       padding: 12px; | ||||||
|  |       background: #fff; | ||||||
|  |       box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.1); | ||||||
|  |       border-radius: 2px; | ||||||
|  |       box-sizing: border-box; | ||||||
|  |       z-index: 999; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .download-item { | ||||||
|  |       text-align: center; | ||||||
|  |  | ||||||
|  |       &:first-child { | ||||||
|  |         margin-right: 13px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       & > img { | ||||||
|  |         width: 105px; | ||||||
|  |         height: 105px; | ||||||
|  |         margin-bottom: 7px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       p { | ||||||
|  |         margin-top: 5px; | ||||||
|  |         font-size: 13px; | ||||||
|  |         color: #333; | ||||||
|  |         text-align: center; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .download-item__middle { | ||||||
|  |         img { | ||||||
|  |           width: 13px; | ||||||
|  |           height: 16px; | ||||||
|  |           vertical-align: sub; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         span { | ||||||
|  |           padding-left: 8px; | ||||||
|  |           font-size: 13px; | ||||||
|  |           color: #333; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :deep(.rightDropdown) { | ||||||
|  |     font-size: 12px; | ||||||
|  |     padding: 0 16px; | ||||||
|  |     height: 48px; | ||||||
|  |     background: rgba(#fff, .1); | ||||||
|  |  | ||||||
|  |     .el-avatar > img { | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .el-row { | ||||||
|  |       gap: 4px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </style> | ||||||
							
								
								
									
										121
									
								
								src/components/headerTools/appQrcode.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/components/headerTools/appQrcode.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="appQrcode"> | ||||||
|  |     <div class="btn">手机APP</div> | ||||||
|  |     <div class="download"> | ||||||
|  |       <div class="download-item" v-if="!androidQRcode&&!iosQRcode"><p class="nowarp-text" v-text="`暂未发布`"/></div> | ||||||
|  |       <template v-else> | ||||||
|  |         <div class="download-item" v-if='iosQRcode'> | ||||||
|  |           <img :src="iosQRcode" alt=""/> | ||||||
|  |           <div class="download-item__middle"> | ||||||
|  |             <span class="iconfont iconIOS"></span> | ||||||
|  |             <span>iPhone</span> | ||||||
|  |           </div> | ||||||
|  |           <p>手机扫码下载APP</p> | ||||||
|  |         </div> | ||||||
|  |         <div class="download-item" v-if='androidQRcode'> | ||||||
|  |           <img :src="androidQRcode" alt=""/> | ||||||
|  |           <div class="download-item__middle"> | ||||||
|  |             <span class="iconfont iconAndroid"></span> | ||||||
|  |             <span>Android</span> | ||||||
|  |           </div> | ||||||
|  |           <p>手机扫码下载APP</p> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |     </div> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "appQrcode", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       iosQRcode: null, | ||||||
|  |       androidQRcode: null | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     getAppQRcode() { | ||||||
|  |       this.$request.post(`/admin/sysversion/getLatestIosVersion`, null, { | ||||||
|  |         params: {type: 3} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.iosQRcode = res.data.qrCodeUrl | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       this.$request.post(`/admin/sysversion/getLatestVersion`, null, { | ||||||
|  |         params: {type: 1} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.androidQRcode = res.data.qrCodeUrl | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.getAppQRcode() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .appQrcode { | ||||||
|  |   position: relative; | ||||||
|  |  | ||||||
|  |   &:hover .download { | ||||||
|  |     display: flex; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .download { | ||||||
|  |     display: none; | ||||||
|  |     position: absolute; | ||||||
|  |     top: 100%; | ||||||
|  |     left: 12px; | ||||||
|  |     transform: translateX(-90%); | ||||||
|  |     width: auto; | ||||||
|  |     height: auto; | ||||||
|  |     padding: 12px; | ||||||
|  |     background: #fff; | ||||||
|  |     box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.1); | ||||||
|  |     border-radius: 2px; | ||||||
|  |     box-sizing: border-box; | ||||||
|  |     z-index: 999; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .download-item { | ||||||
|  |     text-align: center; | ||||||
|  |  | ||||||
|  |     &:first-child { | ||||||
|  |       margin-right: 13px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     & > img { | ||||||
|  |       width: 105px; | ||||||
|  |       height: 105px; | ||||||
|  |       margin-bottom: 7px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     p { | ||||||
|  |       margin-top: 5px; | ||||||
|  |       font-size: 13px; | ||||||
|  |       color: #333; | ||||||
|  |       text-align: center; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .download-item__middle { | ||||||
|  |       img { | ||||||
|  |         width: 13px; | ||||||
|  |         height: 16px; | ||||||
|  |         vertical-align: sub; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       span { | ||||||
|  |         padding-left: 8px; | ||||||
|  |         font-size: 13px; | ||||||
|  |         color: #333; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										221
									
								
								src/components/headerTools/downloanCenterBtn.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/components/headerTools/downloanCenterBtn.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="downloanCenterBtn"> | ||||||
|  |     <el-badge :value="badgeNum" :hidden='badgeNum==0'> | ||||||
|  |       <div class="btn" @click="openDrawer()">下载中心</div> | ||||||
|  |     </el-badge> | ||||||
|  |     <el-drawer title="下载中心" :visible.sync="drawer" :modal-append-to-body="false" size="520"> | ||||||
|  |       <div class="downLoad_main"> | ||||||
|  |         <div class="search_top "> | ||||||
|  |           <p style="color:#999999;">仅显示最近90天的记录</p> | ||||||
|  |           <el-input size="mini" v-model="fileName" placeholder="文件名" clearable prefix-icon="iconfont iconSearch" | ||||||
|  |                     style="width:240px;" @change="getFiles()"/> | ||||||
|  |         </div> | ||||||
|  |         <ul class="infinite-list"> | ||||||
|  |           <li v-for="(item,i) in filesList" class="infinite-list-item " :key="i"> | ||||||
|  |             <div class="left"> | ||||||
|  |               <svg class="svg" aria-hidden="true"> | ||||||
|  |                 <use xlink:href="#iconZip"/> | ||||||
|  |               </svg> | ||||||
|  |             </div> | ||||||
|  |             <div class="middle"> | ||||||
|  |               <p class="fileName">{{ item.fileName }}【密码:{{ item.pwd }}】</p> | ||||||
|  |               <p> | ||||||
|  |                 <span>来源:</span> | ||||||
|  |                 <span>{{ $dict.getLabel('fileFrom', item.fileFrom) }}</span> | ||||||
|  |                 <span>{{ (item.size / 1000).toFixed(2) + "KB" }}</span> | ||||||
|  |                 <span>{{ item.createTime }}</span> | ||||||
|  |               </p> | ||||||
|  |             </div> | ||||||
|  |             <div class="right"> | ||||||
|  |               <span class="iconfont iconResetting" v-if="item.status==0">处理中</span> | ||||||
|  |               <span v-if="item.status==2">已下载</span> | ||||||
|  |               <i class="iconfont iconDownload" @click="downFile(item)" v-if="item.status!=0">下载</i> | ||||||
|  |             </div> | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |       </div> | ||||||
|  |     </el-drawer> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapState} from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "downloanCenterBtn", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       badgeNum: 0, | ||||||
|  |       drawer: false,//抽屉 | ||||||
|  |       filesList: [], | ||||||
|  |       fileName: '', | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['user']) | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     openDrawer() { | ||||||
|  |       this.drawer = true; | ||||||
|  |       this.getFiles(); | ||||||
|  |     }, | ||||||
|  |     getFiles() { | ||||||
|  |       this.$request.post(`/app/sysuserdownload/list`, null, { | ||||||
|  |         params: { | ||||||
|  |           userId: this.user.info.id, | ||||||
|  |           fileName: this.fileName, | ||||||
|  |           current: 1, | ||||||
|  |           size: 1000, | ||||||
|  |         } | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.filesList = res.data.records; | ||||||
|  |           this.searchNum() | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     //查询未完成数量 | ||||||
|  |     searchNum() { | ||||||
|  |       this.$request.post(`/app/sysuserdownload/queryCountByUserId`, null, { | ||||||
|  |         params: { | ||||||
|  |           userId: this.user.info.id | ||||||
|  |         } | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.badgeNum = res.data; | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     downFile(item) { | ||||||
|  |       this.changeStatus(item.id); | ||||||
|  |       // window.open(item.accessUrl); | ||||||
|  |       let elemIF = document.createElement('iframe'); | ||||||
|  |       elemIF.src = item.accessUrl; | ||||||
|  |       elemIF.style.display = 'none'; | ||||||
|  |       document.body.appendChild(elemIF); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.searchNum() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .downloanCenterBtn { | ||||||
|  |   .downLoad_main { | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     padding: 16px; | ||||||
|  |     box-sizing: border-box; | ||||||
|  |  | ||||||
|  |     .search_top { | ||||||
|  |       display: flex; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       align-items: center; | ||||||
|  |       padding-bottom: 8px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .infinite-list { | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |  | ||||||
|  |       .infinite-list-item { | ||||||
|  |         width: 100%; | ||||||
|  |         padding: 8px; | ||||||
|  |         box-sizing: border-box; | ||||||
|  |         background: rgba(255, 255, 255, 1); | ||||||
|  |         border-radius: 4px; | ||||||
|  |         border: 1px solid rgba(208, 212, 220, 1); | ||||||
|  |         margin-bottom: 8px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |  | ||||||
|  |         .left { | ||||||
|  |           display: flex; | ||||||
|  |           justify-content: center; | ||||||
|  |           align-items: center; | ||||||
|  |           width: 30px; | ||||||
|  |  | ||||||
|  |           .svg { | ||||||
|  |             width: 24px; | ||||||
|  |             height: 24px; | ||||||
|  |             vertical-align: middle; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .middle { | ||||||
|  |           flex: 1; | ||||||
|  |  | ||||||
|  |           .fileName { | ||||||
|  |             color: #333333; | ||||||
|  |             font-size: 14px; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           p:nth-child(2) { | ||||||
|  |             color: #999999; | ||||||
|  |             font-size: 12px; | ||||||
|  |  | ||||||
|  |             span { | ||||||
|  |               padding: 0 4px; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             span:nth-child(2) { | ||||||
|  |               border-right: solid 1px #999999; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             span:nth-child(3) { | ||||||
|  |               border-right: solid 1px #999999; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .right { | ||||||
|  |           display: flex; | ||||||
|  |           justify-content: center; | ||||||
|  |           align-items: center; | ||||||
|  |           font-size: 12px; | ||||||
|  |           width: 90px; | ||||||
|  |           text-align: center; | ||||||
|  |  | ||||||
|  |           span { | ||||||
|  |             color: #999999; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           i { | ||||||
|  |             display: block; | ||||||
|  |             width: 50px; | ||||||
|  |             color: #5088FF; | ||||||
|  |             font-size: 12px; | ||||||
|  |             cursor: pointer; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ::-webkit-scrollbar { | ||||||
|  |       width: 4px; | ||||||
|  |       background-color: #eee; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ::-webkit-scrollbar-thumb { | ||||||
|  |       background-color: #8888; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :deep(.el-drawer__wrapper) { | ||||||
|  |   position: fixed; | ||||||
|  |   width: 100%; | ||||||
|  |   top: 0; | ||||||
|  |   bottom: 0; | ||||||
|  |   right: 0; | ||||||
|  |  | ||||||
|  |   .el-drawer__header > span:focus { | ||||||
|  |     outline: 0 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										52
									
								
								src/components/headerTools/dvBtn.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/headerTools/dvBtn.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="dvBtn"> | ||||||
|  |     <el-popover title="数据大屏" width="500" trigger="click"> | ||||||
|  |       <div flex class="wrap"> | ||||||
|  |         <div class="el-button--text pad-r8 pad-b8" style="width: 50%" v-for="op in dvOptions" :key="op.id" v-text="op.name||'无名大屏'" | ||||||
|  |              @click="handleOpenDV(op.id)"/> | ||||||
|  |       </div> | ||||||
|  |       <div slot="reference" class="btn">数据大屏</div> | ||||||
|  |     </el-popover> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import extra from "../../config.json" | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "dvBtn", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       dvOptions: [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     getDvList() { | ||||||
|  |       this.$request.post("/app/appdiylargescreen/allLargeScreenProjectByPage", null, { | ||||||
|  |         params: {size: 9999, status: 1} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.dvOptions = res.data.records | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     handleOpenDV(id) { | ||||||
|  |       window.open(`${location.origin}${extra.base || ""}/dv?id=${id}#dv`) | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.getDvList() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .dvBtn { | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :deep(.el-button--text) { | ||||||
|  |   cursor: pointer; | ||||||
|  |   user-select: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										46
									
								
								src/components/headerTools/linkBtn.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/headerTools/linkBtn.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="linkBtn"> | ||||||
|  |     <el-dropdown v-if="links.length > 0" @command="handleOpenLink"> | ||||||
|  |       <div class="btn">友情链接</div> | ||||||
|  |       <el-dropdown-menu> | ||||||
|  |         <el-dropdown-item v-for="op in links" :key="op.id" :command="op.url"> | ||||||
|  |           {{ op.title }} | ||||||
|  |         </el-dropdown-item> | ||||||
|  |       </el-dropdown-menu> | ||||||
|  |     </el-dropdown> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "linkBtn", | ||||||
|  |   label: "友情链接", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       links: [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     getLinks() { | ||||||
|  |       this.$request.post("/app/appwebnavurl/list", null, { | ||||||
|  |         params: {size: 9999, status: 1} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.links = res.data.records | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     handleOpenLink(url) { | ||||||
|  |       window.open(url) | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.getLinks() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .linkBtn { | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										40
									
								
								src/components/mainContent.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/components/mainContent.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="mainContent"> | ||||||
|  |     <ai-nav-tab :fixed="homePage" :routes="routes"/> | ||||||
|  |     <router-view v-if="refresh"/> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapState} from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "mainContent", | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['user', 'homePage']), | ||||||
|  |     routes: v => v.user.info?.menuSet?.map(e => ({...e, label: e.name, name: e.id})) | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     $route(v, old) { | ||||||
|  |       if (v.meta == old.meta && v.fullPath != old.fullPath) { | ||||||
|  |         this.refresh = false | ||||||
|  |         this.$nextTick(() => this.refresh = true) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       refresh: true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .mainContent { | ||||||
|  |   height: 100%; | ||||||
|  |   width: 100%; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										296
									
								
								src/components/sliderNav.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								src/components/sliderNav.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,296 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="sliderNav"> | ||||||
|  |     <el-input class="searchApp" size="small" v-model="searchApp" placeholder="搜索应用" clearable | ||||||
|  |               prefix-icon="iconfont iconSearch" @change="handleSearchApp"/> | ||||||
|  |     <el-scrollbar class="ai-menu"> | ||||||
|  |       <div v-for="(item,i) in navs" :key="i"> | ||||||
|  |         <div class="rootMenu" :class="{isActive:menuPath.includes(item.id)}" @click.stop="openKidMenu(item)"> | ||||||
|  |           <i :class="item.style||'iconfont iconloudongmoxing'"/> | ||||||
|  |           <span class="fill mar-l8" v-text="item.name"/> | ||||||
|  |           <i v-if="hasChildren(item.children)" class="iconfont mar-l8" :class="arrowIcon(item.showChildren)"/> | ||||||
|  |         </div> | ||||||
|  |         <div class="kidMenu" v-if="hasChildren(item.children)&&item.showChildren" @click.stop> | ||||||
|  |           <div v-for="menu in item.children" :key="menu.id"> | ||||||
|  |             <div class="submenu wrap pad-l16 pad-r16" flex v-if="hasChildren(menu.children)"> | ||||||
|  |               <b v-text="menu.name" :class="{menuBtn:menu.type==1,current:menuPath.includes(menu.id)}" | ||||||
|  |                  @click="handleSelect(menu)"/> | ||||||
|  |               <div class="menuBtn" v-for="kid in menu.children" :key="kid.id" v-text="kid.name" :title="kid.name" | ||||||
|  |                    @click="handleSelect(kid)" :class="{current:menuPath.includes(kid.id)}"/> | ||||||
|  |             </div> | ||||||
|  |             <div v-else class="lv2Btn" v-text="menu.name" @click="handleSelect(menu)" :class="{current:menuPath.includes(menu.id)}"/> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="divider"/> | ||||||
|  |     </el-scrollbar> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapGetters} from "vuex"; | ||||||
|  | import qs from "querystring"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "sliderNav", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       menuList: [], | ||||||
|  |       searchApp: "", | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters(['mods']), | ||||||
|  |     navs: v => v.sortList(v.menuList), | ||||||
|  |     menuPath() { | ||||||
|  |       let paths = [], current = this.mods?.find(e => e.route == this.$route.name) | ||||||
|  |       const findParent = id => { | ||||||
|  |         let menu = this.mods?.find(e => e.id == id) | ||||||
|  |         if (menu) { | ||||||
|  |           paths.push(menu.id) | ||||||
|  |           if (!!menu.parentId) findParent(menu.parentId) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (current) { | ||||||
|  |         findParent(current.id) | ||||||
|  |       } | ||||||
|  |       return paths | ||||||
|  |     }, | ||||||
|  |     modList: v => v.mods.filter(e => e.isMenu == 1 || e.type == 0 || (e.level > 1 && e.type == 1)) | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     initMenu(menus = this.modList) { | ||||||
|  |       //isMenu 旧版本判断是否为菜单 type<2 新版本判断是否是菜单或应用 | ||||||
|  |       if (menus?.length > 0) { | ||||||
|  |         this.menuList = this.$arr2tree(menus) | ||||||
|  |         this.menuList = this.menuList.map(e => ({ | ||||||
|  |           ...e, | ||||||
|  |           showChildren: this.menuPath.includes(e.id) || !!this.searchApp | ||||||
|  |         })) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     openKidMenu(parent) { | ||||||
|  |       if (this.hasChildren(parent.children)) { | ||||||
|  |         parent.showChildren = !parent.showChildren | ||||||
|  |       } else { | ||||||
|  |         this.handleSelect(parent) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleSelect(item) { | ||||||
|  |       if (!item.path) return | ||||||
|  |       if (item.route == this.$route.name) { | ||||||
|  |         //避免同一路由跳转的BUG vue-router官方BUG | ||||||
|  |       } else { | ||||||
|  |         let {route: name, path} = item | ||||||
|  |         if (!name) { | ||||||
|  |           this.$message.warning("暂无应用") | ||||||
|  |         } else { | ||||||
|  |           this.goto({name, query: qs.parse(path.split("?")?.[1])}) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     goto(item) { | ||||||
|  |       return this.$router.push(item) | ||||||
|  |     }, | ||||||
|  |     sortList(list) { | ||||||
|  |       return list?.sort((a, b) => a.showIndex - b.showIndex) || [] | ||||||
|  |     }, | ||||||
|  |     handleSearchApp() { | ||||||
|  |       let {searchApp} = this | ||||||
|  |       if (searchApp) { | ||||||
|  |         let list = this.modList.filter(e => e.name?.indexOf(searchApp) > -1), map = {} | ||||||
|  |         const findParent = e => { | ||||||
|  |           map[e.id] = e | ||||||
|  |           if (!!e.parentId) { | ||||||
|  |             let parent = this.modList.find(m => m.id == e.parentId) | ||||||
|  |             map[parent.id] = parent | ||||||
|  |             if (!!parent.parentId) { | ||||||
|  |               findParent(parent) | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         list.forEach(e => findParent(e)) | ||||||
|  |         console.log(map, list) | ||||||
|  |         this.initMenu(Object.values(map)) | ||||||
|  |       } else { | ||||||
|  |         this.initMenu() | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     arrowIcon(v) { | ||||||
|  |       return v ? "iconArrow_Down" : "iconArrow_Right" | ||||||
|  |     }, | ||||||
|  |     hasChildren(arr) { | ||||||
|  |       return arr?.length > 0 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.initMenu() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .sliderNav { | ||||||
|  |   width: 200px; | ||||||
|  |   height: 100%; | ||||||
|  |   transition: width .1s; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   flex-direction: column; | ||||||
|  |   border-right: 1px solid #e5e5e5; | ||||||
|  |   flex-shrink: 0; | ||||||
|  |   box-sizing: border-box; | ||||||
|  |   background: #EFF1F4; | ||||||
|  |   color: #222; | ||||||
|  |   position: relative; | ||||||
|  |   user-select: none; | ||||||
|  |  | ||||||
|  |   .kidMenu { | ||||||
|  |     font-size: 13px; | ||||||
|  |  | ||||||
|  |     .rootName { | ||||||
|  |       font-size: 20px; | ||||||
|  |       color: #333; | ||||||
|  |       cursor: default; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .submenu { | ||||||
|  |       margin-top: 8px; | ||||||
|  |       width: 100%; | ||||||
|  |       color: #aaa; | ||||||
|  |  | ||||||
|  |       & > b { | ||||||
|  |         width: 100%; | ||||||
|  |         line-height: 28px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       & > * { | ||||||
|  |         cursor: default; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .menuBtn { | ||||||
|  |       display: block; | ||||||
|  |       width: 50%; | ||||||
|  |       cursor: pointer; | ||||||
|  |       line-height: 32px; | ||||||
|  |       color: #333; | ||||||
|  |       flex-shrink: 0; | ||||||
|  |       white-space: nowrap; | ||||||
|  |       overflow: hidden; | ||||||
|  |       text-overflow: ellipsis; | ||||||
|  |  | ||||||
|  |       &.line { | ||||||
|  |         width: 100%; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         color: #26f; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &.current { | ||||||
|  |         color: #26f; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .rootMenu { | ||||||
|  |     padding: 0 16px; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     height: 44px; | ||||||
|  |     cursor: pointer; | ||||||
|  |     box-shadow: 0px -1px 0px 0px #D8DCE3 inset, 0px 1px 0px 0px #FFF inset, -1px 0px 0px 0px #E5E5E5 inset; | ||||||
|  |     font-size: 13px; | ||||||
|  |  | ||||||
|  |     .iconfont { | ||||||
|  |       color: #89B; | ||||||
|  |       font-size: 20px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.isActive { | ||||||
|  |       color: #26f; | ||||||
|  |  | ||||||
|  |       .iconfont { | ||||||
|  |         color: #26f !important; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       color: #26f; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :deep(.ai-menu ){ | ||||||
|  |     padding-left: 0; | ||||||
|  |     flex: 1; | ||||||
|  |     min-height: 0; | ||||||
|  |  | ||||||
|  |     .el-scrollbar__wrap { | ||||||
|  |       overflow-x: auto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &::-webkit-scrollbar { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :deep(.searchApp ){ | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     height: 44px; | ||||||
|  |     padding: 0 16px; | ||||||
|  |     box-shadow: 0px -1px 0px 0px #E5E5E5 inset; | ||||||
|  |  | ||||||
|  |     .el-input__inner { | ||||||
|  |       border: none; | ||||||
|  |       background: inherit; | ||||||
|  |       padding: 0 28px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .el-input__prefix { | ||||||
|  |       left: 16px; | ||||||
|  |  | ||||||
|  |       .iconSearch { | ||||||
|  |         font-size: 20px; | ||||||
|  |         width: fit-content; | ||||||
|  |         color: #89B; | ||||||
|  |         line-height: 44px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .divider { | ||||||
|  |     color: #aaa; | ||||||
|  |     border-top: 1px solid #ddd; | ||||||
|  |     position: relative; | ||||||
|  |     font-size: 12px; | ||||||
|  |     margin: 16px 16px 32px; | ||||||
|  |  | ||||||
|  |     &:before { | ||||||
|  |       content: "到达底部"; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 50%; | ||||||
|  |       left: 50%; | ||||||
|  |       transform: translate(-50%, -50%); | ||||||
|  |       padding: 0 16px; | ||||||
|  |       background: #EFF1F4; | ||||||
|  |       white-space: nowrap; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .lv2Btn { | ||||||
|  |     height: 44px; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     color: #222; | ||||||
|  |     padding-left: 44px; | ||||||
|  |     cursor: pointer; | ||||||
|  |  | ||||||
|  |     &.current { | ||||||
|  |       background: linear-gradient(90deg, #298BFF 0%, #0C61FF 100%); | ||||||
|  |       box-shadow: inset -1px 0 0 0 #E5E5E5, inset 0 2px 8px 0 #1E4599; | ||||||
|  |       color: #fff; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										33
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import Vue from 'vue'; | ||||||
|  | import App from './App.vue'; | ||||||
|  | import ui from 'element-ui'; | ||||||
|  | import router from './utils/router'; | ||||||
|  | import utils from './utils'; | ||||||
|  | import vcUI from 'dui'; | ||||||
|  | import appComp from '@dui/dv'; | ||||||
|  | import store from './utils/store'; | ||||||
|  | import autoRoutes from "./utils/autoRoutes"; | ||||||
|  | import extra from "./config.json" | ||||||
|  | import axios from "./utils/axios"; | ||||||
|  | //import ob from "dui/lib/js/observer" | ||||||
|  | //备注底座信息,勿删 | ||||||
|  | console.log("欢迎使用%s", extra.sysInfo?.name || "构建版本") | ||||||
|  | //new ob() | ||||||
|  | window.Vue = Vue | ||||||
|  | Vue.use(ui); | ||||||
|  | Vue.use(vcUI); | ||||||
|  | Vue.use(appComp); | ||||||
|  | Vue.config.productionTip = false; | ||||||
|  | Vue.prototype.$cdn = "https://cdn.cunwuyun.cn" | ||||||
|  | Vue.prototype.$request = axios | ||||||
|  | Object.keys(utils).map((e) => (Vue.prototype[e] = utils[e])); | ||||||
|  | const loadPage = () => autoRoutes.init().finally(() => new Vue({router, store, render: h => h(App)}).$mount("#app")) | ||||||
|  | let theme = null | ||||||
|  | store.dispatch('getSystem', extra.sysInfo).then(res => { | ||||||
|  |   theme = JSON.parse(res?.colorScheme || null) | ||||||
|  |   return import(`dui/lib/styles/theme.${theme?.web}.scss`).catch(() => 0) | ||||||
|  | }).finally(() => { | ||||||
|  |   Vue.prototype.$theme = theme?.web || "blue" | ||||||
|  |   !!theme?.web && theme?.web != "blue" ? loadPage() : import(`dui/lib/styles/common.scss`).finally(loadPage) | ||||||
|  | }) | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								src/utils/autoRoutes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/utils/autoRoutes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | import {waiting} from "./index"; | ||||||
|  | import router from "./router"; | ||||||
|  | import store from "./store"; | ||||||
|  | import {Message} from "element-ui"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | import extra from "../config.json" | ||||||
|  |  | ||||||
|  | let {state: {user}, commit, dispatch} = store | ||||||
|  | const signOut = () => commit("signOut"), | ||||||
|  |     getUserInfo = () => dispatch("getUserInfo"), | ||||||
|  |     existRoute = route => { | ||||||
|  |       return router.getRoutes()?.find(e => e.name == route?.name || e.path == route?.path) | ||||||
|  |     }, | ||||||
|  |     goto = (route, next) => { | ||||||
|  |       const exist = !!existRoute(route) | ||||||
|  |       return exist ? route.name ? next() : router.replace(route) : | ||||||
|  |           !route.name && route.path == "/" ? router.replace({name: "Home"}).catch(() => 0) : | ||||||
|  |               Message.error("无法找到路由,请联系系统管理员!") | ||||||
|  |     } | ||||||
|  | const loadApps = () => { | ||||||
|  |   //新App的自动化格式 | ||||||
|  |   waiting.init({innerHTML: '应用加载中..'}) | ||||||
|  |   let apps = require.context('../../apps', true, /\.(\/.+)\/App[A-Z][^\/]+\.vue$/, "lazy") | ||||||
|  |   return Promise.all(apps.keys().map(path => apps(path).then(file => { | ||||||
|  |     if (file.default) { | ||||||
|  |       let {name} = file.default | ||||||
|  |       waiting.setContent(`加载${name}...`) | ||||||
|  |       Vue.component(name, file.default) | ||||||
|  |     } else return 0 | ||||||
|  |   }))).then(() => { | ||||||
|  |     waiting.setContent(`正在进入系统...`) | ||||||
|  |     setTimeout(() => waiting.close(), 1000) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | const addHome = homePage => { | ||||||
|  |   const component = extra?.homePage || homePage.path | ||||||
|  |   if (extra?.homePage && Vue.component(component)) { | ||||||
|  |     homePage = {...homePage, path: component, component: () => import('../views/mainEntry'), meta: component} | ||||||
|  |   } | ||||||
|  |   router.addRoute('Home', homePage) | ||||||
|  |   router.options.routes[2].children.unshift(homePage) | ||||||
|  |   commit("setHomePage", { | ||||||
|  |     ...homePage, | ||||||
|  |     label: homePage.name, | ||||||
|  |     id: `/v/${component}`, | ||||||
|  |     isMenu: 1, | ||||||
|  |     route: homePage.name, | ||||||
|  |     component, | ||||||
|  |     path: component, | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | const generateRoutes = (to, from, next) => { | ||||||
|  |   if (router.options.routes[2].children.length > 0) { | ||||||
|  |     goto(to, next) | ||||||
|  |   } else { | ||||||
|  |     Promise.all([getUserInfo(), loadApps()]).then(() => { | ||||||
|  |       //初始化默认工作台 | ||||||
|  |       let homePage = {name: "工作台", path: "console", style: "iconfont iconNav_Dashborad", component: () => import('../views/console')} | ||||||
|  |       addHome(homePage) | ||||||
|  |       const mods = user.info.menuSet?.filter(e => !!e.component)?.map(e => ({route: e.id, ...e})) | ||||||
|  |       mods?.map(({route: name, path, component}) => { | ||||||
|  |         if (!!Vue.component(component) && path && !existRoute({name})) { | ||||||
|  |           let search = path.split("?") | ||||||
|  |           path = search?.[0] || path | ||||||
|  |           const route = {name, path, component: () => import('../views/mainEntry'), meta: component} | ||||||
|  |           router.addRoute('Home', route) | ||||||
|  |           router.options.routes[2].children.push(route) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       to.name == "Home" ? next({name: homePage.name, replace: true}) : next({...to, replace: true}) | ||||||
|  |     }).then(() => commit("setRoutes", router.options.routes[2].children)) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | export const routes = [ | ||||||
|  |   {path: "/login", name: "登录", component: () => import('../views/sign')}, | ||||||
|  |   {path: '/dv', name: '数据大屏入口', component: () => import('../views/dvIndex')}, | ||||||
|  |   {path: '/v', name: 'Home', component: () => import('../views/home'), children: []}, | ||||||
|  |   {path: '/', name: "init"}, | ||||||
|  | ] | ||||||
|  | export default { | ||||||
|  |   init: () => { | ||||||
|  |     router.beforeEach((to, from, next) => { | ||||||
|  |       console.log('%s=>%s', from.name, to.name) | ||||||
|  |       if (to.hash == "#pddv") { | ||||||
|  |         const {query} = to | ||||||
|  |         dispatch("getToken", { | ||||||
|  |           username: "18971406276", | ||||||
|  |           password: "admin321!" | ||||||
|  |         }).then(() => next({name: "数据大屏入口", query, hash: "#dv"})) | ||||||
|  |       } else if (["数据大屏入口", "登录"].includes(to.name)) { | ||||||
|  |         next() | ||||||
|  |       } else if (to.hash == "#dv") { | ||||||
|  |         //数据大屏进行的独立页面跳转 | ||||||
|  |         let {query, hash} = to | ||||||
|  |         next({name: "数据大屏入口", query, hash}) | ||||||
|  |       } else if (user.token) { | ||||||
|  |         to.name == "init" ? next({name: "Home"}) : generateRoutes(to, from, next) | ||||||
|  |       } else { | ||||||
|  |         signOut() | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     router.onError(err => { | ||||||
|  |       console.error(err) | ||||||
|  |     }) | ||||||
|  |     return Promise.resolve() | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								src/utils/axios.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/utils/axios.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import instance from 'dui/lib/js/request' | ||||||
|  | import {Message} from 'element-ui' | ||||||
|  | import extra from "../config.json"; | ||||||
|  | import store from "./store" | ||||||
|  |  | ||||||
|  | let baseURLs = { | ||||||
|  |   production: extra.base || "/", | ||||||
|  |   development: extra.baseURL || '/lan', | ||||||
|  | } | ||||||
|  | instance.defaults.baseURL = baseURLs[process.env.NODE_ENV] | ||||||
|  | instance.interceptors.request.use(config => { | ||||||
|  |   config.timeout = 300000 | ||||||
|  |   if (extra?.isSingleService) { | ||||||
|  |     config.url = config.url.replace(/(app|auth|admin)\//, "api/") | ||||||
|  |   } | ||||||
|  |   if (config.url.startsWith("/node")) { | ||||||
|  |     config.baseURL = "/ns" | ||||||
|  |   } | ||||||
|  |   return config | ||||||
|  | }, error => Message.error(error)) | ||||||
|  | instance.interceptors.response.use(res => res, err => { | ||||||
|  |   if (err?.code == 401) { | ||||||
|  |     store.commit('signOut', 1) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | export default instance | ||||||
							
								
								
									
										100
									
								
								src/utils/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/utils/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | import {MessageBox} from 'element-ui' | ||||||
|  | import tools from 'dui/lib/js/utils' | ||||||
|  | import store from "./store"; | ||||||
|  |  | ||||||
|  | let {state: {user}} = store | ||||||
|  | const addChildParty = (parent, pending) => { | ||||||
|  |   let doBeforeCount = pending.length | ||||||
|  |   parent["children"] = parent["children"] || [] | ||||||
|  |   pending.map((e, index, arr) => { | ||||||
|  |     if (e.partyOrgParentId == parent.partyOrgId) { | ||||||
|  |       parent.children.push(e) | ||||||
|  |       arr.splice(index, 1) | ||||||
|  |       addChildParty(parent, arr) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   if (parent.children.length == 0) { | ||||||
|  |     delete parent.children | ||||||
|  |   } | ||||||
|  |   if (pending.length > 0 && doBeforeCount > pending.length) { | ||||||
|  |     parent.children.map(c => addChildParty(c, pending)) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | /** | ||||||
|  |  * 封装提示框 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const $confirm = (content, options) => { | ||||||
|  |   return MessageBox.confirm(content, { | ||||||
|  |     type: "warning", | ||||||
|  |     confirmButtonText: "确认", | ||||||
|  |     center: true, | ||||||
|  |     title: "提示", | ||||||
|  |     dangerouslyUseHTMLString: true, | ||||||
|  |     ...options | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 封装权限判断方法 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const $permissions = flag => { | ||||||
|  |   const buttons = user?.info?.buttons | ||||||
|  |   if (buttons) return buttons.some(b => b.id == flag || b.permission == flag) | ||||||
|  |   else return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const $decimalCalc = (...arr) => { | ||||||
|  |   //确认提升精度 | ||||||
|  |   let decimalLengthes = arr.map(e => { | ||||||
|  |     let index = ("" + e).indexOf(".") | ||||||
|  |     return ("" + e).length - index | ||||||
|  |   }) | ||||||
|  |   let maxDecimal = Math.max(...decimalLengthes), precision = Math.pow(10, maxDecimal) | ||||||
|  |   //计算 | ||||||
|  |   let intArr = arr.map(e => (Number(e) || 0) * precision) | ||||||
|  |   //返回计算值 | ||||||
|  |   return intArr.reduce((t, a) => t + a) / precision | ||||||
|  | } | ||||||
|  | export const waiting = { | ||||||
|  |   init(ops, count) { | ||||||
|  |     if (document.body) { | ||||||
|  |       let div = document.createElement('div') | ||||||
|  |       div.id = "ai-waiting" | ||||||
|  |       div.innerHTML = "信息正在加载中..." | ||||||
|  |       div.className = "el-loading-mask is-fullscreen" | ||||||
|  |       div.style.zIndex = '202204271710' | ||||||
|  |       div.style.textAlign = 'center' | ||||||
|  |       div.style.lineHeight = '100vh' | ||||||
|  |       div.style.background = 'rgba(255,255,255,.8)' | ||||||
|  |       div.style.backdropFilter = 'blur(6px)' | ||||||
|  |       document.body.appendChild(div) | ||||||
|  |     } else if (count < 10) { | ||||||
|  |       setTimeout(() => this.init(ops, ++count), 500) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   getDom() { | ||||||
|  |     return document.querySelector('#ai-waiting') | ||||||
|  |   }, | ||||||
|  |   setContent(html) { | ||||||
|  |     let div = this.getDom() | ||||||
|  |     div.innerHTML = html | ||||||
|  |   }, | ||||||
|  |   close() { | ||||||
|  |     let div = this.getDom() | ||||||
|  |     div.parentElement.removeChild(div) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | export default { | ||||||
|  |   ...tools, | ||||||
|  |   addChildParty, | ||||||
|  |   $confirm, | ||||||
|  |   $permissions, | ||||||
|  |   $decimalCalc, | ||||||
|  |   $waiting: waiting | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								src/utils/router.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/utils/router.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | import Vue from 'vue' | ||||||
|  | import VueRouter from 'vue-router' | ||||||
|  | import {routes} from "./autoRoutes" | ||||||
|  | import config from "../config.json" | ||||||
|  |  | ||||||
|  | Vue.use(VueRouter) | ||||||
|  | export default new VueRouter({ | ||||||
|  |   base: config.base || '/', | ||||||
|  |   mode: 'history', | ||||||
|  |   hashbang: false, | ||||||
|  |   routes, | ||||||
|  |   scrollBehavior(to) { | ||||||
|  |     if (to.hash) { | ||||||
|  |       return { | ||||||
|  |         selector: to.hash | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }) | ||||||
							
								
								
									
										44
									
								
								src/utils/store.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/utils/store.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import Vue from 'vue' | ||||||
|  | import Vuex from 'vuex' | ||||||
|  | import preState from 'vuex-persistedstate' | ||||||
|  | import * as modules from "dui/lib/js/modules" | ||||||
|  | import axios from "./axios"; | ||||||
|  | import extra from "../config.json" | ||||||
|  |  | ||||||
|  | Vue.use(Vuex) | ||||||
|  |  | ||||||
|  | export default new Vuex.Store({ | ||||||
|  |   state: { | ||||||
|  |     homePage: {} | ||||||
|  |   }, | ||||||
|  |   mutations: { | ||||||
|  |     setHomePage(state, home) { | ||||||
|  |       state.homePage = home | ||||||
|  |     }, | ||||||
|  |     signOut(state, flag) { | ||||||
|  |       const base = extra.base || "" | ||||||
|  |       if (flag) { | ||||||
|  |         state.user.token = null; | ||||||
|  |         state.user.info = {} | ||||||
|  |         sessionStorage.clear(); | ||||||
|  |         location.href = base + '/login' + location.hash; | ||||||
|  |       } else { | ||||||
|  |         axios.delete('/auth/token/logout').then(() => { | ||||||
|  |           state.user.token = null; | ||||||
|  |           sessionStorage.clear(); | ||||||
|  |           state.user.info = {} | ||||||
|  |           location.href = base + '/login'; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   getters: { | ||||||
|  |     //后台数据库中的应用集合,在本工程中不一定存在 | ||||||
|  |     mods: state => [ | ||||||
|  |       state.homePage, | ||||||
|  |       state.user.info?.menuSet?.map(e => ({route: e.id, ...e, label: e.name})) | ||||||
|  |     ].flat().filter(Boolean) | ||||||
|  |   }, | ||||||
|  |   modules, | ||||||
|  |   plugins: [preState()] | ||||||
|  | }) | ||||||
							
								
								
									
										31
									
								
								src/views/building.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/views/building.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="building"> | ||||||
|  |     <div class="title">功能开发中,敬请期待...</div> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "building" | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .building { | ||||||
|  |   position: relative; | ||||||
|  |   height: 100%; | ||||||
|  |   background-image: url("../assets/building.png"); | ||||||
|  |   background-size: 400px 300px; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   background-position: center, center; | ||||||
|  |  | ||||||
|  |   .title { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 50%; | ||||||
|  |     left: 50%; | ||||||
|  |     transform: translate(-50%, -50%); | ||||||
|  |     margin-top: 150px; | ||||||
|  |     font-weight: bold; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										42
									
								
								src/views/console.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/views/console.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="console"> | ||||||
|  |     <div class="consoleBg" v-text="`欢迎使用${system.fullTitle}`"/> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapState} from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "console", | ||||||
|  |   label: "工作台", | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['sys']), | ||||||
|  |     system: v => v.sys.info || {} | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .console { | ||||||
|  |   height: 100%; | ||||||
|  |  | ||||||
|  |   .consoleBg { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 50%; | ||||||
|  |     top: 50%; | ||||||
|  |     transform: translate(-50%, -50%); | ||||||
|  |     background-image: url("https://cdn.cunwuyun.cn/dvcp/consoleBg.png"); | ||||||
|  |     background-size: 600px 362px; | ||||||
|  |     background-repeat: no-repeat; | ||||||
|  |     background-position: center top; | ||||||
|  |     padding-top: 402px; | ||||||
|  |     font-size: 32px; | ||||||
|  |     font-family: MicrosoftYaHei-Bold, MicrosoftYaHei; | ||||||
|  |     font-weight: bold; | ||||||
|  |     color: #95A1B0; | ||||||
|  |     min-width: 600px; | ||||||
|  |     text-align: center; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										83
									
								
								src/views/dvIndex.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/views/dvIndex.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="dvIndex"> | ||||||
|  |     <ai-dv-wrapper v-model="activeTab" :views="views" :title="title" :theme="theme" v-if="views.length" :background="bgImg" :type="currentStyle" :titleSize="titleSize"> | ||||||
|  |       <ai-dv-viewer urlPrefix="/app" :instance="instance" :dict="dict" :id="currentView.id"/> | ||||||
|  |     </ai-dv-wrapper> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import Vue from "vue"; | ||||||
|  | import {waiting} from "../utils"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "dvIndex", | ||||||
|  |   provide() { | ||||||
|  |     return { | ||||||
|  |       dv: this | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     currentView: v => v.views.find(e => e.id == v.activeTab) || v.views?.[0] || {}, | ||||||
|  |     background: v => JSON.parse(v.currentView.config || null)?.dashboard?.backgroundImage?.[0]?.url || "", | ||||||
|  |     bgImg: v => v.theme == 1 ? 'https://cdn.cunwuyun.cn/dvcp/dv/img/dj-bg.png' : v.background, | ||||||
|  |     theme() { | ||||||
|  |       if (!this.currentView) return '0' | ||||||
|  |       if (!this.currentView.config) return '0' | ||||||
|  |       const config = JSON.parse(this.currentView.config) | ||||||
|  |       if (config.custom) { | ||||||
|  |         return '0' | ||||||
|  |       } | ||||||
|  |       return config.dashboard.theme | ||||||
|  |     }, | ||||||
|  |     currentStyle: v => JSON.parse(v.currentView.config || null)?.dashboard?.style || "black", | ||||||
|  |     titleSize: v => JSON.parse(v.currentView.config || "{}").dashboard?.titleSize | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       instance: this.$request, | ||||||
|  |       dict: this.$dict, | ||||||
|  |       activeTab: 0, | ||||||
|  |       views: [], | ||||||
|  |       title: "", | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     getDvOptions() { | ||||||
|  |       let {id} = this.$route.query | ||||||
|  |       return id ? this.instance.post("/app/appdiylargescreen/queryLargeScreenProjectDetailById", null, { | ||||||
|  |         params: {id, status: 1} | ||||||
|  |       }).then(res => { | ||||||
|  |         if (res?.data) { | ||||||
|  |           this.title = res.data.name | ||||||
|  |           this.views = res.data.lsList?.map(e => ({...e, label: e.title})) | ||||||
|  |         } | ||||||
|  |       }) : Promise.reject() | ||||||
|  |     }, | ||||||
|  |     loadDvs() { | ||||||
|  |       //新App的自动化格式 | ||||||
|  |       waiting.init({innerHTML: '应用加载中..'}) | ||||||
|  |       let apps = require.context('../../apps', true, /\.(\/.+)\/App[A-Z][^\/]+D[Vv]\.vue$/, "lazy") | ||||||
|  |       return Promise.all(apps.keys().map(path => apps(path).then(file => { | ||||||
|  |         if (file.default) { | ||||||
|  |           let {name} = file.default | ||||||
|  |           waiting.setContent(`加载${name}...`) | ||||||
|  |           Vue.component(name, file.default) | ||||||
|  |         } else return 0 | ||||||
|  |       }))).then(() => { | ||||||
|  |         waiting.setContent(`正在进入系统...`) | ||||||
|  |         setTimeout(() => waiting.close(), 1000) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.loadDvs().then(() => this.getDvOptions()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .dvIndex { | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										42
									
								
								src/views/home.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/views/home.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="home"> | ||||||
|  |     <header-nav/> | ||||||
|  |     <el-row class="fill" type="flex"> | ||||||
|  |       <slider-nav/> | ||||||
|  |       <main-content class="fill"/> | ||||||
|  |     </el-row> | ||||||
|  |     <ai-copilot v-if="useCopilot" :http="$request"/> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import SliderNav from "../components/sliderNav"; | ||||||
|  | import MainContent from "../components/mainContent"; | ||||||
|  | import HeaderNav from "../components/headerNav"; | ||||||
|  | import configExtra from "../config.json" | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'app', | ||||||
|  |   components: {HeaderNav, MainContent, SliderNav}, | ||||||
|  |   computed: { | ||||||
|  |     useCopilot: () => !!configExtra?.copilot | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     import("../../apps/actions").then(extra => { | ||||||
|  |       const actions = extra?.default || {} | ||||||
|  |       this.$store.hotUpdate({actions}) | ||||||
|  |       Object.keys(actions)?.map(action => this.$store.dispatch(action)) | ||||||
|  |     }).catch(() => 0) | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .home { | ||||||
|  |   height: 100%; | ||||||
|  |   width: 100%; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   padding-top: 48px; | ||||||
|  |   box-sizing: border-box; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										55
									
								
								src/views/mainEntry.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/views/mainEntry.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="mainEntry fill"> | ||||||
|  |     <ai-detail list v-if="hasIntro"> | ||||||
|  |       <template #content> | ||||||
|  |         <ai-intro :id="currentApp.guideId" :instance="$request" @start="handleStartUse"/> | ||||||
|  |       </template> | ||||||
|  |     </ai-detail> | ||||||
|  |     <component v-else :is="app" :instance="$request" :dict="$dict" :permissions="$permissions" :menuName="currentApp.name"/> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  |  | ||||||
|  | import Building from "./building"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | import {mapGetters, mapMutations, mapState} from "vuex"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "mainEntry", | ||||||
|  |   components: {Building}, | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['logs']), | ||||||
|  |     ...mapGetters(['mods']), | ||||||
|  |     currentApp() { | ||||||
|  |       const {fullPath, name} = this.$route | ||||||
|  |       return this.mods.find(e => !name ? fullPath.indexOf(e.path) > -1 : name == e.route) || Building | ||||||
|  |     }, | ||||||
|  |     app() { | ||||||
|  |       const {currentApp} = this | ||||||
|  |       return Vue.component(currentApp?.component) ? currentApp.component : Building | ||||||
|  |     }, | ||||||
|  |     hasIntro() { | ||||||
|  |       const {app, currentApp, logs} = this | ||||||
|  |       return !!currentApp.guideId && !logs?.closeIntro?.includes(app) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations(['addCloseIntro']), | ||||||
|  |     handleStartUse() { | ||||||
|  |       this.addCloseIntro(this.app) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .mainEntry { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |  | ||||||
|  |   & > * { | ||||||
|  |     height: 100%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										172
									
								
								src/views/sign.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/views/sign.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | <template> | ||||||
|  |   <section class="sign"> | ||||||
|  |     <div class="left signLeftBg"> | ||||||
|  |       <el-row type="flex" align="middle"> | ||||||
|  |         <img class="AiIcon" v-if="/[\\\/]/.test(logo.icon)" :src="logo.icon" alt=""/> | ||||||
|  |         <ai-icon v-else type="logo" :icon="logo.icon"/> | ||||||
|  |         <div v-if="logo.text" class="logoText mar-l8" v-text="logo.text"/> | ||||||
|  |       </el-row> | ||||||
|  |       <div class="signLeftContent"> | ||||||
|  |         <div class="titlePane"> | ||||||
|  |           <b v-text="system.name"/> | ||||||
|  |           <div v-text="system.title"/> | ||||||
|  |         </div> | ||||||
|  |         <div class="subTitle" v-for="(t,i) in subTitles" :key="i" v-html="t"/> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="right"> | ||||||
|  |       <div class="projectName mar-b48" :title="system.fullTitle">{{ system.fullTitle }}</div> | ||||||
|  |       <ai-sign v-if="system.edition=='saas'" @login="login" :instance="instance" visible :tps="['wxwork']" :sassLogin="!isDev"/> | ||||||
|  |       <ai-sign v-else isSignIn @login="login" :instance="instance" visible :showScanLogin="system.edition=='standard'||!system.edition"/> | ||||||
|  |       <el-row type="flex" align="middle" class="bottomRecord"> | ||||||
|  |         <div v-if="system.recordDesc" v-text="system.recordDesc"/> | ||||||
|  |         <el-link v-if="system.recordNo" v-text="system.recordNo" :href="system.recordURL"/> | ||||||
|  |         <div v-if="system.ssl" v-html="system.ssl"/> | ||||||
|  |       </el-row> | ||||||
|  |     </div> | ||||||
|  |     <app-licence :instance="instance" ref="licence"/> | ||||||
|  |   </section> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {mapMutations, mapState} from 'vuex' | ||||||
|  | import AppLicence from "../components/AppLicence"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: "sign", | ||||||
|  |   components: {AppLicence}, | ||||||
|  |   computed: { | ||||||
|  |     ...mapState(['user', 'sys']), | ||||||
|  |     instance: v => v.$request, | ||||||
|  |     system: v => v.sys?.info || {}, | ||||||
|  |     subTitles() { | ||||||
|  |       let list = [ | ||||||
|  |         "构建全域数字大脑,助力政府科学决策", | ||||||
|  |         "全域统一应用入口,移动办公高效协同", | ||||||
|  |         "直接触达居民微信,政民互动“零距离”" | ||||||
|  |       ] | ||||||
|  |       return (typeof this.system.desc == "object" ? this.system.desc : JSON.parse(this.system.desc || null)) || list | ||||||
|  |     }, | ||||||
|  |     logo: v => !!v.system.loginLogo ? {icon: v.system.loginLogo, text: v.system.loginLogoText} : {icon: v.system.logo, text: v.system.logoText}, | ||||||
|  |     isDev: () => process.env.NODE_ENV == "development" | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     if (this.user.token) { | ||||||
|  |       this.handleGotoHome() | ||||||
|  |     } else { | ||||||
|  |       const {code, auth_code} = this.$route.query | ||||||
|  |       if (code) { | ||||||
|  |         this.toLogin(code) | ||||||
|  |       } else if (auth_code) { | ||||||
|  |         this.tpLogin(auth_code) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations(['setToken']), | ||||||
|  |     login(data) { | ||||||
|  |       if (data.data == '999') { | ||||||
|  |         return this.$refs.licence.show() | ||||||
|  |       } | ||||||
|  |       if (data?.access_token) { | ||||||
|  |         this.setToken([data.token_type, data.access_token].join(" ")) | ||||||
|  |         this.handleGotoHome() | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleGotoHome() { | ||||||
|  |       this.$message.success("登录成功!") | ||||||
|  |       if (this.$route.hash == "#dv") { | ||||||
|  |         this.$router.push({name: "数据大屏入口", hash: "#dv"}) | ||||||
|  |       } else { | ||||||
|  |         this.$router.push({name: "Home"}) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     toLogin(code) { | ||||||
|  |       this.instance.post(`/auth/wechatcp-qr/token`, { | ||||||
|  |         code: code, | ||||||
|  |         type: 'cpuser' | ||||||
|  |       }, { | ||||||
|  |         auth: { | ||||||
|  |           username: 'villcloud', | ||||||
|  |           password: "villcloud" | ||||||
|  |         }, | ||||||
|  |         params: { | ||||||
|  |           grant_type: 'password', | ||||||
|  |           scope: 'server' | ||||||
|  |         } | ||||||
|  |       }).then(this.login) | ||||||
|  |     }, | ||||||
|  |     tpLogin(code) { | ||||||
|  |       this.instance.post("/auth/wechatcp-qr/token", {code}, { | ||||||
|  |         auth: { | ||||||
|  |           username: 'villcloud', | ||||||
|  |           password: "villcloud" | ||||||
|  |         }, | ||||||
|  |         params: { | ||||||
|  |           grant_type: 'password', | ||||||
|  |           scope: 'server' | ||||||
|  |         } | ||||||
|  |       }).then(this.login).catch(() => this.$router.push({})) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .sign { | ||||||
|  |   display: flex; | ||||||
|  |   box-sizing: border-box; | ||||||
|  |   height: 100%; | ||||||
|  |  | ||||||
|  |   .AiIcon { | ||||||
|  |     font-size: 40px; | ||||||
|  |     height: 40px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .logoText { | ||||||
|  |     font-size: 20px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :deep(.left ) { | ||||||
|  |     width: 480px; | ||||||
|  |     flex-shrink: 0; | ||||||
|  |     background-size: 100% 100%; | ||||||
|  |     background-repeat: no-repeat; | ||||||
|  |     padding-left: 64px; | ||||||
|  |     padding-top: 40px; | ||||||
|  |     box-sizing: border-box; | ||||||
|  |     color: #fff; | ||||||
|  |     font-size: 16px; | ||||||
|  |  | ||||||
|  |     .iconcunwei1 { | ||||||
|  |       font-size: 36px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   :deep(.right ) { | ||||||
|  |     flex: 1; | ||||||
|  |     min-width: 0; | ||||||
|  |     background-color: #F6F8FB; | ||||||
|  |     background-image: url("../assets/loginRightTop.png"), url("../assets/loginRightBottom.png"); | ||||||
|  |     background-repeat: no-repeat; | ||||||
|  |     background-position: calc(100% - 80px) 0, calc(100% - 40px) 100%; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |  | ||||||
|  |     .bottomRecord { | ||||||
|  |       font-size: 12px; | ||||||
|  |       color: #999; | ||||||
|  |       gap: 16px; | ||||||
|  |       position: fixed; | ||||||
|  |       bottom: 20px; | ||||||
|  |  | ||||||
|  |       .el-link { | ||||||
|  |         font-size: inherit; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -13,15 +13,19 @@ module.exports = { | |||||||
|   }, |   }, | ||||||
|   transpileDependencies: [/dui[\\\/]lib[\\\/]js/], |   transpileDependencies: [/dui[\\\/]lib[\\\/]js/], | ||||||
|   chainWebpack: (config) => { |   chainWebpack: (config) => { | ||||||
|  |     config.resolve.alias | ||||||
|  |       .set('@packages', path.resolve(__dirname, 'packages')) | ||||||
|  |       .set('@project', path.resolve(__dirname, 'project')) | ||||||
|     config.module |     config.module | ||||||
|       .rule('js') |       .rule('js') | ||||||
|       .include |       .include | ||||||
|       .add(path.resolve(__dirname, 'packages')) |       .add(path.resolve(__dirname, 'packages')) | ||||||
|       .add(path.resolve(__dirname, 'components')) |  | ||||||
|       .add(path.resolve(__dirname, 'project')) |       .add(path.resolve(__dirname, 'project')) | ||||||
|       .add(path.resolve(__dirname, 'examples')) |       .add(path.resolve(__dirname, 'examples')) | ||||||
|       .add(path.resolve(__dirname, 'ui')) |       .add(path.resolve(__dirname, 'ui/packages')) | ||||||
|       .end().use('babel').loader('babel-loader').tap(options => options); |       .add(path.resolve(__dirname, 'ui/dv')) | ||||||
|  |       .add(path.resolve(__dirname, 'ui/lib/js')) | ||||||
|  |       .end().use('babel').loader('babel-loader').tap(options => options) | ||||||
|     config.plugin("limit").use(require("webpack/lib/optimize/LimitChunkCountPlugin"), [{maxChunks: 20}]).tap(options => options) |     config.plugin("limit").use(require("webpack/lib/optimize/LimitChunkCountPlugin"), [{maxChunks: 20}]).tap(options => options) | ||||||
|   }, |   }, | ||||||
|   devServer: { |   devServer: { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user