This commit is contained in:
ky_sunl
2021-04-14 01:15:09 +00:00
parent d3c4cc3077
commit 3ed8fdfc43
31 changed files with 869 additions and 248 deletions

View File

@@ -1,2 +1,3 @@
VUE_APP_BASE_URL=http://localhost:60161 VUE_APP_NODE_ENV=development
VUE_APP_BASE_URL=http://localhost:5566
VUE_APP_DEV_KEY=CJm9dFWx4IIGXHm^R1e@Y&mp*n8MQXfDKjDYP6ZVGqEAZQiC9LvX9jq8@uaMTT@T VUE_APP_DEV_KEY=CJm9dFWx4IIGXHm^R1e@Y&mp*n8MQXfDKjDYP6ZVGqEAZQiC9LvX9jq8@uaMTT@T

View File

@@ -1,2 +1,3 @@
VUE_APP_NODE_ENV=production
VUE_APP_BASE_URL=http://localhost:60161 VUE_APP_BASE_URL=http://localhost:60161
VUE_APP_DEV_KEY=%0!qF2BpcVorlNceu#kP4SVS1bPiMUqI71%rITatPIosNOCrot@mV7PJ&br$CVvF VUE_APP_DEV_KEY=%0!qF2BpcVorlNceu#kP4SVS1bPiMUqI71%rITatPIosNOCrot@mV7PJ&br$CVvF

View File

@@ -1,4 +1,4 @@
@import '~ant-design-vue/dist/antd.less'; @import './extend.less';
@import './lib/visibility.less'; @import './lib/visibility.less';
@import './lib/container.less'; @import './lib/container.less';
@import './lib/align.less'; @import './lib/align.less';
@@ -19,4 +19,4 @@
@import './lib/form.less'; @import './lib/form.less';
@import './lib/select.less'; @import './lib/select.less';
@import './theme/primary.less'; @import './theme/primary.less';
@import './lib/font-weight.less'; // @import './lib/font-weight.less';

View File

