新增:菜单组件增加国际化语言支持;

master
iczer 4 years ago
parent 68249a0458
commit b16b5893c8
  1. BIN
      public/favicon.ico
  2. BIN
      src/assets/img/logo.png
  3. BIN
      src/assets/img/vue-antd-logo.png
  4. 3
      src/components/menu/SiderMenu.vue
  5. 146
      src/components/menu/menu.js
  6. 1
      src/config/index.js
  7. 26
      src/layouts/GlobalHeader.vue
  8. 1
      src/layouts/GlobalLayout.vue
  9. 4
      src/layouts/HeaderSearch.vue
  10. 2
      src/pages/login/Login.vue
  11. 48
      src/router/i18n.js
  12. 108
      src/router/lazy.js
  13. 4
      src/store/modules/account.js
  14. 3
      src/store/modules/setting.js
  15. 14
      src/theme/default/color.less
  16. 24
      src/utils/Objects.js
  17. 48
      src/utils/utils.less

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@ -6,7 +6,7 @@
<h1>{{systemName}}</h1>
</router-link>
</div>
<i-menu :theme="theme" :collapsed="collapsed" :menuData="menuData" @select="onSelect" class="menu"/>
<i-menu :i18n="menuI18n" :theme="theme" :collapsed="collapsed" :options="menuData" @select="onSelect" class="menu"/>
</a-layout-sider>
</template>
@ -15,6 +15,7 @@ import IMenu from './menu'
export default {
name: 'SiderMenu',
components: {IMenu},
inject: ['menuI18n'],
props: {
collapsible: {
type: Boolean,

@ -1,35 +1,46 @@
/**
* 该插件可根据菜单配置自动生成 ANTD menu组件
* menuData示例
* menuOptions示例
* [
* {
* title: '菜单标题',
* icon: '菜单图标',
* path: '菜单路由',
* invisible: 'boolean, 是否不可见',
* meta: {
* icon: '菜单图标',
* invisible: 'boolean, 是否不可见, 默认 false',
* },
* children: [子菜单配置]
* },
* {
* title: '菜单标题',
* icon: '菜单图标',
* path: '菜单路由',
* invisible: 'boolean, 是否不可见',
* meta: {
* icon: '菜单图标',
* invisible: 'boolean, 是否不可见, 默认 false',
* },
* children: [子菜单配置]
* }
* ]
*
* i18n: 国际化配置组件默认会根据 options route配置的 path name 生成英文以及中文的国际化配置如需自定义或增加其他语言配置
* 此项即可
* i18n: {
* CN: {dashboard: {name: '监控中心'}}
* HK: {dashboard: {name: '監控中心'}}
* }
**/
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
import '@/utils/Objects'
const {Item, SubMenu} = Menu
// 默认菜单图标数组,如果菜单没配置图标,则会设置从该数组随机取一个图标配置
const iconArr = ['dashboard', 'user', 'form', 'setting', 'message', 'safety', 'bell', 'delete', 'code-o', 'poweroff', 'eye-o', 'hourglass']
export default {
name: 'IMenu',
i18n: {
},
props: {
menuData: {
options: {
type: Array,
required: true
},
@ -47,7 +58,8 @@ export default {
type: Boolean,
required: false,
default: false
}
},
i18n: Object
},
data () {
return {
@ -59,14 +71,26 @@ export default {
computed: {
rootSubmenuKeys: (vm) => {
let keys = []
vm.menuData.forEach(item => {
vm.options.forEach(item => {
keys.push(item.path)
})
return keys
}
},
beforeMount() {
let CN = this.generateI18n(new Object(), this.options, 'name')
let US = this.generateI18n(new Object(), this.options, 'path')
this.$i18n.setLocaleMessage('CN', CN)
this.$i18n.setLocaleMessage('US', US)
if(this.i18n) {
Object.keys(this.i18n).forEach(key => {
this.$i18n.mergeLocaleMessage(key, this.i18n[key])
})
}
},
created () {
this.updateMenu()
this.formatOptions(this.options, '')
},
watch: {
collapsed (val) {
@ -83,65 +107,72 @@ export default {
},
methods: {
renderIcon: function (h, icon) {
return icon === 'none' ? null
: h(
Icon,
{
props: {type: icon !== undefined ? icon : iconArr[Math.floor((Math.random() * iconArr.length))]}
})
return !icon || icon == 'none' ? null : h(Icon, {props: {type: icon}})
},
renderMenuItem: function (h, menu, pIndex, index) {
renderMenuItem: function (h, menu) {
return h(
Item,
{
key: menu.path ? menu.path : 'item_' + pIndex + '_' + index
},
Item, {key: menu.fullPath},
[
h(
'RouterLink',
// {attrs: {href: '#' + menu.path}},
{attrs: {to: menu.path}},
h('a', {attrs: {href: '#' + menu.fullPath}},
[
this.renderIcon(h, menu.icon),
h('span', [menu.name])
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none'),
h('span', [this.$t(menu.fullPath.substring(1).replace(new RegExp('/', 'g'), '.') + '.name')])
]
)
]
)
},
renderSubMenu: function (h, menu, pIndex, index) {
var this2_ = this
var subItem = [h('span',
{slot: 'title'},
renderSubMenu: function (h, menu) {
let this_ = this
let subItem = [h('span', {slot: 'title'},
[
this.renderIcon(h, menu.icon),
h('span', [menu.name])
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none'),
h('span', [this.$t(menu.fullPath.substring(1).replace(new RegExp('/', 'g'), '.') + '.name')])
]
)]
var itemArr = []
var pIndex_ = pIndex + '_' + index
menu.children.forEach(function (item, i) {
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
let itemArr = []
menu.children.forEach(function (item) {
itemArr.push(this_.renderItem(h, item))
})
return h(
SubMenu,
{key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index},
return h(SubMenu, {key: menu.fullPath},
subItem.concat(itemArr)
)
},
renderItem: function (h, menu, pIndex, index) {
if (!menu.invisible) {
return menu.children ? this.renderSubMenu(h, menu, pIndex, index) : this.renderMenuItem(h, menu, pIndex, index)
renderItem: function (h, menu) {
const meta = menu.meta
if (!meta || !meta.invisible) {
let renderChildren = false
const children = menu.children
if (children != undefined) {
for (let i = 0; i < children.length; i++) {
const childMeta = children[i].meta
if (!childMeta || !childMeta.invisible) {
renderChildren = true
break
}
}
}
return (menu.children && renderChildren) ? this.renderSubMenu(h, menu) : this.renderMenuItem(h, menu)
}
},
renderMenu: function (h, menuTree) {
var this2_ = this
var menuArr = []
let this_ = this
let menuArr = []
menuTree.forEach(function (menu, i) {
menuArr.push(this2_.renderItem(h, menu, '0', i))
menuArr.push(this_.renderItem(h, menu, '0', i))
})
return menuArr
},
formatOptions(options, parentPath) {
let this_ = this
options.forEach(route => {
let isFullPath = route.path.substring(0, 1) == '/'
route.fullPath = isFullPath ? route.path : parentPath + '/' + route.path
if (route.children) {
this_.formatOptions(route.children, route.fullPath)
}
})
},
onOpenChange (openKeys) {
const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
@ -152,12 +183,29 @@ export default {
},
updateMenu () {
let routes = this.$route.matched.concat()
this.selectedKeys = [routes.pop().path]
const route = routes.pop()
this.selectedKeys = [this.getSelectedKey(route)]
let openKeys = []
routes.forEach((item) => {
openKeys.push(item.path)
})
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.openKeys = openKeys
},
getSelectedKey (route) {
if (route.meta.invisible && route.parent) {
return this.getSelectedKey(route.parent)
}
return route.path
},
generateI18n(lang, options, valueKey) {
options.forEach(menu => {
let keys = menu.fullPath.substring(1).split('/').concat('name')
lang.assignProps(keys, menu[valueKey])
if (menu.children) {
this.generateI18n(lang, menu.children, valueKey)
}
})
return lang
}
},
render (h) {
@ -177,7 +225,7 @@ export default {
this.$emit('select', obj)
}
}
}, this.renderMenu(h, this.menuData)
}, this.renderMenu(h, this.options)
)
}
}

@ -1,5 +1,6 @@
// 系统配置
module.exports = {
lang: 'CN',
themeColor: '#1890ff',
theme: 'dark',
layout: 'side',

@ -8,7 +8,7 @@
<a-divider v-if="isMobile" type="vertical" />
<a-icon v-if="layout === 'side'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
<div v-if="layout == 'head' && !isMobile" class="global-header-menu">
<i-menu style="height: 64px; line-height: 64px;" :theme="headerTheme" mode="horizontal" :menuData="menuData" @select="onSelect"/>
<i-menu style="height: 64px; line-height: 64px;" :i18n="menuI18n" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
</div>
<div :class="['global-header-right', headerTheme]">
<header-search class="header-item" />
@ -19,6 +19,16 @@
</a-tooltip>
<header-notice class="header-item"/>
<header-avatar class="header-item"/>
<a-dropdown class="lang header-item">
<div>
<a-icon type="global"/>
</div>
<a-menu @click="changeLang" :selected-keys="[lang]" slot="overlay">
<a-menu-item key="CN"><span >cn</span> 简体中文</a-menu-item>
<a-menu-item key="HK"><span >hk</span> 繁体中文</a-menu-item>
<a-menu-item key="US"><span >us</span> English</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</div>
</a-layout-header>
@ -40,8 +50,9 @@ export default {
headerTheme: this.theme
}
},
inject: ['menuI18n'],
computed: {
...mapState('setting', ['theme', 'isMobile', 'layout', 'systemName']),
...mapState('setting', ['theme', 'isMobile', 'layout', 'systemName', 'lang']),
headerTheme () {
return (this.layout == 'side' && !this.isMobile) ? 'light' : this.theme
}
@ -52,6 +63,9 @@ export default {
},
onSelect (obj) {
this.$emit('menuSelect', obj)
},
changeLang(lang) {
this.$store.commit('setting/setLang', lang.key)
}
}
}
@ -69,7 +83,7 @@ export default {
}
}
.global-header{
padding: 0 12px 0 0;
padding: 0;
-webkit-box-shadow: 0 1px 4px rgba(0,21,41,.08);
box-shadow: 0 1px 4px rgba(0,21,41,.08);
z-index: 1;
@ -124,11 +138,17 @@ export default {
a, i{
color: #fff !important;
}
.header-item:hover{
background-color: @primary-color;
}
}
.header-item{
padding: 0 12px;
cursor: pointer;
align-self: center;
&:hover{
background-color: @gray-3;
}
i{
font-size: 16px;
color: rgba(0,0,0,.65);

@ -51,6 +51,7 @@ export default {
provide() {
return{
layoutMinHeight: minHeight,
menuI18n: require('@/router/i18n').default
}
},
computed: {

@ -1,5 +1,5 @@
<template>
<span class="header-search">
<div class="header-search">
<a-icon type="search" class="search-icon" @click="enterSearchMode"/>
<a-auto-complete
ref="input"
@ -9,7 +9,7 @@
@blur="leaveSearchMode"
>
</a-auto-complete>
</span>
</div>
</template>
<script>

@ -109,7 +109,7 @@ export default {
if (result.code >= 0) {
const user = result.data.user
this.$router.push('/dashboard/workplace')
this.$store.commit('account/setuser', user)
this.$store.commit('account/setUser', user)
this.$message.success(result.message, 3)
} else {
this.error = result.message

@ -0,0 +1,48 @@
export default {
HK: {
dashboard: {
name: 'Dashboard',
workplace: {name: '工作台'},
analysis: {name: '分析頁'}
},
form: {
name: '表單頁',
basic: {name: '基礎表單'},
step: {name: '分步表單'},
advance: {name: '分步表單'}
},
list: {
name: '列表頁',
query: {name: '查詢表格'},
primary: {name: '標準列表'},
card: {name: '卡片列表'},
search: {
name: '搜索列表',
article: {name: '文章'},
application: {name: '應用'},
project: {name: '項目'}
}
},
details: {
name: '詳情頁',
basic: {name: '基礎詳情頁'},
advance: {name: '高級詳情頁'}
},
result: {
name: '結果頁',
success: {name: '成功'},
error: {name: '失敗'}
},
exception: {
name: '異常頁',
404: {name: '404'},
403: {name: '403'},
500: {name: '500'}
},
components: {
name: '小組件',
taskCard: {name: '任務卡片'},
palette: {name: '顏色複選框'}
}
}
}

@ -20,190 +20,180 @@ export default new Router({
name: '首页',
component: MenuView,
redirect: '/login',
icon: 'none',
invisible: true,
children: [
{
path: '/dashboard',
path: 'dashboard',
name: 'Dashboard',
meta: {
icon: 'dashboard'
},
component: RouteView,
icon: 'dashboard',
children: [
{
path: '/dashboard/workplace',
path: 'workplace',
name: '工作台',
component: () => import('@/pages/dashboard/WorkPlace'),
icon: 'none'
},
{
path: '/dashboard/analysis',
path: 'analysis',
name: '分析页',
component: () => import('@/pages/dashboard/Analysis'),
icon: 'none'
}
]
},
{
path: '/form',
path: 'form',
name: '表单页',
meta: {
icon: 'form',
},
component: PageView,
icon: 'form',
children: [
{
path: '/form/basic',
path: 'basic',
name: '基础表单',
component: () => import('@/pages/form/BasicForm'),
icon: 'none'
},
{
path: '/form/step',
path: 'step',
name: '分步表单',
component: () => import('@/pages/form/stepForm/StepForm'),
icon: 'none'
},
{
path: '/form/advanced',
path: 'advance',
name: '高级表单',
component: () => import('@/pages/form/advancedForm/AdvancedForm'),
icon: 'none'
}
]
},
{
path: '/list',
path: 'list',
name: '列表页',
meta: {
icon: 'table'
},
component: PageView,
icon: 'table',
children: [
{
path: '/list/query',
path: 'query',
name: '查询表格',
component: () => import('@/pages/list/QueryList'),
icon: 'none'
},
{
path: '/list/primary',
path: 'primary',
name: '标准列表',
component: () => import('@/pages/list/StandardList'),
icon: 'none'
},
{
path: '/list/card',
path: 'card',
name: '卡片列表',
component: () => import('@/pages/list/CardList'),
icon: 'none'
},
{
path: '/list/search',
path: 'search',
name: '搜索列表',
component: () => import('@/pages/list/search/SearchLayout'),
icon: 'none',
children: [
{
path: '/list/search/article',
path: 'article',
name: '文章',
component: () => import('@/pages/list/search/ArticleList'),
icon: 'none'
},
{
path: '/list/search/application',
path: 'application',
name: '应用',
component: () => import('@/pages/list/search/ApplicationList'),
icon: 'none'
},
{
path: '/list/search/project',
path: 'project',
name: '项目',
component: () => import('@/pages/list/search/ProjectList'),
icon: 'none'
}
]
}
]
},
{
path: '/detail',
path: 'details',
name: '详情页',
icon: 'profile',
meta: {
icon: 'profile'
},
component: RouteView,
children: [
{
path: '/detail/basic',
path: 'basic',
name: '基础详情页',
icon: 'none',
component: () => import('@/pages/detail/BasicDetail')
},
{
path: '/detail/advanced',
path: 'advance',
name: '高级详情页',
icon: 'none',
component: () => import('@/pages/detail/AdvancedDetail')
}
]
},
{
path: '/result',
path: 'result',
name: '结果页',
icon: 'check-circle-o',
meta: {
icon: 'check-circle-o',
},
component: PageView,
children: [
{
path: '/result/success',
path: 'success',
name: '成功',
icon: 'none',
component: () => import('@/pages/result/Success')
},
{
path: '/result/error',
path: 'error',
name: '失败',
icon: 'none',
component: () => import('@/pages/result/Error')
}
]
},
{
path: '/exception',
path: 'exception',
name: '异常页',
icon: 'warning',
meta: {
icon: 'warning',
},
component: RouteView,
children: [
{
path: '/exception/404',
path: '404',
name: '404',
icon: 'none',
component: () => import('@/pages/exception/404')
},
{
path: '/exception/403',
path: '403',
name: '403',
icon: 'none',
component: () => import('@/pages/exception/403')
},
{
path: '/exception/500',
path: '500',
name: '500',
icon: 'none',
component: () => import('@/pages/exception/500')
}
]
},
{
path: '/components',
redirect: '/components/taskcard',
path: 'components',
name: '小组件',
icon: 'appstore-o',
meta: {
icon: 'appstore-o'
},
component: PageView,
children: [
{
path: '/components/taskcard',
path: 'taskCard',
name: '任务卡片',
icon: 'none',
component: () => import('@/pages/components/TaskCard')
},
{
path: '/components/palette',
path: 'palette',
name: '颜色复选框',
icon: 'none',
component: () => import('@/pages/components/Palette')
}
]

@ -1,6 +1,6 @@
import PouchDB from 'pouchdb'
var db = new PouchDB('admindb')
let db = new PouchDB('adminDb')
export default {
namespaced: true,
@ -8,7 +8,7 @@ export default {
user: {}
},
mutations: {
setuser (state, user) {
setUser (state, user) {
state.user = user
db.get('currUser').then(doc => {
db.put({

@ -33,6 +33,9 @@ export default {
},
setFixedSider(state, fixedSider) {
state.fixedSider = fixedSider
},
setLang(state, lang) {
state.lang = lang
}
}
}

@ -14,3 +14,17 @@
@primary-8: color(~`colorPalette('@{primary}', 8) `);
@primary-9: color(~`colorPalette('@{primary}', 9) `);
@primary-10: color(~`colorPalette('@{primary}', 10) `);
@gray-1: #ffffff;
@gray-2: #fafafa;
@gray-3: #f5f5f5;
@gray-4: #f0f0f0;
@gray-5: #d9d9d9;
@gray-6: #bfbfbf;
@gray-7: #8c8c8c;
@gray-8: #595959;
@gray-9: #434343;
@gray-10: #262626;
@gray-11: #1f1f1f;
@gray-12: #141414;
@gray-13: #000000;

@ -0,0 +1,24 @@
/**
* 给对象注入属性
* @param keys 属性key数组 keys = ['config', 'path'] , 则会给对象注入 object.config.path 的属性
* @param value 属性值
* @returns {Object}
*/
Object.defineProperty(Object.prototype, 'assignProps', {
writable: false,
enumerable: false,
configurable: true,
value: function (keys, value) {
let props = this
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
if (i == keys.length - 1) {
props[key] = value
} else {
props[key] = props[key] == undefined ? {} : props[key]
props = props[key]
}
}
return this
}
})

@ -1,48 +0,0 @@
.textOverflow() {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
overflow: hidden;
position: relative;
line-height: 1.5em;
max-height: @line * 1.5em;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
&:before {
background: @bg;
content: '...';
padding: 0 1px;
position: absolute;
right: 14px;
bottom: 0;
}
&:after {
background: white;
content: '';
margin-top: 0.2em;
position: absolute;
right: 14px;
width: 1em;
height: 1em;
}
}
.clearfix() {
zoom: 1;
&:before,
&:after {
content: ' ';
display: table;
}
&:after {
clear: both;
visibility: hidden;
font-size: 0;
height: 0;
}
}
Loading…
Cancel
Save