@@ -0,0 +1,3 @@
@import '~ant-design-vue/dist/antd.less';
@padding-xxs: 4px;
@padding-xl: 32px;

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
.ant-card { .ant-card {
margin-bottom: @padding-md; margin-bottom: @padding-md;
} }

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
@container-width: 1200px; @container-width: 1200px;
.container { .container {
width: @container-width; width: @container-width;

View File

@@ -1 +1 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
body { body {
font-weight: 100; font-weight: 100;
} }

View File

@@ -1,7 +1,9 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
.yo-form { .yo-form {
&--fixed {
width: 660px; width: 660px;
margin: 0 auto; margin: 0 auto;
}
.yo-form-group { .yo-form-group {
margin-bottom: @padding-md; margin-bottom: @padding-md;
} }
@@ -14,24 +16,123 @@
border: @border-width-base @border-style-base @border-color-split; border: @border-width-base @border-style-base @border-color-split;
background-color: @white; background-color: @white;
@box-shadow-focused: 0 0 0 2px fade(@primary-color, 50%);
&::before, &::before,
&::after { &::after {
content: none; content: none;
} }
.ant-form-item-control {
text-align: right;
}
.ant-input, .ant-input,
.ant-select-selection { .ant-input-number,
border-color: transparent; .ant-mentions,
background-color: #f1f3f4; .ant-select-selection,
.ant-input-group-addon {
z-index: 1;
text-align: left;
color: lighten(@black, 10%);
border: 0;
background-color: lighten(@black, 95%);
}
.ant-mentions {
textarea {
background-color: lighten(@black, 95%);
}
}
.focus {
z-index: 2 !important;
box-shadow: @box-shadow-focused;
}
.ant-input {
&:focus {
.focus();
}
}
.ant-input-number-focused,
.ant-mentions-focused {
.focus();
} }
.ant-select-focused, .ant-select-focused,
.ant-select-open { .ant-select-open {
z-index: 2;
.ant-select-selection { .ant-select-selection {
box-shadow: 0 0 0 2px fade(@primary-color, 50%); .focus();
}
}
.ant-input-group {
.ant-row-flex {
.ant-select {
width: 100%;
}
}
.ant-input-group-addon {
z-index: 0;
}
}
}
.ant-form-item-label {
overflow: hidden;
flex: 1 1 auto;
margin-right: @padding-md;
text-align: left;
text-overflow: ellipsis;
>label {
color: lighten(@black, 10%);
&::after {
content: none;
} }
} }
} }
.ant-form-item-control-wrapper { .ant-form-item-control-wrapper {
width: 50%; flex: 0 0 61.8%;
width: 61.8%;
min-width: 220px; min-width: 220px;
} }
.ant-form-explain {
font-size: @font-size-base - 1px;
margin-top: @padding-xs;
margin-bottom: @padding-xxs;
margin-left: -61.8%;
transition: none;
animation: none;
text-align: left;
opacity: 1;
color: fade(darken(@primary-color, 38.2%), 61.8%);
}
// 上下布局
.yo-form--vertical {
display: block;
.ant-form-item-control {
text-align: left;
}
&-radio {
.ant-radio-wrapper {
line-height: @padding-lg;
display: block;
margin-right: 0;
+.ant-radio-wrapper {
margin-top: @padding-sm;
}
}
}
.ant-form-item-control-wrapper {
margin-left: @padding-lg;
}
.ant-form-explain {
margin-left: 0;
}
}
} }

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
@margin-padding-position: ~'', ~'-top', ~'-left', ~'-right', ~'-bottom'; @margin-padding-position: ~'', ~'-top', ~'-left', ~'-right', ~'-bottom';
@margin-padding-position-name: ~'', ~'t', ~'l', ~'r', ~'b'; @margin-padding-position-name: ~'', ~'t', ~'l', ~'r', ~'b';

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 7px; width: 7px;
height: 7px; height: 7px;

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
.ant-select-arrow { .ant-select-arrow {
.ant-select-arrow-icon { .ant-select-arrow-icon {
transform: scaleY(.75); transform: scaleY(.75);

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) '~@/assets/style/extend.less';
.yo-query-bar { .yo-query-bar {
margin-bottom: @padding-md; margin-bottom: @padding-md;
} }

View File

@@ -1,4 +1,4 @@
@import (reference) '~ant-design-vue/dist/antd.less'; @import (reference) './extend.less';
@import (reference) './lib/container.less'; @import (reference) './lib/container.less';
.main(@nav-background: @layout-header-background, .main(@nav-background: @layout-header-background,

View File

@@ -8,7 +8,7 @@ import { token } from '@/common/token'
import status from './status' import status from './status'
import app from '@/main' import app from '@/main'
axios.defaults.baseURL = process.env.VUE_APP_BASE_URL + '/api' axios.defaults.baseURL = '/api'
/** /**
* 最终直接根据url名称调用接口方法 * 最终直接根据url名称调用接口方法
@@ -44,22 +44,39 @@ const api = {}
for (let key in urls) { for (let key in urls) {
const item = urls[key]
let url = '',
method = 'post'
if (item.constructor === String) {
url = item
} else if (item.constructor === Array) {
url = item[0]
if (item[1]) {
method = item[1].toLowerCase()
}
} else if (item.constructor === Object) {
url = item.url
if (item.method) {
method = item.method.toLowerCase()
}
}
api[`${key}E`] = function (params = {}) { api[`${key}E`] = function (params = {}) {
return initInstance().post(urls[key], params) return initInstance()[method](url, params)
} }
api[key] = function (params = {}) { api[key] = function (params = {}) {
return new Promise((reslove, reject) => { return new Promise((reslove, reject) => {
api[`${key}E`](params) api[`${key}E`](params)
.then(({ data }) => { .then(({ data }) => {
if (data.status === status.OK) { if (data.code === status.OK) {
reslove(data) reslove(data)
} else { } else {
reject(data) reject(data)
} }
}) })
.catch(err => { .catch(err => {
if (process.env.PROD) { if (process.env.VUE_APP_NODE_ENV === 'development') {
alert('发生错误,请联系管理员') alert('发生错误,请联系管理员')
} else { } else {
console.warn(err, urls[key]) console.warn(err, urls[key])

View File

@@ -1,5 +1,20 @@
/**
* 接口的3种配置方式
* 1.string
* 如login: '/login'\
* 将会默认已POST方式请求接口/login
* 2.array
* 如login: ['/login', 'post']
* 数组[0]必填,为接口地址,[1]选填,为请求方式(不区分大小写),默认为POST
* 3.object
* 如login: { url: '/login', method: 'post' }
* [url]必填,为接口地址,[method]选填,为请求方式(不区分大小写),默认为POST
*/
export default { export default {
login: '/gate/login', login: '/login',
logout: ['/logout', 'get'],
getLoginUser: ['/getLoginUser', 'get'],
getMenu: '/menu/get', getMenu: '/menu/get',

View File

@@ -1,46 +1,46 @@
import { api } from '@/common/api'; import { api } from '@/common/api'
import { token } from '@/common/token'; import { token } from '@/common/token'
import { encryptByDES, decryptByDES } from '@/util/des'; import { encryptByDES, decryptByDES } from '@/util/des'
import app from '@/main'; import app from '@/main'
const GLOBAL_KEY = '__GLOBAL'; const GLOBAL_KEY = '__GLOBAL'
const setGlobal = (info) => { const setGlobal = (info) => {
app.$set(app.global, 'info', info); app.$set(app.global, 'info', info)
window.sessionStorage.setItem(GLOBAL_KEY, encryptByDES(JSON.stringify(info))); window.sessionStorage.setItem(GLOBAL_KEY, encryptByDES(JSON.stringify(info)))
} }
const removeGlobal = () => { const removeGlobal = () => {
app.$set(app.global, 'info', undefined); app.$set(app.global, 'info', undefined)
window.sessionStorage.removeItem(GLOBAL_KEY); window.sessionStorage.removeItem(GLOBAL_KEY)
} }
const getGlobal = () => { const getGlobal = () => {
return JSON.parse(decryptByDES(window.sessionStorage.getItem(GLOBAL_KEY))); return JSON.parse(decryptByDES(window.sessionStorage.getItem(GLOBAL_KEY)))
} }
const doLogin = (args) => { const doLogin = (args) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
api.login({ api.login(args).then(({ success, data, message }) => {
account: args.account, if (success) {
password: args.password token.value = data
}).then(({ result }) => { app.$message.success('登录成功')
if (result.success) {
token.value = result.data.token;
app.$message.success('登录成功');
setGlobal(result.data.info)
if (app.$route.query.return) { if (app.$route.query.return) {
const r = decryptByDES(app.$route.query.return); const r = decryptByDES(app.$route.query.return)
app.$router.replace(r) app.$router.replace(r)
} else { } else {
app.$router.replace('/'); app.$router.replace('/')
} }
resolve(); resolve()
} else { } else {
app.$message.error(result.message); app.$message.error(message)
reject(); reject()
} }
}); }).catch(({ message }) => {
if (typeof message === 'object' && message[0]) {
app.$message.error(message[0].messages[0])
}
})
}) })
} }
@@ -48,10 +48,10 @@ const doLogout = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
api.logout().then(({ result: { success, message } }) => { api.logout().then(({ result: { success, message } }) => {
if (success) { if (success) {
removeGlobal(); removeGlobal()
token.value = ''; token.value = ''
if (app.$route.path === '/') { if (app.$route.path === '/') {
app.$router.replace('/login'); app.$router.replace('/login')
} else { } else {
app.$router.replace({ app.$router.replace({
path: '/login', path: '/login',
@@ -60,10 +60,10 @@ const doLogout = () => {
} }
}) })
} }
resolve(); resolve()
} else { } else {
app.$message.error(message); app.$message.error(message)
reject(); reject()
} }
}) })
}) })
@@ -72,11 +72,11 @@ const doLogout = () => {
const doCheck = () => { const doCheck = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
api.checkLogin().then(({ result }) => { api.checkLogin().then(({ result }) => {
setGlobal(result); setGlobal(result)
resolve(); resolve()
}).catch(() => { }).catch(() => {
reject(); reject()
}); })
}) })
} }
@@ -85,5 +85,6 @@ export {
doLogout, doLogout,
doCheck, doCheck,
setGlobal,
getGlobal getGlobal
} }

View File

@@ -0,0 +1,94 @@
/**
* auth: 允许的权限
* authExclude: 排除的权限
*
* auth的几种传值方式
* 1.String
* 例: auth="sysApp:page"
* 直接传入字符串,对单项权限进行验证
*
* 2.Array
* 2.1.单项权限
* 例: :auth="['sysApp:page']"
* 2.2.并且关系多项权限
* 例: :auth="['sysApp:page', 'sysApp:add']"
* 数组中传入多个字符串
* 此时验证的是同时拥有"sysApp:page"和"sysApp:add"两项权限才会渲染
* 2.3.或者关系多项权限
* 例: :auth="[['sysApp:page', 'sysApp:add'], ['sysApp:edit']]"
* 等同于: :auth="[['sysApp:page', 'sysApp:add'], 'sysApp:edit']"
* 二维数组结构,内部数组之间为并且关系
* 此时验证的是"sysApp:page"&"sysApp:add"||"sysApp:edit"
* 2.4.可直接传入布尔值
* 例: :auth="['sysApp:page', 1 === 1]"
* :auth="[['sysApp:page', 'sysApp:add'], 1 === 1]"
*
* 3.Json
* 如果觉得多项权限时每次都要写应用编号比较繁琐,可对Array形式进行简化
* 3.1.单项权限
* 例: :auth="{ sysApp: 'page' }"
* 3.2.并且关系多项权限
* 例: :auth="{ sysApp: ['page', 'add'] }"
* 3.3.或者关系多项权限
* 例: :auth="{ sysApp: [['page', 'add'], ['edit']]}"
* 等同于: :auth="{ sysApp: [['page', 'add'], 'edit']}"
* 3.4.可直接传入布尔值
* 例: :auth="{ sysApp: ['page', 1 === 1] }"
* :auth="{ sysApp: [['page', 'add'], 1 === 1] }"
*
*/
export default {
functional: true,
props: {
auth: {
default() {
return new Array()
},
type: Array,
},
authExclude: {
default() {
return new Array()
},
type: Array,
},
},
render(h, context) {
const { props, scopedSlots } = context
const global = context.parent.$root.global
const auth = props.auth
const authExclude = props.authExclude
if (!global.info) {
return false
}
/**
* 超级管理员
*/
// if (global.info.adminType === 1) {
// return scopedSlots.default()
// }
if (auth.length) {
for (let i = 0; i < auth.length; i++) {
if (global.info && (auth[i] & global.info.authority) === auth[i]) {
return scopedSlots.default()
}
}
} else if (authExclude.length) {
let flag = false
for (let i = 0; i < authExclude.length; i++) {
if (global.info && (authExclude[i] & global.info.authority) === authExclude[i]) {
flag = true
}
}
if (!flag) {
return scopedSlots.default()
}
}
return false
},
}

View File

@@ -44,6 +44,8 @@ Vue.prototype.$_ = _
*/ */
import Container from './components/container' import Container from './components/container'
Vue.component('Container', Container) Vue.component('Container', Container)
import Authorized from './components/authorized'
Vue.component('Auth', Authorized)
/** /**
* 引入主题样式 * 引入主题样式

View File

@@ -0,0 +1,7 @@
<template>
<container>
<a-row :gutter="16">
<a-col></a-col>
</a-row>
</container>
</template>

View File

@@ -0,0 +1,79 @@
<template>
<container>
<br />
<a-form-model class="yo-form yo-form--fixed">
<h3>输入框</h3>
<Input />
<h3>数字输入框</h3>
<div class="yo-form-group">
<a-form-model-item label="数字输入框">
<span slot="help">
<a-tag color="pink">a-input-number</a-tag>可以输入和选择数字
</span>
<a-input-number autocomplete="off" placeholder="请输入数字" />
</a-form-model-item>
</div>
<h3>文本域</h3>
<div class="yo-form-group">
<a-form-model-item label="文本域">
<a-textarea autocomplete="off" placeholder="请输入文字" />
</a-form-model-item>
</div>
<h3>提及</h3>
<div class="yo-form-group">
<a-form-model-item label="提及">
<a-mentions placeholder="请输入'@'进行提及">
<a-mentions-option value="史莱姆">史莱姆</a-mentions-option>
<a-mentions-option value="哥布林">哥布林</a-mentions-option>
<a-mentions-option value="牛头人">牛头人</a-mentions-option>
</a-mentions>
</a-form-model-item>
</div>
<h3>单选框</h3>
<div class="yo-form-group">
<a-form-model-item label="单选框">
<span slot="help">
<a-tag color="pink">a-radio-group > a-radio</a-tag>横排的单选框请注意因为控件区域宽度的限制在这里无法使用单选框按钮
</span>
<a-radio-group>
<a-radio value="1">攻击</a-radio>
<a-radio value="2">防御</a-radio>
<a-radio value="3">剑技</a-radio>
<a-radio value="4">魔法</a-radio>
<a-radio value="5">回复</a-radio>
<a-radio value="6">虐杀</a-radio>
<a-radio value="7">踩在脚下</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item class="yo-form--vertical yo-form--vertical-radio" label="垂直单选框">
<span slot="help">
垂直单选框需要在垂直布局控件的基础上添加类
<a-tag color="pink">.yo-form--vertical-radio</a-tag>
</span>
<a-radio-group>
<a-radio value="1">攻击</a-radio>
<a-radio value="2">防御</a-radio>
<a-radio value="3">剑技</a-radio>
<a-radio value="4">魔法</a-radio>
<a-radio value="5">回复</a-radio>
<a-radio value="6">虐杀</a-radio>
<a-radio value="7">踩在脚下</a-radio>
<a-radio value="8">
<a-input />
</a-radio>
</a-radio-group>
</a-form-model-item>
</div>
</a-form-model>
</container>
</template>
<script>
import Input from './yo/Input';
export default {
components: {
Input,
},
};
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="yo-form-group">
<a-form-model-item label="输入框">
<a-input autocomplete="off" placeholder="请输入文字" />
</a-form-model-item>
<a-form-model-item label="只读输入框">
<a-input autocomplete="off" placeholder="请输入文字" readonly />
</a-form-model-item>
<a-form-model-item label="前缀和后缀">
<a-input addon-after="后缀" addon-before="前缀" autocomplete="off" placeholder="请输入文字" />
</a-form-model-item>
<a-form-model-item label="输入框组">
<a-input-group compact>
<a-input style="width: 40%" />
<a-input style="width: 60%" />
</a-input-group>
<a-input-group>
<a-row :gutter="8">
<a-col :span="10">
<a-input />
</a-col>
<a-col :span="14">
<a-input />
</a-col>
</a-row>
</a-input-group>
<a-input-group>
<a-row type="flex">
<a-col flex="120px">
<a-select default-value="Zhejiang">
<a-select-option value="Zhejiang">Zhejiang</a-select-option>
<a-select-option value="Jiangsu">Jiangsu</a-select-option>
</a-select>
</a-col>
<a-col flex="auto">
<a-input default-value="Xihu District, Hangzhou" />
</a-col>
</a-row>
</a-input-group>
<a-input-group compact>
<a-select default-value="1">
<a-select-option value="1">Between</a-select-option>
<a-select-option value="2">Except</a-select-option>
</a-select>
<a-input placeholder="Minimum" style=" width: 100px; text-align: center" />
<a-input
disabled
placeholder="~"
style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: transparent"
/>
<a-input placeholder="Maximum" style=" width: 100px; text-align: center; border-left: 0" />
</a-input-group>
</a-form-model-item>
<a-form-model-item class="yo-form--vertical" label="上下布局">
<span slot="help">
上下布局请在
<a-tag color="pink">a-form-model-item</a-tag>上添加类
<a-tag color="pink">.yo-form--vertical</a-tag>
</span>
<a-input autocomplete="off" placeholder="请输入文字" />
</a-form-model-item>
</div>
</template>

View File

@@ -0,0 +1,143 @@
<template>
<container>
<br />
<a-card :bordered="false">
<Auth :auth="{ sysApp: ['page'] }">
<div class="yo-query-bar">
<a-form-model :model="query" layout="inline">
<a-form-model-item label="应用名称">
<a-input placeholder="请输入应用名称" v-model="query.name" />
</a-form-model-item>
<a-form-model-item label="唯一编码">
<a-input placeholder="请输入唯一编码" v-model="query.code" />
</a-form-model-item>
<a-form-model-item>
<a-button html-type="submit" type="primary">查询</a-button>
</a-form-model-item>
</a-form-model>
</div>
</Auth>
<a-table
:bordered="true"
:columns="columns"
:data-source="data"
:pagination="{ pageSize: 20}"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
>
<div class="yo-action-bar" slot="title">
<div class="yo-action-bar--actions">
<a-button>Button</a-button>
<a-button>Button</a-button>
<a-button :disabled="true">Button</a-button>
<a-button>Button</a-button>
<a-button-group>
<a-button>Button</a-button>
<a-button>Button</a-button>
<a-button>Button</a-button>
</a-button-group>
<a-dropdown>
<a-menu slot="overlay">
<a-menu-item key="1">1st item</a-menu-item>
<a-menu-item key="2">2nd item</a-menu-item>
<a-menu-item key="3">3rd item</a-menu-item>
</a-menu>
<a-button>
Actions
<a-icon type="down" />
</a-button>
</a-dropdown>
<a-dropdown-button>
Dropdown
<a-menu slot="overlay">
<a-menu-item key="1">
<a-icon type="user" />1st menu item
</a-menu-item>
<a-menu-item key="2">
<a-icon type="user" />2nd menu item
</a-menu-item>
<a-menu-item key="3">
<a-icon type="user" />3rd item
</a-menu-item>
</a-menu>
</a-dropdown-button>
</div>
</div>
</a-table>
</a-card>
<br />
</container>
</template>
<script>
const _data = [
{
area: '海曙区',
title: '曙光电影院地块',
count: 13,
date: '2021-01-01',
},
{
area: '江北区',
title: '大庆新村地块旧城区改建项目',
count: 322,
date: '2021-01-01',
},
{
area: '宁海县',
title: '桥头胡街道旧城区改造华驰文教地块',
count: 1,
date: '2021-01-01',
},
{
area: '慈溪市',
title: '七二三南延道路工程',
count: 1,
date: '2021-01-01',
},
{
area: '北仑区',
title: '原粮食局宿舍楼1号、2号楼太河路北延工程',
count: 32,
date: '2021-01-01',
},
];
const data = Object.assign([], _data, _data, _data);
data.map((p, i) => (p.key = 'abcdefghijklmnopqrstuvwxyz'[i]));
export default {
data() {
return {
query: {
area: '宁波市',
year: '2021',
},
columns: [
{
title: '区域',
dataIndex: 'area',
},
{
title: '项目名称',
dataIndex: 'title',
},
{
title: '户数',
dataIndex: 'count',
},
{
title: '时间',
dataIndex: 'date',
},
],
data,
selectedRowKeys: [],
};
},
methods: {
onSelectChange(selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys;
},
},
};
</script>

View File

@@ -1,18 +1,19 @@
<template> <template>
<a-form-model :model="formInline" @submit="handleSubmit" @submit.native.prevent layout="inline"> <a-form-model :model="form" @submit="handleSubmit" @submit.native.prevent layout="inline">
<a-form-model-item> <a-form-model-item>
<a-input placeholder="Username" v-model="formInline.user"> <a-input placeholder="Username" v-model="form.user">
<a-icon slot="prefix" style="color:rgba(0,0,0,.25)" type="user" /> <a-icon slot="prefix" style="color:rgba(0,0,0,.25)" type="user" />
</a-input> </a-input>
</a-form-model-item> </a-form-model-item>
<a-form-model-item> <a-form-model-item>
<a-input placeholder="Password" type="password" v-model="formInline.password"> <a-input placeholder="Password" type="password" v-model="form.password">
<a-icon slot="prefix" style="color:rgba(0,0,0,.25)" type="lock" /> <a-icon slot="prefix" style="color:rgba(0,0,0,.25)" type="lock" />
</a-input> </a-input>
</a-form-model-item> </a-form-model-item>
<a-form-model-item> <a-form-model-item>
<a-button <a-button
:disabled="formInline.user === '' || formInline.password === ''" :disabled="form.user === '' || form.password === ''"
:loading="loading"
html-type="submit" html-type="submit"
type="primary" type="primary"
>Log in</a-button> >Log in</a-button>
@@ -25,7 +26,8 @@ import { doLogin } from '@/common/login';
export default { export default {
data() { data() {
return { return {
formInline: { loading: false,
form: {
user: '', user: '',
password: '', password: '',
}, },
@@ -33,9 +35,12 @@ export default {
}, },
methods: { methods: {
handleSubmit(e) { handleSubmit(e) {
this.loading = true;
doLogin({ doLogin({
account: this.formInline.user, account: this.form.user,
password: this.formInline.password, password: this.form.password,
}).then(() => {
this.loading = false;
}); });
}, },
}, },

View File

@@ -13,6 +13,13 @@
{{ pane.title }} {{ pane.title }}
</div> </div>
<a-menu slot="overlay"> <a-menu slot="overlay">
<template v-if="mode === 'development'">
<a-menu-item @click="onCopyComponent(pane)" key="-1">
复制组件地址
<a-tag color="red">dev</a-tag>
</a-menu-item>
<a-menu-divider />
</template>
<a-menu-item @click="onLoadContentWindow(pane.key)" key="0">重新加载</a-menu-item> <a-menu-item @click="onLoadContentWindow(pane.key)" key="0">重新加载</a-menu-item>
<a-menu-divider /> <a-menu-divider />
<a-menu-item :disabled="!pane.closable" @click="$emit('close', pane.key);" key="1">关闭</a-menu-item> <a-menu-item :disabled="!pane.closable" @click="$emit('close', pane.key);" key="1">关闭</a-menu-item>
@@ -42,6 +49,7 @@ export default {
}, },
data() { data() {
return { return {
mode: process.env.VUE_APP_NODE_ENV,
actived: '', actived: '',
}; };
}, },
@@ -79,6 +87,22 @@ export default {
onChange(activeKey) { onChange(activeKey) {
this.$emit('change', activeKey); this.$emit('change', activeKey);
}, },
onCopyComponent(pane) {
try {
const copy = document.createElement('textarea');
document.body.append(copy);
copy.value = `/pages${pane.path}`;
copy.select();
setTimeout(() => {
document.execCommand('copy');
copy.remove();
this.$message.success('已复制到剪切板');
});
} catch {
this.$message.error('复制错误');
}
},
}, },
}; };
</script> </script>

View File

@@ -29,7 +29,7 @@
<container v-else-if="$root.global.settings.layout === 'top-nav'"> <container v-else-if="$root.global.settings.layout === 'top-nav'">
<div class="header-actions"> <div class="header-actions">
<Logo /> <Logo />
<Sider @open="(nav) => $emit('open', nav)" /> <Sider :nav="nav" @open="(nav) => $emit('open', nav)" />
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a-input-search placeholder="请输入检索关键字" /> <a-input-search placeholder="请输入检索关键字" />
@@ -57,5 +57,16 @@ export default {
Logo, Logo,
Sider, Sider,
}, },
props: {
nav: {
default() {
return {
modules: [],
menus: [],
};
},
type: Object,
},
},
}; };
</script> </script>

View File

@@ -1,176 +0,0 @@
<template>
<section>
<a-layout-sider
v-if="$root.global.settings.layout === 'left-menu' || $root.global.settings.layout === 'right-menu'"
v-model="$root.global.settings.siderCollapsed"
width="200"
>
<Logo />
<swiper :options="siderSwiperOptions" :style="{ height: '100%' }" ref="sider-swiper">
<swiper-slide :style="{ height: 'auto' }">
<a-menu
:selectable="false"
:style="{ height: '100%', borderRight: 0 }"
:theme="$root.global.settings.navTheme"
@openChange="onMenuOpenChange"
mode="inline"
>
<template v-for="c1 in menu">
<a-sub-menu :key="c1.id" v-if="c1.children && c1.children.length">
<span slot="title">
<a-icon :type="c1.icon" v-if="c1.icon" />
<span>{{ c1.name }}</span>
</span>
<template v-for="c2 in c1.children">
<a-menu-item :key="c2.id" @click="onOpenContentWindow(c2)">
<a-icon :type="c2.icon" v-if="c2.icon" />
<span>{{ c2.name }}</span>
</a-menu-item>
</template>
</a-sub-menu>
<a-menu-item :key="c1.id" @click="onOpenContentWindow(c1)" v-else>
<a-icon :type="c1.icon" v-if="c1.icon" />
<span>{{ c1.name }}</span>
</a-menu-item>
</template>
</a-menu>
</swiper-slide>
<div class="swiper-scrollbar"></div>
</swiper>
</a-layout-sider>
<template v-else-if="$root.global.settings.layout === 'top-nav'">
<a-menu
:selectable="false"
:style="{ borderBottom: 0 }"
:theme="$root.global.settings.navTheme"
@openChange="onMenuOpenChange"
mode="horizontal"
>
<template v-for="c1 in menu">
<a-sub-menu :key="c1.id" v-if="c1.children && c1.children.length">
<span slot="title">
<a-icon :type="c1.icon" v-if="c1.icon" />
<span>{{ c1.name }}</span>
</span>
<template v-for="c2 in c1.children">
<a-menu-item :key="c2.id" @click="onOpenContentWindow(c2)">
<a-icon :type="c2.icon" v-if="c2.icon" />
<span>{{ c2.name }}</span>
</a-menu-item>
</template>
</a-sub-menu>
<a-menu-item :key="c1.id" @click="onOpenContentWindow(c1)" v-else>
<a-icon :type="c1.icon" v-if="c1.icon" />
<span>{{ c1.name }}</span>
</a-menu-item>
</template>
</a-menu>
</template>
</section>
</template>
<script>
import Logo from './logo';
const menu = [
{
id: '1',
name: '列表',
icon: 'unordered-list',
children: [
{
id: '1-1',
name: '表格及查询',
icon: 'table',
component: '/list/query',
},
],
},
{
id: '2',
name: '表单',
icon: 'form',
children: [
{
id: '2-1',
name: '一般表单',
component: '/form/normal',
},
{
id: '2-2',
name: 'yo表单',
component: '/form/yo',
},
],
},
{
id: '100',
name: '文档',
icon: 'snippets',
children: [
{
id: '100-1',
name: '使用文档',
icon: 'code',
component: '/doc/use',
},
],
},
];
export default {
components: {
Logo,
},
data() {
return {
siderSwiperOptions: {
direction: 'vertical',
slidesPerView: 'auto',
freeMode: true,
scrollbar: {
el: '.swiper-scrollbar',
},
mousewheel: true,
},
menu,
};
},
computed: {
swiper() {
return this.$refs['sider-swiper'].$swiper;
},
},
mounted() {
window.addEventListener('resize', () => {
if (this.$root.global.settings.layout === 'left-menu' || this.$root.global.settings.layout === 'right-menu') {
setTimeout(() => {
this.swiper.update();
}, 300);
}
});
// this.$api.getMenu().then(({ result: { data } }) => {
// this.menu = data;
// });
},
methods: {
onMenuOpenChange() {
this.windowTriggerResize();
},
windowTriggerResize() {
let e = new Event('resize');
window.dispatchEvent(e);
},
onOpenContentWindow(nav) {
this.$emit('open', {
key: nav.id,
title: nav.name,
icon: nav.icon,
path: nav.component,
});
},
},
};
</script>

View File

@@ -0,0 +1,89 @@
<template>
<section>
<a-layout-sider
v-if="$root.global.settings.layout === 'left-menu' || $root.global.settings.layout === 'right-menu'"
v-model="$root.global.settings.siderCollapsed"
width="200"
>
<Logo />
<swiper :options="siderSwiperOptions" :style="{ height: '100%' }" ref="sider-swiper">
<swiper-slide :style="{ height: 'auto' }">
<Menu
:menu-style="{ height: '100%', borderRight: 0 }"
:nav="nav"
@openChange="onMenuOpenChange"
mode="inline"
/>
</swiper-slide>
<div class="swiper-scrollbar"></div>
</swiper>
</a-layout-sider>
<template v-else-if="$root.global.settings.layout === 'top-nav'">
<Menu
:menu-style="{ borderBottom: 0 }"
:nav="nav"
@openChange="onMenuOpenChange"
mode="horizontal"
/>
</template>
</section>
</template>
<script>
import Logo from '../logo';
import Menu from './menu';
export default {
components: {
Logo,
Menu,
},
props: {
nav: {
default() {
return {
modules: [],
menus: [],
};
},
type: Object,
},
},
data() {
return {
siderSwiperOptions: {
direction: 'vertical',
slidesPerView: 'auto',
freeMode: true,
scrollbar: {
el: '.swiper-scrollbar',
},
mousewheel: true,
},
};
},
computed: {
swiper() {
return this.$refs['sider-swiper'].$swiper;
},
},
mounted() {
window.addEventListener('resize', () => {
if (this.$root.global.settings.layout === 'left-menu' || this.$root.global.settings.layout === 'right-menu') {
setTimeout(() => {
this.swiper.update();
}, 300);
}
});
},
methods: {
onMenuOpenChange() {
this.windowTriggerResize();
},
windowTriggerResize() {
let e = new Event('resize');
window.dispatchEvent(e);
},
},
};
</script>

View File

@@ -0,0 +1,100 @@
<template>
<a-menu
:mode="mode"
:selectable="false"
:style="menuStyle"
:theme="$root.global.settings.navTheme"
@openChange="onMenuOpenChange"
>
<template v-for="c1 in menu">
<a-sub-menu :key="c1.id" v-if="c1.children && c1.children.length">
<span slot="title">
<a-icon :type="c1.meta.icon" v-if="c1.meta.icon" />
<span>{{ c1.meta.title }}</span>
</span>
<template v-for="c2 in c1.children">
<a-menu-item :key="c2.id" @click="onOpenContentWindow(c2)">
<a-icon :type="c2.meta.icon" v-if="c2.meta.icon" />
<span>{{ c2.meta.title }}</span>
</a-menu-item>
</template>
</a-sub-menu>
<a-menu-item :key="c1.id" @click="onOpenContentWindow(c1)" v-else>
<a-icon :type="c1.meta.icon" v-if="c1.meta.icon" />
<span>{{ c1.meta.title }}</span>
</a-menu-item>
</template>
</a-menu>
</template>
<script>
export default {
props: {
nav: {
default() {
return {
modules: [],
menus: [],
};
},
type: Object,
},
menuStyle: {
default() {
return {};
},
type: Object || String,
},
mode: {
default: 'inline',
type: String,
},
},
data() {
return {
menu: [],
};
},
created() {
this.serializeMenu();
},
watch: {
nav: {
deep: true,
handler() {
this.serializeMenu();
},
},
},
methods: {
serializeMenu() {
const menu = this.$_.cloneDeep(this.nav.menus);
const children = this.$_.groupBy(menu, 'pid');
this.menu = menu
.filter((p) => !p.pid)
.map((p) => {
return {
...p,
children: children[p.id],
};
});
},
onMenuOpenChange() {
this.$emit('openChange');
},
onOpenContentWindow(menu) {
this.openContentWindow({
key: menu.id,
title: menu.meta.title,
icon: menu.meta.icon,
path: menu.component,
});
},
},
};
</script>

View File

@@ -7,9 +7,14 @@
[`yo-layout--${$root.global.settings.layout}`]: true, [`yo-layout--${$root.global.settings.layout}`]: true,
}" }"
> >
<Sider @open="onOpenContentWindow" v-if="$root.global.settings.layout === 'left-menu'" /> <Sider
:nav="nav"
@open="onOpenContentWindow"
v-if="$root.global.settings.layout === 'left-menu'"
/>
<a-layout> <a-layout>
<Header <Header
:nav="nav"
@open="onOpenContentWindow" @open="onOpenContentWindow"
@reload="onReloadContentWindow" @reload="onReloadContentWindow"
@setting="setting.visible = true" @setting="setting.visible = true"
@@ -26,7 +31,11 @@
/> />
</a-layout> </a-layout>
</a-layout> </a-layout>
<Sider @open="onOpenContentWindow" v-if="$root.global.settings.layout === 'right-menu'" /> <Sider
:nav="nav"
@open="onOpenContentWindow"
v-if="$root.global.settings.layout === 'right-menu'"
/>
</a-layout> </a-layout>
<Setting :visible="setting.visible" @close="setting.visible = false" /> <Setting :visible="setting.visible" @close="setting.visible = false" />
</section> </section>
@@ -41,11 +50,14 @@ import 'moment/locale/zh-cn';
moment.locale('zh-cn'); moment.locale('zh-cn');
import Header from './_layout/header'; import Header from './_layout/header';
import Sider from './_layout/sider'; import Sider from './_layout/sider/index';
import Content from './_layout/content'; import Content from './_layout/content';
import Setting from './setting'; import Setting from './setting';
import { api } from '@/common/api';
import { setGlobal } from '@/common/login';
const getNewID = () => { const getNewID = () => {
return Math.random().toString(16).slice(2); return Math.random().toString(16).slice(2);
}; };
@@ -67,9 +79,28 @@ export default {
setting: { setting: {
visible: false, visible: false,
}, },
nav: {
loaded: false,
modules: [],
menus: [],
},
}; };
}, },
created() {
api.getLoginUser().then(({ data }) => {
const info = { ...data };
delete info.apps;
delete info.menus;
setGlobal(info);
this.nav.modules = data.apps;
this.nav.menus = data.menus;
this.nav.loaded = true;
});
},
mounted() { mounted() {
Vue.prototype.openContentWindow = this.onOpenContentWindow; Vue.prototype.openContentWindow = this.onOpenContentWindow;
Vue.prototype.closeContentWindow = this.onCloseContentWindow; Vue.prototype.closeContentWindow = this.onCloseContentWindow;
@@ -98,7 +129,7 @@ export default {
closable, closable,
}; };
if (settings.path && settings.path.startsWith('/')) { if (settings.path) {
const key = settings.key || getNewID(); const key = settings.key || getNewID();
/** /**
@@ -110,6 +141,8 @@ export default {
return; return;
} }
const path = settings.path.startsWith('/') ? settings.path : `/${settings.path}`;
/** /**
* 向标签页队列中添加一个新的标签页 * 向标签页队列中添加一个新的标签页
*/ */
@@ -119,7 +152,7 @@ export default {
icon: settings.icon, icon: settings.icon,
title: settings.title || '新建窗口', title: settings.title || '新建窗口',
component: null, component: null,
path: settings.path, path,
loaded: false, loaded: false,
}); });

View File

@@ -2,6 +2,14 @@ module.exports = {
devServer: { devServer: {
open: true, open: true,
port: 6588, port: 6588,
proxy: {
'/api': {
target: process.env.VUE_APP_BASE_URL,
pathRewrite: {
'^/api': ''
}
}
}
}, },
lintOnSave: false, lintOnSave: false,
css: { css: {