This commit is contained in:
孟帅
2022-02-25 17:11:17 +08:00
parent 9bd05abb2c
commit 8f3d679a57
897 changed files with 95731 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
<template>
<div class="antd-pro-components-article-list-content-index-listContent">
<div class="description">
<slot>
{{ description }}
</slot>
</div>
<div class="extra">
<a-avatar :src="avatar" size="small" />
<a :href="href">{{ owner }}</a> 发布在 <a :href="href">{{ href }}</a>
<em>{{ updateAt | moment }}</em>
</div>
</div>
</template>
<script>
export default {
name: 'ArticleListContent',
props: {
prefixCls: {
type: String,
default: 'antd-pro-components-article-list-content-index-listContent'
},
description: {
type: String,
default: ''
},
owner: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
href: {
type: String,
required: true
},
updateAt: {
type: String,
required: true
}
}
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-article-list-content-index-listContent {
.description {
max-width: 720px;
line-height: 22px;
}
.extra {
margin-top: 16px;
color: @text-color-secondary;
line-height: 22px;
& /deep/ .ant-avatar {
position: relative;
top: 1px;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: top;
}
& > em {
margin-left: 16px;
color: @disabled-color;
font-style: normal;
}
}
}
@media screen and (max-width: @screen-xs) {
.antd-pro-components-article-list-content-index-listContent {
.extra {
& > em {
display: block;
margin-top: 8px;
margin-left: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
import ArticleListContent from './ArticleListContent'
export default ArticleListContent

View File

@@ -0,0 +1,24 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { Tooltip, Avatar } from 'ant-design-vue'
import { getSlotOptions } from 'ant-design-vue/lib/_util/props-util'
import { warning } from 'ant-design-vue/lib/vc-util/warning'
export const AvatarListItemProps = {
tips: PropTypes.string.def(null),
src: PropTypes.string.def('')
}
const Item = {
__ANT_AVATAR_CHILDREN: true,
name: 'AvatarListItem',
props: AvatarListItemProps,
created () {
warning(getSlotOptions(this.$parent).__ANT_AVATAR_LIST, 'AvatarListItem must be a subcomponent of AvatarList')
},
render () {
const AvatarDom = <Avatar size={this.$parent.size} src={this.src} />
return this.tips && <Tooltip title={this.tips}>{AvatarDom}</Tooltip> || <AvatarDom />
}
}
export default Item

View File

@@ -0,0 +1,72 @@
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import Avatar from 'ant-design-vue/es/avatar'
import Item from './Item.jsx'
import { filterEmpty } from '@/components/_util/util'
/**
* size: `number`、 `large`、`small`、`default` 默认值: default
* maxLength: number
* excessItemsStyle: CSSProperties
*/
const AvatarListProps = {
prefixCls: PropTypes.string.def('ant-pro-avatar-list'),
size: {
validator: val => {
return typeof val === 'number' || ['small', 'large', 'default'].includes(val)
},
default: 'default'
},
maxLength: PropTypes.number.def(0),
excessItemsStyle: PropTypes.object.def({
color: '#f56a00',
backgroundColor: '#fde3cf'
})
}
const AvatarList = {
__ANT_AVATAR_LIST: true,
Item,
name: 'AvatarList',
props: AvatarListProps,
render (h) {
const { prefixCls, size } = this.$props
const className = {
[`${prefixCls}`]: true,
[`${size}`]: true
}
const items = filterEmpty(this.$slots.default)
const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{this.getItems(items)}</ul> : null
return (
<div class={className}>
{itemsDom}
</div>
)
},
methods: {
getItems (items) {
const className = {
[`${this.prefixCls}-item`]: true,
[`${this.size}`]: true
}
const totalSize = items.length
if (this.maxLength > 0) {
items = items.slice(0, this.maxLength)
items.push((<Avatar size={this.size} style={this.excessItemsStyle}>{`+${totalSize - this.maxLength}`}</Avatar>))
}
return items.map((item) => (
<li class={className}>{item}</li>
))
}
}
}
AvatarList.install = function (Vue) {
Vue.component(AvatarList.name, AvatarList)
Vue.component(AvatarList.Item.name, AvatarList.Item)
}
export default AvatarList

View File

@@ -0,0 +1,9 @@
import AvatarList from './List'
import Item from './Item'
export {
AvatarList,
Item as AvatarListItem
}
export default AvatarList

View File

@@ -0,0 +1,60 @@
@import "../index";
@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
.@{avatar-list-prefix-cls} {
display: inline-block;
ul {
list-style: none;
display: inline-block;
padding: 0;
margin: 0 0 0 8px;
font-size: 0;
}
}
.@{avatar-list-item-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
margin-left: -8px;
width: @avatar-size-base;
height: @avatar-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
cursor: pointer;
}
}
&.large {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
&.small {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
&.mini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
# AvatarList 用户头像列表
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
引用方式:
```javascript
import AvatarList from '@/components/AvatarList'
const AvatarListItem = AvatarList.Item
export default {
components: {
AvatarList,
AvatarListItem
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<avatar-list size="mini">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
```html
<avatar-list :max-length="3">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | -------- | ---------------------------------- | --------- |
| size | 头像大小 | `large``small``mini`, `default` | `default` |
| maxLength | 要显示的最大项目 | number | - |
| excessItemsStyle | 多余的项目风格 | CSSProperties | - |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
| ---- | ------ | --------- | --- |
| tips | 头像展示文案 | string | - |
| src | 头像图片连接 | string | - |

View File

@@ -0,0 +1,113 @@
import Modal from 'ant-design-vue/es/modal'
export default (Vue) => {
function dialog (component, componentProps, modalProps) {
const _vm = this
modalProps = modalProps || {}
if (!_vm || !_vm._isVue) {
return
}
let dialogDiv = document.querySelector('body>div[type=dialog]')
if (!dialogDiv) {
dialogDiv = document.createElement('div')
dialogDiv.setAttribute('type', 'dialog')
document.body.appendChild(dialogDiv)
}
const handle = function (checkFunction, afterHandel) {
if (checkFunction instanceof Function) {
const res = checkFunction()
if (res instanceof Promise) {
res.then(c => {
c && afterHandel()
})
} else {
res && afterHandel()
}
} else {
// checkFunction && afterHandel()
checkFunction || afterHandel()
}
}
const dialogInstance = new Vue({
data () {
return {
visible: true
}
},
router: _vm.$router,
store: _vm.$store,
mounted () {
this.$on('close', (v) => {
this.handleClose()
})
},
methods: {
handleClose () {
handle(this.$refs._component.onCancel, () => {
this.visible = false
this.$refs._component.$emit('close')
this.$refs._component.$emit('cancel')
dialogInstance.$destroy()
})
},
handleOk () {
handle(this.$refs._component.onOK || this.$refs._component.onOk, () => {
this.visible = false
this.$refs._component.$emit('close')
this.$refs._component.$emit('ok')
dialogInstance.$destroy()
})
}
},
render: function (h) {
const that = this
const modalModel = modalProps && modalProps.model
if (modalModel) {
delete modalProps.model
}
const ModalProps = Object.assign({}, modalModel && { model: modalModel } || {}, {
attrs: Object.assign({}, {
...(modalProps.attrs || modalProps)
}, {
visible: this.visible
}),
on: Object.assign({}, {
...(modalProps.on || modalProps)
}, {
ok: () => {
that.handleOk()
},
cancel: () => {
that.handleClose()
}
})
})
const componentModel = componentProps && componentProps.model
if (componentModel) {
delete componentProps.model
}
const ComponentProps = Object.assign({}, componentModel && { model: componentModel } || {}, {
ref: '_component',
attrs: Object.assign({}, {
...((componentProps && componentProps.attrs) || componentProps)
}),
on: Object.assign({}, {
...((componentProps && componentProps.on) || componentProps)
})
})
return h(Modal, ModalProps, [h(component, ComponentProps)])
}
}).$mount(dialogDiv)
}
Object.defineProperty(Vue.prototype, '$dialog', {
get: () => {
return function () {
dialog.apply(this, arguments)
}
}
})
}

View File

@@ -0,0 +1,64 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
/*
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
*/
export default {
name: 'Ellipsis',
components: {
Tooltip
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis'
},
tooltip: {
type: Boolean
},
length: {
type: Number,
required: true
},
lines: {
type: Number,
default: 1
},
fullWidthRecognition: {
type: Boolean,
default: false
}
},
methods: {
getStrDom (str, fullLength) {
return (
<span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
)
},
getTooltip (fullStr, fullLength) {
return (
<Tooltip>
<template slot="title">{ fullStr }</template>
{ this.getStrDom(fullStr, fullLength) }
</Tooltip>
)
}
},
render () {
const { tooltip, length } = this.$props
const str = this.$slots.default.map(vNode => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return (
strDom
)
}
}
</script>

View File

@@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis

View File

@@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -

View File

@@ -0,0 +1,47 @@
<template>
<div :class="prefixCls" :style="{ width: barWidth, transition: '0.3s all' }">
<div style="float: left">
<slot name="extra">{{ extra }}</slot>
</div>
<div style="float: right">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FooterToolBar',
props: {
prefixCls: {
type: String,
default: 'ant-pro-footer-toolbar'
},
collapsed: {
type: Boolean,
default: false
},
isMobile: {
type: Boolean,
default: false
},
siderWidth: {
type: Number,
default: undefined
},
extra: {
type: [String, Object],
default: ''
}
},
computed: {
barWidth () {
return this.isMobile ? undefined : `calc(100% - ${this.collapsed ? 80 : this.siderWidth || 256}px)`
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,4 @@
import FooterToolBar from './FooterToolBar'
import './index.less'
export default FooterToolBar

View File

@@ -0,0 +1,23 @@
@import "../index";
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
.@{footer-toolbar-prefix-cls} {
position: fixed;
width: 100%;
bottom: 0;
right: 0;
height: 56px;
line-height: 56px;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
background: #fff;
border-top: 1px solid #e8e8e8;
padding: 0 24px;
z-index: 9;
&:after {
content: "";
display: block;
clear: both;
}
}

View File

@@ -0,0 +1,48 @@
# FooterToolbar 底部工具栏
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
引用方式:
```javascript
import FooterToolBar from '@/components/FooterToolbar'
export default {
components: {
FooterToolBar
}
}
```
## 代码演示
```html
<footer-tool-bar>
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
```html
<footer-tool-bar extra="扩展信息提示">
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children (slot) | 工具栏内容,向右对齐 | - | -
extra | 额外信息,向左对齐 | String, Object | -

View File

@@ -0,0 +1,21 @@
<template>
<global-footer class="footer custom-render">
<template v-slot:links>
<!-- <a target="_blank">Gitee</a>
<a target="_blank">Github</a> -->
</template>
<template v-slot:copyright>
<a target="_blank">Hot Go!</a>
</template>
</global-footer>
</template>
<script>
import { GlobalFooter } from '@/components/ProLayout'
export default {
name: 'ProGlobalFooter',
components: {
GlobalFooter
}
}
</script>

View File

@@ -0,0 +1,83 @@
<template>
<a-dropdown v-if="name" placement="bottomRight">
<span class="ant-pro-account-avatar">
<a-avatar size="small" :src="avatar" class="antd-pro-global-header-index-avatar" />
<span>{{ name }}</span>
</span>
<template v-slot:overlay>
<a-menu class="ant-pro-drop-down menu" :selected-keys="[]">
<!-- <a-menu-item v-if="menu" key="center" @click="handleToCenter">
<a-icon type="user" />
个人中心
</a-menu-item> -->
<a-menu-item v-if="menu" key="settings" @click="handleToSettings">
<a-icon type="setting" />
个人中心
</a-menu-item>
<a-menu-divider v-if="menu" />
<a-menu-item key="logout" @click="handleLogout">
<a-icon type="logout" />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<span v-else>
<a-spin size="small" :style="{ marginLeft: 8, marginRight: 8 }" />
</span>
</template>
<script>
import { Modal } from 'ant-design-vue'
import { mapGetters } from 'vuex'
export default {
name: 'AvatarDropdown',
props: {
menu: {
type: Boolean,
default: true
}
},
computed: {
...mapGetters([
'avatar',
'name'
])
},
methods: {
handleToCenter () {
this.$router.push({ path: '/account/center' })
},
handleToSettings () {
this.$router.push({ path: '/account/settings' })
},
handleLogout (e) {
Modal.confirm({
title: '提示',
content: '确定注销并退出系统吗?',
onOk: () => {
// return new Promise((resolve, reject) => {
// setTimeout(Math.random() > 0.5 ? resolve : reject, 1500)
// }).catch(() => console.log('Oops errors!'))
return this.$store.dispatch('Logout').then(() => {
this.$router.push({ name: 'login' })
})
},
onCancel () {}
})
}
}
}
</script>
<style lang="less" scoped>
.ant-pro-drop-down {
/deep/ .action {
margin-right: 8px;
}
/deep/ .ant-dropdown-menu-item {
min-width: 160px;
}
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<ant-modal
:visible="open"
:modal-title="formTitle"
:adjust-size="true"
:isShowTitle="false"
:closeAble="true"
:footer="null"
modalWidth="600"
modalHeight="350"
@cancel="cancel"
>
<a-row slot="content">
<a-col :span="8">
<div class="copyright-icon"><a-icon type="key" /></div>
</a-col>
<a-col :span="16">
<div class="copyright-content">
<div class="copyright-text">
<h2>平台授权信息.</h2>
<h3>非常感谢您对我们产品的认可与支持</h3>
{{ versionContent[0] }}<br>
{{ versionContent[1] }}<br>
{{ versionContent[2] }}<br>
授权产品名称AiDex<br>
当前平台版本V1.2.1
</div>
<a-button type="primary" icon="close-circle" @click="cancel">
关闭页面
</a-button>
</div>
</a-col>
</a-row>
</ant-modal>
</template>
<script>
import AntModal from '@/components/pt/dialog/AntModal'
import { mapGetters } from 'vuex'
export default {
name: 'CreateForm',
components: {
AntModal
},
data () {
return {
loading: false,
formTitle: '',
open: false,
versionContent: []
}
},
filters: {
},
created () {
},
computed: {
...mapGetters([
'platformVersion'
])
},
watch: {
},
methods: {
cancel () {
this.open = false
this.$emit('close')
},
showVersion () {
this.open = true
this.formTitle = '授权信息'
if (this.platformVersion !== null && this.platformVersion !== '') {
const licenseInfo = JSON.parse(this.platformVersion)
const customName = licenseInfo.customName
const versionDes = licenseInfo.versionDes
const version = licenseInfo.version
let deadLine = licenseInfo.deadLine
if (version === '2') {
const beforeYear = deadLine.split('-')[0]
let myDate = new Date()
myDate = myDate.getFullYear()
if ((beforeYear - myDate) >= 10) {
deadLine = '无限制'
}
}
this.versionContent.push('授权对象:' + customName)
this.versionContent.push('版本信息:' + versionDes)
this.versionContent.push('到期时间:' + deadLine)
} else {
this.versionContent.push('授权对象:未知')
this.versionContent.push('版本信息:未知')
this.versionContent.push('到期时间:未知')
}
}
}
}
</script>
<style lang="less">
.copyright-content{
padding: 30px 10px 20px;
}
.copyright-icon{
text-align: center;
font-size: 80px;
padding: 20px 30px 10px;
color: #85c1fb;
}
.copyright-text{
margin-bottom: 20px;
h2{
font-size:22px;
color: #333333;
}
}
</style>

View File

@@ -0,0 +1,251 @@
<template>
<div :class="wrpCls" style="margin-right:16px">
<a-space size="middle">
<a-tooltip>
<a-dropdown>
<a class="ant-dropdown-link" style="color:#fff;">
切换工作台
<a-icon type="down"/>
</a>
<a-menu slot="overlay" class="setUl" style="left:-10px;top:12px; width:200px">
<a-menu-item
:key="item.id"
v-for="(item) in portalConfigs"
:style="{'background-color': defaultPortal.id === item.id ? '#f0f5ff' : '' }"
style="position: relative;">
<a-icon
v-if="defaultPortal.id === item.id"
style="left: 10px;"
type="check"
:style="{'color': defaultPortal.id === item.id ? '#2f54eb' : '#999999' }"/>
<a class="homeTit" target="_blank" @click="toIndex(item)">{{ item.name }}</a>
<a-icon style="right: 8px;" type="delete" target="_blank" @click="toDesignIndex(item,'delete')"/>
<a-icon style="right: 28px;" type="edit" target="_blank" @click="toDesignIndex(item)"/>
</a-menu-item>
<a-menu-divider/>
<a-menu-item class="menu-operation" key="3" v-if="portalConfigs.length <=15">
<a target="_blank" @click="toDesignIndex()">
<a-icon type="plus"/>
添加工作台</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</a-tooltip>
<a-tooltip v-if="userType === '1'">
<template slot="title">
控制台
</template>
<a-icon type="desktop" @click="toConsole" :style="{ fontSize: '18px'}"/>
</a-tooltip>
<a-tooltip @click="toNotice" style="cursor:pointer">
<template slot="title">
消息
</template>
<a-badge :count="msgCount">
<a-icon type="sound" :style="{ fontSize: '18px'}"/>
</a-badge>
</a-tooltip>
<a-tooltip>
<template slot="title">
换肤
</template>
<a-icon type="setting" @click="showColorSetting()" :style="{ fontSize: '18px'}"/>
</a-tooltip>
<a-tooltip>
<template slot="title">
{{ fullScreen ? '退出全屏' : '切为全屏' }}
</template>
<a-icon
:type="fullScreen ? 'fullscreen-exit' : 'fullscreen'"
@click="toggleFullScreen"
:style="{ fontSize: '18px'}"/>
</a-tooltip>
<avatar-dropdown :menu="showMenu" :current-user="currentUser" :class="prefixCls"/>
<!-- 暂只支持中文国际化可自行扩展 -->
<select-lang :class="prefixCls"/>
</a-space>
<platform-version
v-if="modalVisible"
ref="platformVersionModal"
@close="modalVisible = false"
/>
</div>
</template>
<script>
import AvatarDropdown from './AvatarDropdown'
import SelectLang from '@/components/SelectLang'
import PlatformVersion from './PlatformVersion'
import {
mapGetters
} from 'vuex'
export default {
name: 'RightContent',
components: {
AvatarDropdown,
SelectLang,
PlatformVersion
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-global-header-index-action'
},
isMobile: {
type: Boolean,
default: () => false
},
topMenu: {
type: Boolean,
required: true
},
theme: {
type: String,
required: true
}
},
data() {
return {
modalVisible: false,
showMenu: true,
showPortalDefined: false,
currentUser: {},
fullScreen: false,
msgCount: 0,
docUrl: 'https://docs.geekera.cn/AiDex-Antdv/',
githubUrl: 'https://github.com/fuzui/AiDex-Antdv'
}
},
computed: {
wrpCls() {
return {
'ant-pro-global-header-index-right': true,
[`ant-pro-global-header-index-${(this.isMobile || !this.topMenu) ? 'light' : this.theme}`]: true
}
},
...mapGetters([
'userType',
'portalConfigs',
'defaultPortal',
'sysNoticeList'
])
},
mounted() {
setTimeout(() => {
this.currentUser = {
name: 'RuoYi'
}
}, 1500)
this.msgCount = this.sysNoticeList.length
},
methods: {
showColorSetting() {
this.$emit('showSetting')
},
toConsole() {
this.$message.success(
'尚未实现',
3
)
},
toNotice() {
this.$router.push({
path: '/system/notice/NoticeReadIndex'
})
this.msgCount = 0
},
toIndex(item) {
this.$router.push({
name: 'index',
params: {
key: item.id
}
})
if (item.applicationRange === 'U') {
// 当选中小页时用户自定义时,修改选中小页为默认小页
this.defaultPortal.id = item.id
}
this.$emit('reloadTab', item)
},
toDesignIndex(item, type) {
this.$message.success(
'尚未实现',
3
)
},
// 全屏切换
toggleFullScreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen()
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
this.fullScreen = !this.fullScreen
},
versionInfo() {
this.modalVisible = true
this.$nextTick(() => (
this.$refs.platformVersionModal.showVersion()
))
}
}
}
</script>
<style lang="less" scoped="scoped">
.ant-pro-global-header {
.anticon {
vertical-align: middle;
}
}
.ant-modal-confirm-content {
p {
height: 15px;
}
}
.setUl {
.ant-dropdown-menu-item {
padding: 5px 32px;
font-size: 12px;
}
.ant-dropdown-menu-item > .anticon:first-child {
font-size: 12px;
}
.ant-dropdown-menu-item i {
position: absolute;
top: 10px;
font-size: 12px;
color: #969696;
}
.ant-dropdown-menu-item > a.homeTit {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #333;
}
.menu-operation {
text-align: center;
i {
position: relative;
top: 0px;
margin-right: 5px;
}
}
.menu-operation:hover {
i {
color: #1890ff
}
}
}
</style>

View File

@@ -0,0 +1,44 @@
'use strict'
Object.defineProperty(exports, '__esModule', {
value: true
})
exports['default'] = void 0
require('./index.less')
var _vueTypes = _interopRequireDefault(require('ant-design-vue/es/_util/vue-types'))
var _util = require('../../utils/util')
function _interopRequireDefault (obj) { return obj && obj.__esModule ? obj : { 'default': obj } }
function _defineProperty (obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true })
} else {
obj[key] = value
} return obj
}
var GridContent = {
name: 'GridContent',
functional: true,
props: {
children: _vueTypes['default'].any,
contentWidth: _vueTypes['default'].oneOf(['Fluid', 'Fixed']).def('Fluid')
},
render: function render (h, content) {
var _classNames
var contentWidth = content.props.contentWidth
var children = content.children
var propsContentWidth = (0, _util.layoutContentWidth)(contentWidth)
var classNames = (_classNames = {}, _defineProperty(_classNames, 'ant-pro-grid-content', true), _defineProperty(_classNames, 'wide', propsContentWidth), _classNames)
return h('div', {
'class': classNames
}, [children])
}
}
var _default = GridContent
exports['default'] = _default

View File

@@ -0,0 +1,14 @@
@import "~ant-design-vue/es/style/themes/default";
@grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content';
.@{grid-content-prefix-cls} {
width: 100%;
min-height: 100%;
transition: 0.3s;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}

View File

@@ -0,0 +1,187 @@
<template>
<div class="prefixCls">
<a-tabs v-model="currentTab" @change="handleTabChange">
<a-tab-pane v-for="v in icons" :key="v.key">
<span slot="tab" :style="{ fontSize: '10px' }">
{{ v.title }}
</span>
<ul v-if="v.key != 'custom'" style="height: calc(100vh - 196px) ;">
<li v-for="(icon, key) in iconList" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
<a-icon :type="icon" :component="allIcon[icon + 'Icon']" :style="{ fontSize: '24px' }" />
<span class="anticon-class">
<span class="ant-badge">
{{ icon }}
</span>
</span>
</li>
</ul>
<ul class="IconList" v-if="v.key == 'custom'" style="height: calc(100vh - 196px) ;">
<li v-for="(icon, key) in iconList" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon,'1')" >
<a-icon :component="allIcon[icon + 'Icon']" :type="icon"/>
<span class="anticon-class">
<span class="ant-badge">
{{ icon }}
</span>
</span>
</li>
</ul>
</a-tab-pane>
<a-input-search class="inputsearch" slot="tabBarExtraContent" placeholder="全局搜索图标" @search="onSearchAll" />
</a-tabs>
</div>
</template>
<script>
import icons from './icons'
import allCustomIcon from '@/core/icons'
export default {
name: 'IconSelect',
props: {
prefixCls: {
type: String,
default: 'ant-pro-icon-selector'
},
// eslint-disable-next-line
value: {
type: String
},
svgIcons: {
type: Array,
required: true
},
allIcon: {
type: Object,
required: true
}
},
data () {
return {
selectedIcon: this.value || '',
currentTab: 'custom',
icons: icons,
allCustomIcon,
iconList: [], // 页面真实展示图标集合,根据搜索条件改变
currentIconList: [] // 记录当前页面图标集合,不会根据搜索条件改变
}
},
watch: {
value (val) {
this.selectedIcon = val
this.autoSwitchTab()
}
},
created () {
if (this.value) {
this.autoSwitchTab()
}
const custom = [{
key: 'custom',
title: '自定义图标',
icons: this.svgIcons
}]
this.icons = custom.concat(this.icons)
this.getCurrentIconList()
},
methods: {
handleSelectedIcon (icon, type) {
this.selectedIcon = icon
if (allCustomIcon[icon + 'Icon']) {
// 自定义图标,这里不能根据页签区分是否为自定义图标,因为搜索为全局搜索
type = '1'
} else {
type = '2'
}
let copayValue = '<a-icon type="' + icon + '" />'
if (type === '1') {
// 自定义图标
copayValue = '<a-icon type="" :component="allIcon.' + icon + 'Icon"/>'
}
var domType = document.createElement('input')
domType.value = copayValue
domType.id = 'creatDom'
document.body.appendChild(domType)
domType.select() // 选择对象
document.execCommand('Copy') // 执行浏览器复制命令
const creatDom = document.getElementById('creatDom')
creatDom.parentNode.removeChild(creatDom)
this.$message.success(
copayValue + ' 复制成功 🎉🎉🎉',
3
)
},
handleTabChange (activeKey) {
this.currentTab = activeKey
this.getCurrentIconList()
},
autoSwitchTab () {
const icons = this.icons
icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
},
getCurrentIconList () {
this.icons.forEach((icon, index) => {
if (icon.key === this.currentTab) {
this.iconList = icon.icons
this.currentIconList = icon.icons
}
})
},
onSearchAll (text) {
if (text === '') {
this.iconList = this.currentIconList
return
}
this.iconList = []
this.icons.forEach((icon, index) => {
icon.icons.forEach((icon, index) => {
if (icon.toUpperCase().indexOf(text.toUpperCase()) >= 0) {
this.iconList.push(icon)
}
})
})
}
}
}
</script>
<style lang="less" scoped>
@import "../index.less";
.prefixCls{
background: #ffffff;
.inputsearch{
width: 200px;
margin-right: 15px;
}
}
ul{
list-style: none;
padding: 0;
overflow-y: scroll;
li{
display: inline-block;
width: 100px;
height:105px;
padding: 0;
margin: 15px 13px;
text-align: center;
border-radius: @border-radius-base;
&:hover, &.active{
// box-shadow: 0px 0px 5px 2px @primary-color;
cursor: pointer;
color: @white;
background-color: @primary-color;
}
i.anticon {
display: inline-block;
margin-top: 25px;
font-size: 24px;
}
.anticon-class {
display: block;
text-align: center;
transform: scale(.83);
margin-top: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div :class="prefixCls">
<a-tabs v-model="currentTab" @change="handleTabChange">
<a-tab-pane v-for="v in icons" :key="v.key">
<span slot="tab" :style="{ fontSize: '10px' }">
{{ v.title }}
</span>
<ul v-if="v.key != 'custom'">
<li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
<a-icon :type="icon" :style="{ fontSize: '24px' }" />
</li>
</ul>
<ul v-if="v.key == 'custom'">
<li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
<a-icon :component="allIcon[icon + 'Icon']" :style="{ fontSize: '24px' }"/>
</li>
</ul>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import icons from './icons'
export default {
name: 'IconSelect',
props: {
prefixCls: {
type: String,
default: 'ant-pro-icon-selector'
},
// eslint-disable-next-line
value: {
type: String
},
svgIcons: {
type: Array,
required: true
},
allIcon: {
type: Object,
required: true
}
},
data () {
return {
selectedIcon: this.value || '',
currentTab: 'custom',
icons: icons
}
},
watch: {
value (val) {
this.selectedIcon = val
this.autoSwitchTab()
}
},
created () {
if (this.value) {
this.autoSwitchTab()
}
const custom = [{
key: 'custom',
title: '自定义图标',
icons: this.svgIcons
}]
this.icons = custom.concat(this.icons)
},
methods: {
handleSelectedIcon (icon) {
this.selectedIcon = icon
this.$emit('change', icon)
},
handleTabChange (activeKey) {
this.currentTab = activeKey
},
autoSwitchTab () {
const icons = this.icons
icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
}
}
}
</script>
<style lang="less" scoped>
@import "../index.less";
ul{
list-style: none;
padding: 0;
overflow-y: scroll;
height: 250px;
li{
display: inline-block;
padding: @padding-sm;
margin: 3px 0;
border-radius: @border-radius-base;
&:hover, &.active{
// box-shadow: 0px 0px 5px 2px @primary-color;
cursor: pointer;
color: @white;
background-color: @primary-color;
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
IconSelector
====
> 图标选择组件,常用于为某一个数据设定一个图标时使用
> eg: 设定菜单列表时,为每个菜单设定一个图标
该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装
### 使用方式
```vue
<template>
<div>
<icon-selector @change="handleIconChange"/>
</div>
</template>
<script>
import IconSelector from '@/components/IconSelector'
export default {
name: 'YourView',
components: {
IconSelector
},
data () {
return {
}
},
methods: {
handleIconChange (icon) {
console.log('change Icon', icon)
}
}
}
</script>
```
### 事件
| 名称 | 说明 | 类型 | 默认值 |
| ------ | -------------------------- | ------ | ------ |
| change | 当改变了 `icon` 选中项触发 | String | - |

View File

@@ -0,0 +1,36 @@
/**
* 增加新的图标时,请遵循以下数据结构
* Adding new icon please follow the data structure below
*/
export default [
{
key: 'directional',
title: '方向性图标',
icons: ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt', 'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle', 'left-circle', 'right-circle', 'double-right', 'double-left', 'vertical-left', 'vertical-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap', 'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'up-square', 'down-square', 'left-square', 'right-square', 'login', 'logout', 'menu-fold', 'menu-unfold', 'border-bottom', 'border-horizontal', 'border-inner', 'border-left', 'border-right', 'border-top', 'border-verticle', 'pic-center', 'pic-left', 'pic-right', 'radius-bottomleft', 'radius-bottomright', 'radius-upleft', 'fullscreen', 'fullscreen-exit']
},
{
key: 'suggested',
title: '提示建议性图标',
icons: ['question', 'question-circle', 'plus', 'plus-circle', 'pause', 'pause-circle', 'minus', 'minus-circle', 'plus-square', 'minus-square', 'info', 'info-circle', 'exclamation', 'exclamation-circle', 'close', 'close-circle', 'close-square', 'check', 'check-circle', 'check-square', 'clock-circle', 'warning', 'issues-close', 'stop']
},
{
key: 'editor',
title: '编辑类图标',
icons: ['edit', 'form', 'copy', 'scissor', 'delete', 'snippets', 'diff', 'highlight', 'align-center', 'align-left', 'align-right', 'bg-colors', 'bold', 'italic', 'underline', 'strikethrough', 'redo', 'undo', 'zoom-in', 'zoom-out', 'font-colors', 'font-size', 'line-height', 'colum-height', 'dash', 'small-dash', 'sort-ascending', 'sort-descending', 'drag', 'ordered-list', 'radius-setting']
},
{
key: 'data',
title: '数据类图标',
icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
},
{
key: 'brand_logo',
title: '网站通用图标',
icons: ['lock', 'unlock', 'bars', 'book', 'calendar', 'cloud', 'cloud-download', 'code', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-word', 'file-excel', 'file-jpg', 'file-ppt', 'file-markdown', 'file-add', 'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'meh', 'smile', 'inbox', 'laptop', 'appstore', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture', 'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tags', 'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload', 'star', 'heart', 'environment', 'eye', 'camera', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export', 'customer-service', 'qrcode', 'scan', 'like', 'dislike', 'message', 'pay-circle', 'calculator', 'pushpin', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect', 'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool', 'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman', 'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet', 'bank', 'trophy', 'contacts', 'global', 'shake', 'api', 'fork', 'dashboard', 'table', 'profile', 'alert', 'audit', 'branches', 'build', 'border', 'crown', 'experiment', 'fire', 'money-collect', 'property-safety', 'read', 'reconciliation', 'rest', 'security-scan', 'insurance', 'interation', 'safety-certificate', 'project', 'thunderbolt', 'block', 'cluster', 'deployment-unit', 'dollar', 'euro', 'pound', 'file-done', 'file-exclamation', 'file-protect', 'file-search', 'file-sync', 'gateway', 'gold', 'robot', 'shopping']
},
{
key: 'application',
title: '品牌和标识',
icons: ['android', 'apple', 'windows', 'ie', 'chrome', 'github', 'aliwangwang', 'dingtalk', 'weibo-square', 'weibo-circle', 'taobao-circle', 'html5', 'weibo', 'twitter', 'wechat', 'youtube', 'alipay-circle', 'taobao', 'skype', 'qq', 'medium-workmark', 'gitlab', 'medium', 'linkedin', 'google-plus', 'dropbox', 'facebook', 'codepen', 'code-sandbox', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
}
]

View File

@@ -0,0 +1,2 @@
import IconSelector from './IconSelector'
export default IconSelector

View File

@@ -0,0 +1,175 @@
<script>
import events from './events'
import { i18nRender } from '@/locales'
export default {
name: 'MultiTab',
data () {
return {
fullPathList: [],
pages: [],
activeKey: '',
newTabIndex: 0
}
},
created () {
// bind event
events.$on('open', val => {
if (!val) {
throw new Error(`multi-tab: open tab ${val} err`)
}
this.activeKey = val
}).$on('close', val => {
if (!val) {
this.closeThat(this.activeKey)
return
}
this.closeThat(val)
}).$on('rename', ({ key, name }) => {
console.log('rename', key, name)
try {
const item = this.pages.find(item => item.path === key)
item.meta.customTitle = name
this.$forceUpdate()
} catch (e) {
}
})
this.pages.push(this.$route)
this.fullPathList.push(this.$route.fullPath)
this.selectedLastPath()
},
methods: {
reloadCurrent (e) {
this.$emit('reload')
},
onEdit (targetKey, action) {
this[action](targetKey)
},
remove (targetKey) {
this.pages = this.pages.filter(page => page.fullPath !== targetKey)
this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
// 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
if (!this.fullPathList.includes(this.activeKey)) {
this.selectedLastPath()
}
},
selectedLastPath () {
this.activeKey = this.fullPathList[this.fullPathList.length - 1]
},
// content menu
closeThat (e) {
// 判断是否为最后一个标签页,如果是最后一个,则无法被关闭
if (this.fullPathList.length > 1) {
this.remove(e)
} else {
this.$message.info('这是最后一个标签了, 无法被关闭')
}
},
closeLeft (e) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex > 0) {
this.fullPathList.forEach((item, index) => {
if (index < currentIndex) {
this.remove(item)
}
})
} else {
this.$message.info('左侧没有标签')
}
},
closeRight (e, type) {
const currentIndex = this.fullPathList.indexOf(e)
if (currentIndex < (this.fullPathList.length - 1)) {
this.fullPathList.forEach((item, index) => {
if (index > currentIndex) {
this.remove(item)
}
})
} else {
if (type === '') {
this.$message.info('右侧没有标签')
}
}
},
closeAll (e) {
const currentIndex = this.fullPathList.indexOf(e)
this.fullPathList.forEach((item, index) => {
if (index !== currentIndex) {
this.remove(item)
}
})
},
closeOther (e) {
this.closeRight(e, 'other')
this.closeLeft(e)
},
closeMenuClick (key, route) {
this[key](route)
},
renderTabPaneMenu (e) {
return (
<a-menu {...{ on: { click: ({ key, item, domEvent }) => { this.closeMenuClick(key, e) } } }}>
<a-menu-item key="reloadCurrent" disabled={this.activeKey !== e}><a-icon type="reload" />刷新</a-menu-item>
<a-menu-item key="closeThat"><a-icon type="close" />关闭</a-menu-item>
<a-menu-divider/>
<a-menu-item key="closeLeft"><a-icon type="vertical-right" />关闭左侧</a-menu-item>
<a-menu-item key="closeRight"><a-icon type="vertical-left" />关闭右侧</a-menu-item>
<a-menu-divider/>
<a-menu-item key="closeOther"> <a-icon type="column-width" />关闭其他</a-menu-item>
<a-menu-item key="closeAll"><a-icon type="minus" />关闭全部</a-menu-item>
</a-menu>
)
},
// render
renderTabPane (title, keyPath) {
const menu = this.renderTabPaneMenu(keyPath)
return (
<a-dropdown overlay={menu} trigger={['contextmenu']}>
<span style={{ userSelect: 'none' }}>{ i18nRender(title) } </span>
</a-dropdown>
)
}
},
watch: {
'$route': function (newVal) {
this.activeKey = newVal.fullPath
if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
this.fullPathList.push(newVal.fullPath)
this.pages.push(newVal)
}
},
activeKey: function (newPathKey) {
this.$router.push({ path: newPathKey })
}
},
render () {
const { onEdit, $data: { pages } } = this
const panes = pages.map(page => {
return (
<a-tab-pane
style={{ height: 0 }}
tab={this.renderTabPane(page.meta.customTitle || page.meta.title, page.fullPath)}
key={page.fullPath} closable={pages.length > 1}
>
</a-tab-pane>)
})
return (
<div class="ant-pro-multi-tab">
<div class="ant-pro-multi-tab-wrapper">
<a-tabs
hideAdd
type={'editable-card'}
v-model={this.activeKey}
tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
{...{ on: { edit: onEdit } }}>
{panes}
</a-tabs>
</div>
</div>
)
}
}
</script>

View File

@@ -0,0 +1,2 @@
import Vue from 'vue'
export default new Vue()

View File

@@ -0,0 +1,40 @@
import events from './events'
import MultiTab from './MultiTab'
import './index.less'
const api = {
/**
* open new tab on route fullPath
* @param config
*/
open: function (config) {
events.$emit('open', config)
},
rename: function (key, name) {
events.$emit('rename', { key: key, name: name })
},
/**
* close current page
*/
closeCurrentPage: function () {
this.close()
},
/**
* close route fullPath tab
* @param config
*/
close: function (config) {
events.$emit('close', config)
}
}
MultiTab.install = function (Vue) {
if (Vue.prototype.$multiTab) {
return
}
api.instance = events
Vue.prototype.$multiTab = api
Vue.component('multi-tab', MultiTab)
}
export default MultiTab

View File

@@ -0,0 +1,31 @@
@import '../index';
@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
/*
.topmenu .@{multi-tab-prefix-cls} {
max-width: 1200px;
margin: -23px auto 24px auto;
}
*/
.@{multi-tab-prefix-cls} {
margin: -23px -24px 24px -24px;
background: #fff;
}
.topmenu .@{multi-tab-wrapper-prefix-cls} {
max-width: 1200px;
margin: 0 auto;
}
.topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
max-width: 100%;
margin: 0 auto;
}
.@{multi-tab-wrapper-prefix-cls} > .ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active {
border-color: #fff;
border-top-right-radius: 0px;
background: #fff;
box-shadow: 2px 0 4px rgb(0 21 41 / 5%);
}

View File

@@ -0,0 +1,76 @@
@import url('../index.less');
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: @primary-color;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: @primary-color;
border-left-color: @primary-color;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@@ -0,0 +1,90 @@
<template>
<a-popover
v-model="visible"
trigger="click"
placement="bottomRight"
overlayClassName="header-notice-wrapper"
:getPopupContainer="() => $refs.noticeRef.parentElement"
:autoAdjustOverflow="true"
:arrowPointAtCenter="true"
:overlayStyle="{ width: '300px', top: '50px' }"
>
<template slot="content">
<a-spin :spinning="loading">
<a-tabs>
<a-tab-pane tab="通知" key="1">
<a-list>
<a-list-item>
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane tab="消息" key="2">
123
</a-tab-pane>
<a-tab-pane tab="待办" key="3">
123
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<span @click="fetchNotice" class="header-notice" ref="noticeRef" style="padding: 0 18px">
<a-badge count="12">
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
</a-badge>
</span>
</a-popover>
</template>
<script>
export default {
name: 'HeaderNotice',
data () {
return {
loading: false,
visible: false
}
},
methods: {
fetchNotice () {
if (!this.visible) {
this.loading = true
setTimeout(() => {
this.loading = false
}, 2000)
} else {
this.loading = false
}
this.visible = !this.visible
}
}
}
</script>
<style lang="css">
.header-notice-wrapper {
top: 50px !important;
}
</style>
<style lang="less" scoped>
.header-notice{
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
}
</style>

View File

@@ -0,0 +1,2 @@
import NoticeIcon from './NoticeIcon'
export default NoticeIcon

View File

@@ -0,0 +1,54 @@
<template>
<div :class="[prefixCls]">
<slot name="subtitle">
<div :class="[`${prefixCls}-subtitle`]">{{ typeof subTitle === 'string' ? subTitle : subTitle() }}</div>
</slot>
<div class="number-info-value">
<span>{{ total }}</span>
<span class="sub-total">
{{ subTotal }}
<icon :type="`caret-${status}`" />
</span>
</div>
</div>
</template>
<script>
import Icon from 'ant-design-vue/es/icon'
export default {
name: 'NumberInfo',
props: {
prefixCls: {
type: String,
default: 'ant-pro-number-info'
},
total: {
type: Number,
required: true
},
subTotal: {
type: Number,
required: true
},
subTitle: {
type: [String, Function],
default: ''
},
status: {
type: String,
default: 'up'
}
},
components: {
Icon
},
data () {
return {}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

View File

@@ -0,0 +1,3 @@
import NumberInfo from './NumberInfo'
export default NumberInfo

View File

@@ -0,0 +1,55 @@
@import "../index";
@numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info";
.@{numberInfo-prefix-cls} {
.ant-pro-number-info-subtitle {
color: @text-color-secondary;
font-size: @font-size-base;
height: 22px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.number-info-value {
margin-top: 4px;
font-size: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
& > span {
color: @heading-color;
display: inline-block;
line-height: 32px;
height: 32px;
font-size: 24px;
margin-right: 32px;
}
.sub-total {
color: @text-color-secondary;
font-size: @font-size-lg;
vertical-align: top;
margin-right: 0;
i {
font-size: 12px;
transform: scale(0.82);
margin-left: 4px;
}
:global {
.anticon-caret-up {
color: @red-6;
}
.anticon-caret-down {
color: @green-6;
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
# NumberInfo 数据文本
常用在数据卡片中,用于突出展示某个业务数据。
引用方式:
```javascript
import NumberInfo from '@/components/NumberInfo'
export default {
components: {
NumberInfo
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<number-info
:sub-title="() => { return 'Visits this week' }"
:total="12321"
status="up"
:sub-total="17.1"></number-info>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 标题 | ReactNode\|string | -
subTitle | 子标题 | ReactNode\|string | -
total | 总量 | ReactNode\|string | -
subTotal | 子总量 | ReactNode\|string | -
status | 增加状态 | 'up \| down' | -
theme | 状态样式 | string | 'light'
gap | 设置数字和描述之间的间距(像素)| number | 8

View File

@@ -0,0 +1,114 @@
<script>
const carbonUrl = '//cdn.carbonads.com/carbon.js?serve=CK7DL2JW&placement=antdvcom'
export default {
props: {
isMobile: Boolean
},
watch: {
$route (e, t) {
const adId = '#carbonads'
// if(isGitee) {
// adId = '#cf';
// }
if (e.path !== t.path && this.$el.querySelector(adId)) {
this.$el.innerHTML = ''
this.load()
}
this.adInterval && clearInterval(this.adInterval)
this.adInterval = setInterval(() => {
if (!this.$el.querySelector(adId)) {
this.$el.innerHTML = ''
this.load()
}
}, 20000)
}
},
mounted () {
this.load()
},
methods: {
load () {
// if(isGitee) {
// axios.get('https://api.codefund.app/properties/162/funder.html?template=horizontal')
// .then(function (response) {
// document.getElementById("codefund-ads").innerHTML = response.data;
// });
// } else
if (carbonUrl) {
const e = document.createElement('script')
e.id = '_carbonads_js'
e.src = carbonUrl
this.$el.appendChild(e)
}
}
},
render () {
return <div id="carbon-ads" class={this.isMobile ? 'carbon-mobile' : ''} />
}
}
</script>
<style lang="less">
#carbon-ads {
width: 256px;
/* float: right; */
margin-top: 75px;
position: fixed;
left: 0;
bottom: 0;
padding: 0;
overflow: hidden;
z-index: 100;
background-color: #fff;
/* border-radius: 3px; */
font-size: 13px;
background: #f5f5f5;
font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
}
#carbonads {
overflow: hidden;
}
#carbon-ads a {
display: inline-block;
color: #7f8c8d;
font-weight: normal;
}
#carbon-ads span {
color: #7f8c8d;
}
#carbon-ads img {
float: left;
padding-right: 10px;
}
#carbon-ads .carbon-img,
#carbon-ads .carbon-text {
display: block;
font-weight: normal;
color: #34495e;
}
#carbon-ads .carbon-text {
padding-top: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
}
#carbon-ads .carbon-poweredby {
color: #aaa;
font-weight: normal;
line-height: 1.2;
margin-top: 6px;
}
#carbon-ads.carbon-mobile {
width: 100%;
position: relative;
right: 0;
bottom: 0;
padding: 0;
margin-bottom: 15px;
margin-top: 5px;
.carbon-img {
float: left;
margin-right: 10px;
}
}
</style>

View File

@@ -0,0 +1,106 @@
import { Spin } from 'ant-design-vue'
export const PageLoading = {
name: 'PageLoading',
props: {
tip: {
type: String,
default: 'Loading..'
},
size: {
type: String,
default: 'large'
}
},
render () {
const style = {
textAlign: 'center',
background: 'rgba(0,0,0,0.6)',
position: 'fixed',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 1100
}
const spinStyle = {
position: 'absolute',
left: '50%',
top: '40%',
transform: 'translate(-50%, -50%)'
}
return (<div style={style}>
<Spin size={this.size} style={spinStyle} tip={this.tip} />
</div>)
}
}
const version = '0.0.1'
const loading = {}
loading.newInstance = (Vue, options) => {
let loadingElement = document.querySelector('body>div[type=loading]')
if (!loadingElement) {
loadingElement = document.createElement('div')
loadingElement.setAttribute('type', 'loading')
loadingElement.setAttribute('class', 'ant-loading-wrapper')
document.body.appendChild(loadingElement)
}
const cdProps = Object.assign({ visible: false, size: 'large', tip: 'Loading...' }, options)
const instance = new Vue({
data () {
return {
...cdProps
}
},
render () {
const { tip } = this
const props = {}
this.tip && (props.tip = tip)
if (this.visible) {
return <PageLoading { ...{ props } } />
}
return null
}
}).$mount(loadingElement)
function update (config) {
const { visible, size, tip } = { ...cdProps, ...config }
instance.$set(instance, 'visible', visible)
if (tip) {
instance.$set(instance, 'tip', tip)
}
if (size) {
instance.$set(instance, 'size', size)
}
}
return {
instance,
update
}
}
const api = {
show: function (options) {
this.instance.update({ ...options, visible: true })
},
hide: function () {
this.instance.update({ visible: false })
}
}
const install = function (Vue, options) {
if (Vue.prototype.$loading) {
return
}
api.instance = loading.newInstance(Vue, options)
Vue.prototype.$loading = api
}
export default {
version,
install
}

View File

@@ -0,0 +1,187 @@
import './BasicLayout.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import { ContainerQuery } from 'vue-container-query'
import { SiderMenuWrapper } from './components'
import { getComponentFromProp, isFun } from './utils/util'
import { SiderMenuProps } from './components/SiderMenu'
import HeaderView, { HeaderViewProps } from './Header'
import WrapContent from './WrapContent'
import ConfigProvider from './components/ConfigProvider'
import PageHeaderWrapper from './components/PageHeaderWrapper'
export const BasicLayoutProps = {
...SiderMenuProps,
...HeaderViewProps,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
// contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'),
locale: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('en-US'),
breadcrumbRender: PropTypes.func,
disableMobile: PropTypes.bool.def(false),
mediaQuery: PropTypes.object.def({}),
handleMediaQuery: PropTypes.func,
footerRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(undefined)
}
const MediaQueryEnum = {
'screen-xs': {
maxWidth: 575
},
'screen-sm': {
minWidth: 576,
maxWidth: 767
},
'screen-md': {
minWidth: 768,
maxWidth: 991
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599
},
'screen-xxl': {
minWidth: 1600
}
}
const getPaddingLeft = (
hasLeftPadding,
collapsed = undefined,
siderWidth
) => {
if (hasLeftPadding) {
return collapsed ? 60 : siderWidth
}
return 0
}
const getLeft = (collapsed = undefined) => {
return collapsed ? 60 : 210
}
const headerRender = (h, props) => {
if (props.headerRender === false) {
return null
}
return <HeaderView { ...{ props } } />
}
const defaultI18nRender = (key) => key
const BasicLayout = {
name: 'BasicLayout',
functional: true,
props: BasicLayoutProps,
render (h, content) {
const { props, children } = content
const {
layout,
// theme,
isMobile,
collapsed,
mediaQuery,
handleMediaQuery,
handleCollapse,
siderWidth,
fixSiderbar,
i18nRender = defaultI18nRender,
multiTab
} = props
const footerRender = getComponentFromProp(content, 'footerRender')
const rightContentRender = getComponentFromProp(content, 'rightContentRender')
const collapsedButtonRender = getComponentFromProp(content, 'collapsedButtonRender')
const menuHeaderRender = getComponentFromProp(content, 'menuHeaderRender')
const breadcrumbRender = getComponentFromProp(content, 'breadcrumbRender')
const headerContentRender = getComponentFromProp(content, 'headerContentRender')
const menuRender = getComponentFromProp(content, 'menuRender')
const headerBottomRender = getComponentFromProp(content, 'headerBottomRender')
const isTopMenu = layout === 'topmenu'
const hasSiderMenu = !isTopMenu
// If it is a fix menu, calculate padding
// don't need padding in phone mode
const hasLeftPadding = fixSiderbar && !isTopMenu && !isMobile
const cdProps = {
...props,
hasSiderMenu,
footerRender,
menuHeaderRender,
rightContentRender,
collapsedButtonRender,
breadcrumbRender,
headerContentRender,
menuRender
}
return (
<ConfigProvider i18nRender={i18nRender} contentWidth={props.contentWidth} breadcrumbRender={breadcrumbRender}>
<ContainerQuery query={MediaQueryEnum} onChange={handleMediaQuery}>
<Layout class={{
'ant-pro-basicLayout': true,
'ant-pro-topmenu': isTopMenu,
...mediaQuery
}}>
<SiderMenuWrapper
{ ...{ props: cdProps } }
collapsed={collapsed}
onCollapse={handleCollapse}
/>
<Layout class={[layout]} style={{
paddingLeft: hasSiderMenu
? `${getPaddingLeft(!!hasLeftPadding, collapsed, siderWidth)}px`
: undefined,
minHeight: '100vh'
}}>
{headerRender(h, {
...cdProps,
mode: 'horizontal'
})}
<div style={{
left: hasSiderMenu ? `${getLeft(collapsed)}px` : '0px',
height: '40px',
position: 'fixed',
top: '50px',
right: '0px',
zIndex: 8
}}
>
{headerBottomRender}
</div>
<WrapContent class="ant-pro-basicLayout-content"
contentWidth={props.contentWidth}
style={{
paddingTop: multiTab ? '40px' : '0',
margin: '0px',
overflow: 'hidden'
}}>
<div style="" >
<div >
{children}
</div>
</div>
</WrapContent>
{ footerRender !== false && (
<Layout.Footer>
{ isFun(footerRender) && footerRender(h) || footerRender }
</Layout.Footer>
) || null
}
</Layout>
</Layout>
</ContainerQuery>
</ConfigProvider>
)
}
}
BasicLayout.install = function (Vue) {
Vue.component(PageHeaderWrapper.name, PageHeaderWrapper)
Vue.component('PageContainer', PageHeaderWrapper)
Vue.component('ProLayout', BasicLayout)
}
export default BasicLayout

View File

@@ -0,0 +1,101 @@
@import "~ant-design-vue/es/style/themes/default";
@basicLayout-prefix-cls: ~'@{ant-prefix}-pro-basicLayout';
@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu';
@nav-header-height: @layout-header-height;
.@{basicLayout-prefix-cls} {
&:not('.ant-pro-basicLayout-mobile') {
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.06);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.12);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
}
}
// BFC
display: flex;
flex-direction: column;
width: 100%;
min-height: 100%;
.ant-layout-header {
&:not(.ant-pro-top-menu) {
background: @component-background;
}
&.ant-pro-fixed-header {
position: fixed;
top: 0;
}
}
&-content {
position: relative;
margin: 24px;
transition: all 0.2s;
.@{ant-prefix}-pro-page-header-wrap {
margin: -24px -24px 0;
}
&-disable-margin {
margin: 0;
> div > .@{ant-prefix}-pro-page-header-wrap {
margin: 0;
}
}
> .ant-layout {
max-height: 100%;
}
}
// append hook styles
.ant-layout-sider-children {
height: 100%;
}
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
&-content {
position: relative;
margin: 24px;
transition: all 0.2s;
.@{ant-prefix}-pro-page-header-wrap {
margin: -24px -24px 0;
}
&-disable-margin {
margin: 0;
> div > .@{ant-prefix}-pro-page-header-wrap {
margin: 0;
}
}
> .ant-layout {
max-height: 100%;
}
}
.color-picker {
margin: 10px 0;
}
}

View File

@@ -0,0 +1,9 @@
const BlockLayout = {
name: 'BlockLayout',
functional: true,
render (createElement, content) {
return content.children
}
}
export default BlockLayout

View File

@@ -0,0 +1,109 @@
import './Header.less'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import BaseMenu from './components/RouteMenu/BaseMenu'
import { defaultRenderLogoAntTitle, SiderMenuProps } from './components/SiderMenu/SiderMenu'
import GlobalHeader, { GlobalHeaderProps } from './components/GlobalHeader'
import { VueFragment } from './components'
import { isFun } from './utils/util'
const { Header } = Layout
export const HeaderViewProps = {
...GlobalHeaderProps,
...SiderMenuProps,
isMobile: PropTypes.bool.def(false),
collapsed: PropTypes.bool,
logo: PropTypes.any,
hasSiderMenu: PropTypes.bool,
autoHideHeader: PropTypes.bool,
menuRender: PropTypes.any,
headerRender: PropTypes.any,
rightContentRender: PropTypes.any,
visible: PropTypes.bool.def(true)
}
const renderContent = (h, props) => {
const isTop = props.layout === 'topmenu'
const maxWidth = 1200 - 280 - 120
const contentWidth = props.contentWidth === 'Fixed'
const baseCls = 'ant-pro-top-nav-header'
const { logo, title, theme, isMobile, headerRender, rightContentRender, menuHeaderRender } = props
const rightContentProps = { theme, isTop, isMobile }
let defaultDom = <GlobalHeader {...{ props: props }} />
if (isTop && !isMobile) {
defaultDom = (
<div class={[baseCls, theme]}>
<div class={[`${baseCls}-main`, contentWidth ? 'wide' : '']}>
{menuHeaderRender && (
<div class={`${baseCls}-left`}>
<div class={`${baseCls}-logo`} key="logo" id="logo">
{defaultRenderLogoAntTitle(h, { logo, title, menuHeaderRender })}
</div>
</div>
)}
<div class={`${baseCls}-menu`} style={{ maxWidth: `${maxWidth}px`, flex: 1 }}>
<BaseMenu {...{ props: props }} />
</div>
{isFun(rightContentRender) && rightContentRender(h, rightContentProps) || rightContentRender}
</div>
</div>
)
}
if (headerRender) {
return headerRender(h, props)
}
return defaultDom
}
const HeaderView = {
name: 'HeaderView',
props: HeaderViewProps,
render (h) {
const {
visible,
isMobile,
layout,
collapsed,
siderWidth,
fixedHeader,
hasSiderMenu
} = this.$props
const props = this.$props
const isTop = layout === 'topmenu'
const needSettingWidth = fixedHeader && hasSiderMenu && !isTop && !isMobile
const className = {
'ant-pro-fixed-header': fixedHeader,
'ant-pro-top-menu': isTop
}
// 没有 <></> 暂时代替写法
return (
visible ? (
<VueFragment>
{ fixedHeader && <Header />}
<Header
style={{
padding: 0,
width: needSettingWidth
? `calc(100% - ${collapsed ? 60 : siderWidth}px)`
: '100%',
zIndex: 9,
right: fixedHeader ? 0 : undefined
}}
class={className}
>
{renderContent(h, props)}
</Header>
</VueFragment>
) : null
)
}
}
export default HeaderView

View File

@@ -0,0 +1,91 @@
@import "~ant-design-vue/es/style/themes/default";
@top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header';
@pro-layout-fixed-header-prefix-cls: ~'@{ant-prefix}-pro-fixed-header';
.@{top-nav-header-prefix-cls} {
position: relative;
width: 100%;
height: 50px;
box-shadow: @box-shadow-base;
transition: background 0.3s, width 0.2s;
.ant-menu-submenu.ant-menu-submenu-horizontal {
height: 100%;
line-height: @layout-header-height;
.ant-menu-submenu-title {
height: 100%;
}
}
&.light {
background-color: @component-background;
h1 {
color: #002140;
}
}
&-main {
display: flex;
height: @layout-header-height;
padding-left: 24px;
&.wide {
max-width: 1200px;
margin: auto;
padding-left: 0;
}
.left {
display: flex;
flex: 1;
}
.right {
width: 324px;
}
}
&-logo {
position: relative;
width: 165px;
height: @layout-header-height;
overflow: hidden;
line-height: @layout-header-height;
transition: all 0.3s;
img, svg {
display: inline-block;
height: 32px;
width: 32px;
vertical-align: middle;
}
h1 {
display: inline-block;
margin: 0 0 0 12px;
color: @btn-primary-color;
font-weight: 400;
font-size: 16px;
vertical-align: top;
}
}
&-menu {
.ant-menu.ant-menu-horizontal {
height: @layout-header-height;
line-height: @layout-header-height;
border: none;
}
}
}
.@{pro-layout-fixed-header-prefix-cls} {
z-index: 9;
width: 100%;
transition: width 0.2s;
}
.drop-down {
&.menu {
.anticon {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 160px;
}
}
}

View File

@@ -0,0 +1,14 @@
import { PageHeaderWrapper } from './components'
const PageView = {
name: 'PageView',
render () {
return (
<PageHeaderWrapper>
<router-view />
</PageHeaderWrapper>
)
}
}
export default PageView

View File

@@ -0,0 +1,44 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import ConfigProvider from 'ant-design-vue/es/config-provider'
import GridContent from './components/GridContent'
const { Content } = Layout
const WrapContentProps = {
isChildrenLayout: PropTypes.bool,
location: PropTypes.any,
contentHeight: PropTypes.number,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid')
}
const WrapContent = {
name: 'WrapContent',
props: WrapContentProps,
render (h) {
const {
isChildrenLayout,
contentWidth
} = this.$props
return (
<Content>
<ConfigProvider
getPopupContainer={(el, dialogContext) => {
if (isChildrenLayout) {
return el.parentNode()
}
return document.body
}}
>
<div class="ant-pro-basicLayout-children-content-wrap" >
<GridContent contentWidth={contentWidth}>{this.$slots.default}</GridContent>
</div>
</ConfigProvider>
</Content>
)
}
}
export default WrapContent

View File

@@ -0,0 +1,27 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
const ProConfigProviderProps = {
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
breadcrumbRender: PropTypes.func
}
const ConfigProvider = {
name: 'ProConfigProvider',
props: ProConfigProviderProps,
provide () {
const _self = this
return {
locale: _self.$props.i18nRender,
contentWidth: _self.$props.contentWidth,
breadcrumbRender: _self.$props.breadcrumbRender
}
},
render () {
const { $scopedSlots } = this
const children = this.children || $scopedSlots.default
return children()
}
}
export default ConfigProvider

View File

@@ -0,0 +1,17 @@
/* eslint-disable */
class SideEffect {
constructor ({ propsToState, handleStateChange }) {
if (typeof propsToState !== 'function') {
throw new Error('Expected propsToState to be a function.')
}
if (typeof handleStateChange !== 'function') {
throw new Error('Expected handleStateChange to be a function.')
}
this.options = {
propsToState,
handleStateChange
}
}
}
export default SideEffect

View File

@@ -0,0 +1,89 @@
// import SideEffect from './SideEffect'
import { setDocumentTitle } from './util'
// const sideEffect = new SideEffect({
// propsToState (propsList) {
// var innermostProps = propsList[propsList.length - 1]
// if (innermostProps) {
// return innermostProps.title
// }
// },
// handleStateChange (title, prefix) {
// console.log('title', title, prefix)
// const nextTitle = `${(title || '')} - ${prefix}`
// if (nextTitle !== document.title) {
// setDocumentTitle(nextTitle)
// }
// }
// })
const handleStateChange = (title, prefix) => {
const nextTitle = `${(title || '')} - ${prefix}`
if (nextTitle !== document.title) {
setDocumentTitle(nextTitle)
}
}
const DocumentTitle = {
name: 'DocumentTitle',
functional: true,
props: {
prefix: {
type: String,
required: false,
default: 'Ant Design Pro'
},
title: {
type: String,
required: true
}
},
// { props, data, children }
// eslint-disable-next-line
render (createElement, { props, data, children }) {
handleStateChange(props.title, props.prefix)
return children
}
}
DocumentTitle.install = function (Vue) {
// const mountedInstances = []
// let state
// function __emit (sideEffect) {
// const options = sideEffect.options
// state = options.propsToState(mountedInstances.map(function (instance) {
// return instance
// }))
// options.handleStateChange(state)
// }
// Vue.mixin({
// beforeMount () {
// const sideEffect = this.$options.sideEffect
// if (sideEffect) {
// mountedInstances.push(this)
// __emit(sideEffect)
// }
// },
// updated () {
// const sideEffect = this.$options.sideEffect
// if (sideEffect) {
// __emit(sideEffect)
// }
// },
// beforeDestroy () {
// const sideEffect = this.$options.sideEffect
// if (sideEffect) {
// const index = mountedInstances.indexOf(this)
// mountedInstances.splice(index, 1)
// __emit(sideEffect)
// }
// }
// })
Vue.component(DocumentTitle.name, DocumentTitle)
}
export default DocumentTitle

View File

@@ -0,0 +1,17 @@
export const setDocumentTitle = function (title) {
document.title = title
const ua = navigator.userAgent
// eslint-disable-next-line
const regex = /\bMicroMessenger\/([\d\.]+)/
if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) {
const i = document.createElement('iframe')
i.src = '/favicon.ico'
i.style.display = 'none'
i.onload = function () {
setTimeout(function () {
i.remove()
}, 9)
}
document.body.appendChild(i)
}
}

View File

@@ -0,0 +1,7 @@
export default {
name: 'VueFragment',
functional: true,
render (h, ctx) {
return ctx.children.length > 1 ? h('div', {}, ctx.children) : ctx.children
}
}

View File

@@ -0,0 +1,43 @@
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { getComponentFromProp, hasProp } from 'ant-design-vue/lib/_util/props-util'
const GlobalFooterProps = {
links: PropTypes.array,
copyright: PropTypes.any
}
const GlobalFooter = {
name: 'GlobalFooter',
props: GlobalFooterProps,
render () {
const copyright = getComponentFromProp(this, 'copyright')
const links = getComponentFromProp(this, 'links')
const linksType = hasProp(links)
return (
<footer class="ant-pro-global-footer">
<div class="ant-pro-global-footer-links">
{linksType && (
links.map(link => (
<a
key={link.key}
title={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
>
{link.title}
</a>
))
) || links}
</div>
{copyright && (
<div class="ant-pro-global-footer-copyright">{copyright}</div>
)}
</footer>
)
}
}
export default GlobalFooter

View File

@@ -0,0 +1,31 @@
@import "~ant-design-vue/es/style/themes/default";
@global-footer-prefix-cls: ~'@{ant-prefix}-pro-global-footer';
.@{global-footer-prefix-cls} {
margin: 48px 0 24px 0;
padding: 0 16px;
text-align: center;
&-links {
margin-bottom: 8px;
a {
color: @text-color-secondary;
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
}
&:hover {
color: @text-color;
}
}
}
&-copyright {
color: @text-color-secondary;
font-size: @font-size-base;
}
}

View File

@@ -0,0 +1,81 @@
import './index.less'
import debounce from 'lodash/debounce'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { triggerEvent, inBrowser, isFun } from '../../utils/util'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
import { defaultRenderLogo } from '../SiderMenu/SiderMenu'
export const GlobalHeaderProps = {
collapsed: PropTypes.bool,
handleCollapse: PropTypes.func,
isMobile: PropTypes.bool.def(false),
fixedHeader: PropTypes.bool.def(false),
logo: PropTypes.any,
menuRender: PropTypes.any,
collapsedButtonRender: PropTypes.any,
headerContentRender: PropTypes.any,
rightContentRender: PropTypes.any
}
const defaultRenderCollapsedButton = (h, collapsed) => (
<Icon type={collapsed ? 'menu-unfold' : 'menu-fold'}/>
)
const GlobalHeader = {
name: 'GlobalHeader',
props: GlobalHeaderProps,
render (h) {
const { isMobile, logo, rightContentRender, headerContentRender } = this.$props
const toggle = () => {
const { collapsed, handleCollapse } = this.$props
if (handleCollapse) handleCollapse(!collapsed)
this.triggerResizeEvent()
}
const renderCollapsedButton = () => {
const {
collapsed,
collapsedButtonRender = defaultRenderCollapsedButton,
menuRender
} = this.$props
if (collapsedButtonRender !== false && menuRender !== false) {
return (
<span class="ant-pro-global-header-trigger" onClick={toggle}>
{isFun(collapsedButtonRender) && collapsedButtonRender(h, collapsed) || collapsedButtonRender}
</span>
)
}
return null
}
const headerCls = 'ant-pro-global-header'
return (
<div class={headerCls}>
{isMobile && (
<a class={`${headerCls}-logo`} key="logo" href={'/'}>
{defaultRenderLogo(h, logo)}
</a>
)}
{renderCollapsedButton()}
{headerContentRender && (
<div class={`${headerCls}-content`}>
{isFun(headerContentRender) && headerContentRender(h, this.$props) || headerContentRender}
</div>
)}
{isFun(rightContentRender) && rightContentRender(h, this.$props) || rightContentRender}
</div>
)
},
methods: {
triggerResizeEvent: debounce(() => {
inBrowser && triggerEvent(window, 'resize')
})
},
beforeDestroy () {
this.triggerResizeEvent.cancel && this.triggerResizeEvent.cancel()
}
}
export default GlobalHeader

View File

@@ -0,0 +1,109 @@
@import "~ant-design-vue/es/style/themes/default";
@global-header-prefix-cls: ~'@{ant-prefix}-pro-global-header';
@pro-header-bg: @component-background;
@pro-header-hover-bg: @component-background;
@pro-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
@pro-header-hover-trigger-action-bg: rgba(0,0,0,.025);
.@{global-header-prefix-cls} {
position: relative;
height: @layout-header-height;
padding: 0;
background: @pro-header-bg;
box-shadow: @pro-header-box-shadow;
&-index-right {
float: right;
height: 100%;
margin-left: auto;
overflow: hidden;
.@{global-header-prefix-cls}-index-action {
display: inline-block;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all .3s;
&:hover {
background: @pro-header-hover-trigger-action-bg;
}
}
}
&-logo {
display: inline-block;
height: @layout-header-height;
padding: 0 0 0 24px;
font-size: 20px;
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
img, svg {
display: inline-block;
width: 32px;
height: 32px;
vertical-align: middle;
}
}
&-menu {
.anticon {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 160px;
}
}
&-trigger {
height: @layout-header-height;
line-height: @layout-header-height;
vertical-align: top;
padding: 0 22px;
display: inline-block;
cursor: pointer;
transition: all 0.3s, padding 0s;
.anticon {
font-size: 20px;
vertical-align: -0.225em;
}
&:hover {
background: @pro-header-hover-trigger-action-bg;
}
}
&-content {
height: @layout-header-height;
line-height: @layout-header-height;
vertical-align: top;
display: inline-block;
}
.dark {
height: @layout-header-height;
.action {
color: rgba(255, 255, 255, 0.85);
> i {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&.opened {
background: @primary-color;
}
.ant-badge {
color: rgba(255, 255, 255, 0.85);
}
}
}
.ant-pro-global-header-index-action {
i {
color: rgba(0,0,0,.65);
vertical-align: middle;
}
}
}

View File

@@ -0,0 +1,27 @@
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { layoutContentWidth } from '../../utils/util'
const GridContent = {
name: 'GridContent',
functional: true,
props: {
children: PropTypes.any,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid')
},
render (h, content) {
const { contentWidth } = content.props
const children = content.children
const propsContentWidth = layoutContentWidth(contentWidth)
const classNames = {
'ant-pro-grid-content': true,
'wide': propsContentWidth
}
return <div class={classNames}>{children}</div>
}
}
export default GridContent

View File

@@ -0,0 +1,13 @@
@import "~ant-design-vue/es/style/themes/default";
@grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content';
.@{grid-content-prefix-cls} {
width: 100%;
min-height: 100%;
transition: 0.3s;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}

View File

@@ -0,0 +1,232 @@
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { isArray } from 'ant-design-vue/lib/_util/vue-types/utils'
import GridContent from '../GridContent'
import 'ant-design-vue/es/page-header/style'
import PageHeader, { PageHeaderProps } from 'ant-design-vue/es/page-header'
import 'ant-design-vue/es/tabs/style'
import Tabs from 'ant-design-vue/es/tabs'
import { getComponentFromProp } from 'ant-design-vue/lib/_util/props-util'
const prefixedClassName = 'ant-pro-page-header-wrap'
const PageHeaderTabConfig = {
tabList: PropTypes.array,
tabActiveKey: PropTypes.string,
tabProps: PropTypes.object,
tabChange: PropTypes.func
}
const PageHeaderWrapperProps = {
...PageHeaderTabConfig,
...PageHeaderProps,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
content: PropTypes.any,
extraContent: PropTypes.any,
pageHeaderRender: PropTypes.func,
breadcrumb: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).def(true),
back: PropTypes.func,
// only use `pro-layout` in children
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
const defaultI18nRender = (t) => t
const useContext = (route) => {
return route && {
...route.meta
} || null
}
const noop = () => {
}
// TODO :: tabList tab 支持图标 优化
const renderFooter = (h, tabConfigProps, i18nRender) => {
const {
tabList,
tabActiveKey,
tabChange: onTabChange,
tabBarExtraContent,
tabProps
} = tabConfigProps
return tabList && tabList.length > 0 && (
<Tabs
class={`${prefixedClassName}-tabs`}
activeKey={tabActiveKey}
onChange={key => {
if (onTabChange) {
onTabChange(key)
}
}}
tabBarExtraContent={tabBarExtraContent}
{...tabProps}
>
{tabList.map(item => (
<Tabs.TabPane {...item} tab={i18nRender(item.tab)} key={item.key}/>
))}
</Tabs>
)
}
const renderPageHeader = (h, content, extraContent) => {
if (!content && !extraContent) {
return null
}
return (
<div class={`${prefixedClassName}-detail`}>
<div class={`${prefixedClassName}-main`}>
<div class={`${prefixedClassName}-row`}>
{content && (
<div class={`${prefixedClassName}-content`}>{content}</div>
)}
{extraContent && (
<div class={`${prefixedClassName}-extraContent`}>
{extraContent}
</div>
)}
</div>
</div>
</div>
)
}
const defaultPageHeaderRender = (h, props, pageMeta, i18nRender) => {
const {
title: propTitle,
tags,
content,
pageHeaderRender,
extra,
extraContent,
breadcrumb,
back: handleBack,
...restProps
} = props
if (pageHeaderRender) {
return pageHeaderRender({ ...props })
}
let pageHeaderTitle = propTitle
if (!propTitle && propTitle !== false) {
pageHeaderTitle = pageMeta.title
}
// title props 不是 false 且不是 array 则直接渲染 title
// 反之认为是 VNode, 作为 render 参数直接传入到 PageHeader
const title = isArray(pageHeaderTitle)
? pageHeaderTitle
: pageHeaderTitle && i18nRender(pageHeaderTitle)
const tabProps = {
breadcrumb,
extra,
tags,
title,
footer: renderFooter(h, restProps, i18nRender)
}
if (!handleBack) {
tabProps.backIcon = false
}
return (
<PageHeader {...{ props: tabProps }} onBack={handleBack || noop}>
{renderPageHeader(h, content, extraContent)}
</PageHeader>
)
// return
}
const PageHeaderWrapper = {
name: 'PageHeaderWrapper',
props: PageHeaderWrapperProps,
inject: ['locale', 'contentWidth', 'breadcrumbRender'],
render (h) {
const { $route, $listeners } = this
const children = this.$slots.default
const title = getComponentFromProp(this, 'title')
const tags = getComponentFromProp(this, 'tags')
const content = getComponentFromProp(this, 'content')
const extra = getComponentFromProp(this, 'extra')
const extraContent = getComponentFromProp(this, 'extraContent')
const pageMeta = useContext(this.$props.route || $route)
const i18n = this.$props.i18nRender || this.locale || defaultI18nRender
const contentWidth = this.$props.contentWidth || this.contentWidth || false
// 当未设置 back props 或未监听 @back不显示 back
// props 的 back 事件优先级高于 @back需要注意
const onBack = this.$props.back || $listeners.back
const back = onBack && (() => {
// this.$emit('back')
// call props back func
onBack && onBack()
}) || undefined
const onTabChange = this.$props.tabChange
const tabChange = (key) => {
this.$emit('tabChange', key)
onTabChange && onTabChange(key)
}
let breadcrumb = {}
const propsBreadcrumb = this.$props.breadcrumb
if (propsBreadcrumb === true) {
const routes = $route.matched.concat().map(route => {
return {
path: route.path,
breadcrumbName: i18n(route.meta.title),
redirect: route.redirect
}
})
const defaultItemRender = ({ route, params, routes, paths, h }) => {
return (route.redirect === 'noRedirect' || routes.indexOf(route) === routes.length - 1) && (
<span>{route.breadcrumbName}</span>
) || (
<router-link to={{ path: route.path || '/', params }}>{route.breadcrumbName}</router-link>
)
}
// If custom breadcrumb render undefined
// use default breadcrumb..
const itemRender = this.breadcrumbRender || defaultItemRender
routes.splice(0, 1)
breadcrumb = { props: { routes, itemRender } }
} else {
breadcrumb = propsBreadcrumb || null
}
const props = {
...this.$props,
title,
tags,
content,
extra,
extraContent,
breadcrumb,
tabChange,
back
}
return (
<div class="ant-pro-page-header-wrap">
<div class={`${prefixedClassName}-page-header-warp`}>
<GridContent>{defaultPageHeaderRender(h, props, pageMeta, i18n)}</GridContent>
</div>
{children ? (
<GridContent contentWidth={contentWidth}>
<div class={`${prefixedClassName}-children-content`}>
{children}
</div>
</GridContent>
) : null}
</div>
)
}
}
PageHeaderWrapper.install = function (Vue) {
Vue.component(PageHeaderWrapper.name, PageHeaderWrapper)
Vue.component('page-container', PageHeaderWrapper)
}
export default PageHeaderWrapper

View File

@@ -0,0 +1,93 @@
@import "~ant-design-vue/es/style/themes/default";
@ant-pro-page-header-wrap: ~'@{ant-prefix}-pro-page-header-wrap';
.@{ant-pro-page-header-wrap}-children-content {
margin: 24px 24px 0;
}
.@{ant-pro-page-header-wrap}-page-header-warp {
background-color: @component-background;
}
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-detail {
display: flex;
}
.@{ant-pro-page-header-wrap}-row {
display: flex;
width: 100%;
}
.@{ant-pro-page-header-wrap}-title-content {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-title,
.@{ant-pro-page-header-wrap}-content {
flex: auto;
}
.@{ant-pro-page-header-wrap}-extraContent,
.@{ant-pro-page-header-wrap}-main {
flex: 0 1 auto;
}
.@{ant-pro-page-header-wrap}-main {
width: 100%;
}
.@{ant-pro-page-header-wrap}-title {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-logo {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-extraContent {
min-width: 242px;
margin-left: 88px;
text-align: right;
}
}
@media screen and (max-width: @screen-xl) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-lg) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 20px;
}
}
}
@media screen and (max-width: @screen-md) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-row {
display: block;
}
.@{ant-pro-page-header-wrap}-action,
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 0;
text-align: left;
}
}
}
@media screen and (max-width: @screen-sm) {
.@{ant-pro-page-header-wrap}-detail {
display: block;
}
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 0;
}
}

View File

@@ -0,0 +1,181 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/menu/style'
import Menu from 'ant-design-vue/es/menu'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
const {
Item: MenuItem,
SubMenu
} = Menu
export const RouteMenuProps = {
menus: PropTypes.array,
theme: PropTypes.string.def('dark'),
mode: PropTypes.string.def('inline'),
collapsedWidth: PropTypes.number.def('60'),
collapsed: PropTypes.bool.def(false),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
const httpReg = /(http|https|ftp):\/\/([\w.]+\/?)\S*/
const renderMenu = (h, item, i18nRender) => {
if (item && !item.hidden) {
const bool = item.children && !item.hideChildrenInMenu
return bool ? renderSubMenu(h, item, i18nRender) : renderMenuItem(h, item, i18nRender)
}
return null
}
const renderSubMenu = (h, item, i18nRender) => {
return (
<SubMenu key={item.path} title={(
<span>
{renderIcon(h, item.meta.icon)}
<span>{renderTitle(h, item.meta.title, i18nRender)}</span>
</span>
)}>
{!item.hideChildrenInMenu && item.children.map(cd => renderMenu(h, cd, i18nRender))}
</SubMenu>
)
}
const renderMenuItem = (h, item, i18nRender) => {
const meta = Object.assign({}, item.meta)
const target = meta.target || null
const hasRemoteUrl = httpReg.test(item.path)
const CustomTag = target && 'a' || 'router-link'
const props = { to: { name: item.name } }
const attrs = (hasRemoteUrl || target) ? { href: item.path, target: target } : {}
if (item.children && item.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
item.children.forEach(cd => {
cd.meta = Object.assign(cd.meta || {}, { hidden: true })
})
}
return (
<MenuItem key={item.path}>
<CustomTag {...{ props, attrs }}>
{renderIcon(h, meta.icon)}
{renderTitle(h, meta.title, i18nRender)}
</CustomTag>
</MenuItem>
)
}
const renderIcon = (h, icon) => {
if (icon === undefined || icon === 'none' || icon === null) {
return null
}
const props = {}
typeof (icon) === 'object' ? (props.component = icon) : (props.type = icon)
return <Icon {...{ props }} />
}
const renderTitle = (h, title, i18nRender) => {
return <span>{ i18nRender && i18nRender(title) || title }</span>
}
const RouteMenu = {
name: 'RouteMenu',
props: RouteMenuProps,
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: [],
cachedSelectedKeys: []
}
},
render (h) {
const { mode, theme, menus, i18nRender } = this
const handleOpenChange = (openKeys) => {
// 在水平模式下时,不再执行后续
if (mode === 'horizontal') {
this.openKeys = openKeys
return
}
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
}
const dynamicProps = {
props: {
mode,
theme,
openKeys: this.openKeys,
selectedKeys: this.selectedKeys
},
on: {
select: menu => {
this.$emit('select', menu)
if (!httpReg.test(menu.key)) {
this.selectedKeys = menu.selectedKeys
}
},
openChange: handleOpenChange
}
}
const menuItems = menus.map(item => {
if (item.hidden) {
return null
}
return renderMenu(h, item, i18nRender)
})
return <Menu {...dynamicProps } inlineIndent={16} >{menuItems}</Menu>
},
methods: {
updateMenu () {
const routes = this.$route.matched.concat()
const { hidden } = this.$route.meta
if (routes.length >= 3 && hidden) {
routes.pop()
this.selectedKeys = [routes[routes.length - 1].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
item.path && openKeys.push(item.path)
})
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
}
},
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menus.forEach(item => keys.push(item.path))
return keys
}
},
created () {
this.$watch('$route', () => {
this.updateMenu()
})
this.$watch('collapsed', val => {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
})
},
mounted () {
this.updateMenu()
}
}
export default RouteMenu

View File

@@ -0,0 +1,2 @@
import BaseMenu from './BaseMenu'
export default BaseMenu

View File

@@ -0,0 +1,69 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/tooltip/style'
import Tooltip from 'ant-design-vue/es/tooltip'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
const BlockCheckboxProps = {
value: PropTypes.string,
// Item: { key, url, title }
list: PropTypes.array,
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
const baseClassName = 'ant-pro-setting-drawer-block-checbox'
const BlockCheckbox = {
props: BlockCheckboxProps,
inject: ['locale'],
render (h) {
const { value, list } = this
const i18n = this.$props.i18nRender || this.locale
const items = list || [
{
key: 'sidemenu',
url:
'https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg',
title: i18n('app.setting.sidemenu')
},
{
key: 'topmenu',
url:
'https://gw.alipayobjects.com/zos/antfincdn/URETY8%24STp/KDNDBbriJhLwuqMoxcAr.svg',
title: i18n('app.setting.topmenu')
}
]
const handleChange = (key) => {
this.$emit('change', key)
}
const disableStyle = {
cursor: 'not-allowed'
}
return (
<div class={baseClassName} key={value}>
{items.map(item => (
<Tooltip title={item.title} key={item.key}>
<div class={`${baseClassName}-item`} style={ item.disable && disableStyle } onClick={() => !item.disable && handleChange(item.key)}>
<img src={item.url} alt={item.key} />
<div
class={`${baseClassName}-selectIcon`}
style={{
display: value === item.key ? 'block' : 'none'
}}
>
<Icon type="check" />
</div>
</div>
</Tooltip>
))}
</div>
)
}
}
export default BlockCheckbox

View File

@@ -0,0 +1,95 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/tooltip/style'
import Tooltip from 'ant-design-vue/es/tooltip'
import 'ant-design-vue/es/list/style'
import List from 'ant-design-vue/es/list'
import 'ant-design-vue/es/select/style'
import Select from 'ant-design-vue/es/select'
import 'ant-design-vue/es/switch/style'
import Switch from 'ant-design-vue/es/switch'
export const renderLayoutSettingItem = (h, item) => {
const action = { ...item.action }
return (
<Tooltip title={item.disabled ? item.disabledReason : ''} placement="left">
<List.Item actions={[action]}>
<span style={{ opacity: item.disabled ? 0.5 : 1 }}>{item.title}</span>
</List.Item>
</Tooltip>
)
}
export const LayoutSettingProps = {
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
fixedHeader: PropTypes.bool,
fixSiderbar: PropTypes.bool,
layout: PropTypes.oneOf(['sidemenu', 'topmenu']),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
export default {
props: LayoutSettingProps,
inject: ['locale'],
render (h) {
const i18n = this.$props.i18nRender || this.locale
const { contentWidth, fixedHeader, layout, fixSiderbar } = this
const handleChange = (type, value) => {
this.$emit('change', { type, value })
}
return (
<List
split={false}
dataSource={[
{
title: i18n('app.setting.content-width'),
action: (
<Select
value={contentWidth}
size="small"
onSelect={(value) => handleChange('contentWidth', value)}
style={{ width: '80px' }}
>
{layout === 'sidemenu' ? null : (
<Select.Option value="Fixed">
{i18n('app.setting.content-width.fixed')}
</Select.Option>
)}
<Select.Option value="Fluid">
{i18n('app.setting.content-width.fluid')}
</Select.Option>
</Select>
)
},
{
title: i18n('app.setting.fixedheader'),
action: (
<Switch
size="small"
checked={!!fixedHeader}
onChange={(checked) => handleChange('fixedHeader', checked)}
/>
)
},
{
title: i18n('app.setting.fixedsidebar'),
disabled: layout === 'topmenu',
disabledReason: i18n('app.setting.fixedsidebar.hint'),
action: (
<Switch
size="small"
disabled={layout === 'topmenu'}
checked={!!fixSiderbar}
onChange={(checked) => handleChange('fixSiderbar', checked)}
/>
)
}
]}
renderItem={(item, index) => renderLayoutSettingItem(h, item)}
/>
)
}
}

View File

@@ -0,0 +1,75 @@
import './ThemeColor.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { genThemeToString } from '../../utils/util'
import 'ant-design-vue/es/tooltip/style'
import Tooltip from 'ant-design-vue/es/tooltip'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
const baseClassName = 'theme-color'
export const TagProps = {
color: PropTypes.string,
check: PropTypes.bool
}
const Tag = {
props: TagProps,
functional: true,
render (h, content) {
const { props: { color, check }, data } = content
return (
<div {...data} style={{ backgroundColor: color }}>
{ check ? <Icon type="check" /> : null }
</div>
)
}
}
export const ThemeColorProps = {
colors: PropTypes.array,
title: PropTypes.string,
value: PropTypes.string,
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
const ThemeColor = {
props: ThemeColorProps,
inject: ['locale'],
render (h) {
const { title, value, colors = [] } = this
const i18n = this.$props.i18nRender || this.locale
const handleChange = (key) => {
this.$emit('change', key)
}
return (
<div class={baseClassName} ref={'ref'}>
<h3 class={`${baseClassName}-title`}>{title}</h3>
<div class={`${baseClassName}-content`}>
{colors.map(item => {
const themeKey = genThemeToString(item.key)
const check = value === item.key || genThemeToString(value) === item.key
return (
<Tooltip
key={item.color}
title={themeKey ? i18n(`app.setting.themecolor.${themeKey}`) : item.key}
>
<Tag
class={`${baseClassName}-block`}
color={item.color}
check={check}
onClick={() => handleChange(item.key)}
/>
</Tooltip>
)
})}
</div>
</div>
)
}
}
export default ThemeColor

View File

@@ -0,0 +1,26 @@
@import './index.less';
.@{ant-pro-setting-drawer}-content {
.theme-color {
margin-top: 24px;
overflow: hidden;
.theme-color-title {
margin-bottom: 12px;
font-size: 14px;
line-height: 22px;
}
.theme-color-block {
float: left;
width: 20px;
height: 20px;
margin-right: 8px;
color: #fff;
font-weight: bold;
text-align: center;
border-radius: 2px;
cursor: pointer;
}
}
}

View File

@@ -0,0 +1,267 @@
import './index.less'
import omit from 'omit.js'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/divider/style'
import Divider from 'ant-design-vue/es/divider'
import 'ant-design-vue/es/drawer/style'
import Drawer from 'ant-design-vue/es/drawer'
import 'ant-design-vue/es/button/style'
import Button from 'ant-design-vue/es/button'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
import 'ant-design-vue/es/alert/style'
import Alert from 'ant-design-vue/es/alert'
import antPortal from 'ant-design-vue/es/_util/portalDirective'
import 'ant-design-vue/es/message/style'
import message from 'ant-design-vue/es/message'
import BlockCheckbox from './BlockCheckbox'
import ThemeColor from './ThemeColor'
import { updateTheme, updateColorWeak } from '../../utils/dynamicTheme'
import { genStringToTheme } from '../../utils/util'
import CopyToClipboard from 'vue-copy-to-clipboard'
const baseClassName = 'ant-pro-setting-drawer'
const BodyProps = {
title: PropTypes.string.def('')
}
const Body = {
props: BodyProps,
render (h) {
const { title } = this
return (
<div style={{ marginBottom: 24 }}>
<h3 class={`${baseClassName}-title`}>{title}</h3>
{this.$slots.default}
</div>
)
}
}
const defaultI18nRender = (t) => t
const getThemeList = (i18nRender) => {
const list = window.umi_plugin_ant_themeVar || []
const themeList = [
{
key: 'light',
url: 'https://gw.alipayobjects.com/zos/antfincdn/NQ%24zoisaD2/jpRkZQMyYRryryPNtyIC.svg',
title: i18nRender('app.setting.pagestyle.light')
}
]
const darkColorList = [
{
key: '#1890ff',
color: '#1890ff',
theme: 'dark'
}
]
const lightColorList = [
{
key: '#1890ff',
color: '#1890ff',
theme: 'dark'
}
]
// insert theme color List
list.forEach(item => {
const color = (item.modifyVars || {})['@primary-color']
if (item.theme === 'dark' && color) {
darkColorList.push({
color,
...item
})
}
if (!item.theme || item.theme === 'light') {
lightColorList.push({
color,
...item
})
}
})
return {
colorList: {
dark: darkColorList,
light: lightColorList
},
themeList
}
}
const handleChangeSetting = (key, value, hideMessageLoading) => {
if (key === 'primaryColor') {
// 更新主色调
updateTheme(value)
}
if (key === 'colorWeak') {
updateColorWeak(value)
}
}
const genCopySettingJson = (settings) =>
JSON.stringify(
omit(
{
...settings,
primaryColor: genStringToTheme(settings.primaryColor)
},
['colorWeak']
),
null,
2
)
export const settings = {
theme: PropTypes.oneOf(['dark', 'light', 'realDark']),
primaryColor: PropTypes.string,
layout: PropTypes.oneOf(['sidemenu', 'topmenu']),
colorWeak: PropTypes.bool,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
// 替换兼容 PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid')
// contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'),
fixedHeader: PropTypes.bool,
fixSiderbar: PropTypes.bool,
hideHintAlert: PropTypes.bool.def(false),
hideCopyButton: PropTypes.bool.def(false)
}
export const SettingDrawerProps = {
getContainer: PropTypes.func,
settings: PropTypes.objectOf(settings),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
const SettingDrawer = {
name: 'SettingDrawer',
props: SettingDrawerProps,
inject: ['locale'],
data () {
return {
show: false
}
},
render (h) {
const {
setShow,
getContainer,
settings
} = this
const {
theme = 'dark',
primaryColor = 'daybreak',
hideHintAlert,
hideCopyButton
} = settings
const i18n = this.$props.i18nRender || this.locale || defaultI18nRender
const themeList = getThemeList(i18n)
const iconStyle = {
color: '#fff',
fontSize: 20
}
const changeSetting = (type, value) => {
this.$emit('change', { type, value })
handleChangeSetting(type, value, false)
}
return (
<Drawer
visible={this.show}
width={300}
onClose={() => setShow(false)}
placement="right"
getContainer={getContainer}
style={{
zIndex: 999
}}
>
<template slot="handle">
<div class={`${baseClassName}-handle`} onClick={() => setShow(!this.show)} style="display:none;">
{this.show
? (<Icon type="close" style={iconStyle}/>)
: (<Icon type="setting" style={iconStyle}/>)
}
</div>
</template>
<div class={`${baseClassName}-content`}>
<Body title={i18n('app.setting.pagestyle')}>
<BlockCheckbox i18nRender={i18n} list={themeList.themeList} value={theme} onChange={(val) => {
changeSetting('theme', val)
}} />
</Body>
<Divider />
<ThemeColor
i18nRender={i18n}
title={i18n('app.setting.themecolor')}
value={primaryColor}
colors={themeList.colorList[theme === 'realDark' ? 'dark' : 'light']}
onChange={(color) => {
// changeSetting('primaryColor', color, null)
// 解决严重漏洞
changeSetting('primaryColor', color)
}}
/>
{hideHintAlert && hideCopyButton ? null : <Divider />}
{hideHintAlert ? null : (
<Alert
type="warning"
message={i18n('app.setting.production.hint')}
icon={(<Icon type={'notification'} />)}
showIcon
style={{ marginBottom: '16px' }}
/>
)}
{hideCopyButton ? null : (
<CopyToClipboard
text={genCopySettingJson(settings)}
onCopy={() =>
message.success(i18n('app.setting.copyinfo'))
}
>
<Button block>
<Icon type={'copy'} />{i18n('app.setting.copy')}
</Button>
</CopyToClipboard>
)}
<div class={`${baseClassName}-content-footer`}>
{this.$slots.default}
</div>
</div>
</Drawer>
)
},
methods: {
setShow (flag) {
this.show = flag
}
}
}
SettingDrawer.install = function (Vue) {
Vue.use(antPortal)
Vue.component(SettingDrawer.name, SettingDrawer)
}
export default SettingDrawer

View File

@@ -0,0 +1,90 @@
@import "../../../../assets/styles/default.less";
@ant-pro-setting-drawer: ~'@{ant-prefix}-pro-setting-drawer';
.@{ant-pro-setting-drawer} {
&-content {
position: relative;
min-height: 100%;
.ant-list-item {
span {
flex: 1;
}
}
}
&-block-checbox {
display: flex;
&-item {
position: relative;
margin-right: 16px;
// box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
border-radius: @border-radius-base;
cursor: pointer;
img {
width: 48px;
}
}
&-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: @primary-color;
font-weight: bold;
font-size: 14px;
.action {
color: @primary-color;
}
}
}
&-color_block {
display: inline-block;
width: 38px;
height: 22px;
margin: 4px;
margin-right: 12px;
vertical-align: middle;
border-radius: 4px;
cursor: pointer;
}
&-title {
margin-bottom: 12px;
color: @heading-color;
font-size: 14px;
line-height: 22px;
}
&-handle {
position: absolute;
top: 240px;
right: 300px;
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
font-size: 16px;
text-align: center;
background: @primary-color;
border-radius: 4px 0 0 4px;
cursor: pointer;
pointer-events: auto;
}
&-production-hint {
margin-top: 16px;
font-size: 12px;
}
}

View File

@@ -0,0 +1,344 @@
import './index.less'
import omit from 'omit.js'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/divider/style'
import Divider from 'ant-design-vue/es/divider'
import 'ant-design-vue/es/drawer/style'
import Drawer from 'ant-design-vue/es/drawer'
import 'ant-design-vue/es/list/style'
import List from 'ant-design-vue/es/list'
import 'ant-design-vue/es/switch/style'
import Switch from 'ant-design-vue/es/switch'
import 'ant-design-vue/es/button/style'
import Button from 'ant-design-vue/es/button'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
import 'ant-design-vue/es/alert/style'
import Alert from 'ant-design-vue/es/alert'
import antPortal from 'ant-design-vue/es/_util/portalDirective'
import 'ant-design-vue/es/message/style'
import message from 'ant-design-vue/es/message'
import BlockCheckbox from './BlockCheckbox'
import ThemeColor from './ThemeColor'
import LayoutSetting, { renderLayoutSettingItem } from './LayoutChange'
import { updateTheme, updateColorWeak } from '../../utils/dynamicTheme'
import { genStringToTheme } from '../../utils/util'
import CopyToClipboard from 'vue-copy-to-clipboard'
const baseClassName = 'ant-pro-setting-drawer'
const BodyProps = {
title: PropTypes.string.def('')
}
const Body = {
props: BodyProps,
render (h) {
const { title } = this
return (
<div style={{ marginBottom: 24 }}>
<h3 class={`${baseClassName}-title`}>{title}</h3>
{this.$slots.default}
</div>
)
}
}
const defaultI18nRender = (t) => t
const getThemeList = (i18nRender) => {
const list = window.umi_plugin_ant_themeVar || []
const themeList = [
{
key: 'light',
url: 'https://gw.alipayobjects.com/zos/antfincdn/NQ%24zoisaD2/jpRkZQMyYRryryPNtyIC.svg',
title: i18nRender('app.setting.pagestyle.light')
},
{
key: 'dark',
url: 'https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg',
title: i18nRender('app.setting.pagestyle.dark')
}
]
const darkColorList = [
{
key: '#1890ff',
color: '#1890ff',
theme: 'dark'
}
]
const lightColorList = [
{
key: '#1890ff',
color: '#1890ff',
theme: 'dark'
}
]
if (list.find((item) => item.theme === 'dark')) {
themeList.push({
// disable click
disable: true,
key: 'realDark',
url: 'https://gw.alipayobjects.com/zos/antfincdn/hmKaLQvmY2/LCkqqYNmvBEbokSDscrm.svg',
title: i18nRender('app.setting.pagestyle.realdark')
})
}
// insert theme color List
list.forEach(item => {
const color = (item.modifyVars || {})['@primary-color']
if (item.theme === 'dark' && color) {
darkColorList.push({
color,
...item
})
}
if (!item.theme || item.theme === 'light') {
lightColorList.push({
color,
...item
})
}
})
return {
colorList: {
dark: darkColorList,
light: lightColorList
},
themeList
}
}
const handleChangeSetting = (key, value, hideMessageLoading) => {
if (key === 'primaryColor') {
// 更新主色调
updateTheme(value)
}
if (key === 'colorWeak') {
updateColorWeak(value)
}
}
const genCopySettingJson = (settings) =>
JSON.stringify(
omit(
{
...settings,
primaryColor: genStringToTheme(settings.primaryColor)
},
['colorWeak']
),
null,
2
)
export const settings = {
theme: PropTypes.oneOf(['dark', 'light', 'realDark']),
primaryColor: PropTypes.string,
layout: PropTypes.oneOf(['sidemenu', 'topmenu']),
colorWeak: PropTypes.bool,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
// 替换兼容 PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid')
// contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'),
fixedHeader: PropTypes.bool,
fixSiderbar: PropTypes.bool,
hideHintAlert: PropTypes.bool.def(false),
hideCopyButton: PropTypes.bool.def(false)
}
export const SettingDrawerProps = {
getContainer: PropTypes.func,
settings: PropTypes.objectOf(settings),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false)
}
const SettingDrawer = {
name: 'SettingDrawer',
props: SettingDrawerProps,
inject: ['locale'],
data () {
return {
show: false
}
},
render (h) {
const {
setShow,
getContainer,
settings
} = this
const {
theme = 'dark',
primaryColor = 'daybreak',
layout = 'sidemenu',
fixedHeader = false,
fixSiderbar = false,
contentWidth,
hideHintAlert,
hideCopyButton,
colorWeak
} = settings
const i18n = this.$props.i18nRender || this.locale || defaultI18nRender
const themeList = getThemeList(i18n)
const isTopMenu = layout === 'topmenu'
const iconStyle = {
color: '#fff',
fontSize: 20
}
const changeSetting = (type, value) => {
this.$emit('change', { type, value })
handleChangeSetting(type, value, false)
}
return (
<Drawer
visible={this.show}
width={300}
onClose={() => setShow(false)}
placement="right"
getContainer={getContainer}
/* handle={
<div class="ant-pro-setting-drawer-handle" onClick={() => setShow(!this.show)}>
{this.show
? (<Icon type="close" style={iconStyle} />)
: (<Icon type="setting" style={iconStyle} />)
}
</div>
} */
style={{
zIndex: 999
}}
>
<template slot="handle">
<div class={`${baseClassName}-handle`} onClick={() => setShow(!this.show)} style="display:none;">
{this.show
? (<Icon type="close" style={iconStyle}/>)
: (<Icon type="setting" style={iconStyle}/>)
}
</div>
</template>
<div class={`${baseClassName}-content`}>
<Body title={i18n('app.setting.pagestyle')}>
<BlockCheckbox i18nRender={i18n} list={themeList.themeList} value={theme} onChange={(val) => {
changeSetting('theme', val)
}} />
</Body>
<ThemeColor
i18nRender={i18n}
title={i18n('app.setting.themecolor')}
value={primaryColor}
colors={themeList.colorList[theme === 'realDark' ? 'dark' : 'light']}
onChange={(color) => {
// changeSetting('primaryColor', color, null)
// 解决严重漏洞
changeSetting('primaryColor', color)
}}
/>
<Divider />
<Body title={i18n('app.setting.navigationmode')}>
<BlockCheckbox i18nRender={i18n} value={layout} onChange={(value) => {
// changeSetting('layout', value, null)
// 解决严重漏洞
changeSetting('layout', value)
}} />
</Body>
<LayoutSetting
i18nRender={i18n}
contentWidth={contentWidth}
fixedHeader={fixedHeader}
fixSiderbar={isTopMenu ? false : fixSiderbar}
layout={layout}
onChange={({ type, value }) => {
changeSetting(type, value)
}}
/>
<Divider />
<Body title={i18n('app.setting.othersettings')}>
<List
split={false}
renderItem={(item) => renderLayoutSettingItem(h, item)}
dataSource={[
{
title: i18n('app.setting.weakmode'),
action: (
<Switch
size="small"
checked={!!colorWeak}
onChange={(checked) => changeSetting('colorWeak', checked)}
/>
)
}
]}
/>
</Body>
{hideHintAlert && hideCopyButton ? null : <Divider />}
{hideHintAlert ? null : (
<Alert
type="warning"
message={i18n('app.setting.production.hint')}
icon={(<Icon type={'notification'} />)}
showIcon
style={{ marginBottom: '16px' }}
/>
)}
{hideCopyButton ? null : (
<CopyToClipboard
text={genCopySettingJson(settings)}
onCopy={() =>
message.success(i18n('app.setting.copyinfo'))
}
>
<Button block>
<Icon type={'copy'} />{i18n('app.setting.copy')}
</Button>
</CopyToClipboard>
)}
<div class={`${baseClassName}-content-footer`}>
{this.$slots.default}
</div>
</div>
</Drawer>
)
},
methods: {
setShow (flag) {
this.show = flag
}
}
}
SettingDrawer.install = function (Vue) {
Vue.use(antPortal)
Vue.component(SettingDrawer.name, SettingDrawer)
}
export default SettingDrawer

View File

@@ -0,0 +1,137 @@
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import { isFun } from '../../utils/util'
import BaseMenu from '../RouteMenu'
const { Sider } = Layout
export const SiderMenuProps = {
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
mode: PropTypes.string.def('inline'),
theme: PropTypes.string.def('light '),
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
collapsible: PropTypes.bool,
collapsed: PropTypes.bool,
handleCollapse: PropTypes.func,
menus: PropTypes.array,
siderWidth: PropTypes.number.def(210),
collapsedWidth: PropTypes.number.def(60),
isMobile: PropTypes.bool,
layout: PropTypes.string.def('inline'),
fixSiderbar: PropTypes.bool,
logo: PropTypes.any,
title: PropTypes.string.def(''),
multiTab: PropTypes.bool,
// render function or vnode
menuHeaderRender: PropTypes.oneOfType([PropTypes.func, PropTypes.array, PropTypes.object, PropTypes.bool]),
menuRender: PropTypes.oneOfType([PropTypes.func, PropTypes.array, PropTypes.object, PropTypes.bool])
}
export const defaultRenderLogo = (h, logo) => {
if (typeof logo === 'string') {
return <img src={logo} alt="logo" />
}
if (typeof logo === 'function') {
return logo()
}
return h(logo)
}
export const defaultRenderLogoAntTitle = (h, props) => {
const {
logo = 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg',
title,
menuHeaderRender
} = props
if (menuHeaderRender === false) {
return null
}
const logoDom = defaultRenderLogo(h, logo)
const titleDom = <h1>{title}</h1>
if (menuHeaderRender) {
return isFun(menuHeaderRender) &&
menuHeaderRender(h, logoDom, props.collapsed ? null : titleDom, props) ||
menuHeaderRender
}
return (
<span>
{logoDom}
{titleDom}
</span>
)
}
const SiderMenu = {
name: 'SiderMenu',
model: {
prop: 'collapsed',
event: 'collapse'
},
props: SiderMenuProps,
render (h) {
const {
collapsible,
collapsed,
siderWidth,
fixSiderbar,
collapsedWidth,
mode,
theme,
menus,
logo,
title,
onMenuHeaderClick = () => null,
i18nRender,
menuHeaderRender,
menuRender
} = this
const siderCls = ['ant-pro-sider-menu-sider']
if (fixSiderbar) siderCls.push('fix-sider-bar')
if (theme === 'light') siderCls.push('light')
//
// const handleCollapse = (collapsed, type) => {
// this.$emit('collapse', collapsed)
// }
const headerDom = defaultRenderLogoAntTitle(h, {
logo, title, menuHeaderRender, collapsed
})
return (<Sider
class={siderCls}
breakpoint={'lg'}
trigger={null}
width={siderWidth}
theme={theme}
collapsible={collapsible}
collapsed={collapsed}
collapsedWidth={collapsedWidth}
>
{headerDom && (
<div
class="ant-pro-sider-menu-logo"
onClick={onMenuHeaderClick}
id="logo"
>
<router-link to={{ path: '/' }}>
{headerDom}
</router-link>
</div>
)}
{menuRender && (
isFun(menuRender) &&
menuRender(h, this.$props) ||
menuRender
) || (
<BaseMenu collapsed={collapsed} menus={menus} mode={mode} theme={theme} i18nRender={i18nRender} collapsedWidth={collapsedWidth} />
)}
</Sider>)
}
}
export default SiderMenu

View File

@@ -0,0 +1,54 @@
import './index.less'
import 'ant-design-vue/es/drawer/style'
import Drawer from 'ant-design-vue/es/drawer'
import SiderMenu, { SiderMenuProps } from './SiderMenu'
const SiderMenuWrapper = {
name: 'SiderMenuWrapper',
model: {
prop: 'collapsed',
event: 'collapse'
},
props: SiderMenuProps,
render (h) {
const {
layout,
isMobile,
collapsed
} = this
const isTopMenu = layout === 'topmenu'
const handleCollapse = (e) => {
this.$emit('collapse', true)
}
return isMobile ? (
<Drawer
class="ant-pro-sider-menu"
visible={!collapsed}
placement="left"
maskClosable
getContainer={null}
onClose={handleCollapse}
bodyStyle={{
padding: 0,
height: '100vh'
}}
>
<SiderMenu {...{ props: { ...this.$props, collapsed: isMobile ? false : collapsed } } } />
</Drawer>
) : !isTopMenu && (
<SiderMenu class="ant-pro-sider-menu" {...{ props: this.$props }} />
)
}
}
SiderMenuWrapper.install = function (Vue) {
Vue.component(SiderMenuWrapper.name, SiderMenuWrapper)
}
export {
SiderMenu,
SiderMenuProps
}
export default SiderMenuWrapper

View File

@@ -0,0 +1,130 @@
@import "../../../../assets/styles/default.less";
@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu';
@nav-header-height: @layout-header-height;
.@{sider-menu-prefix-cls} {
&-logo {
position: relative;
height: 64px;
padding-left: 24px;
overflow: hidden;
transition: all .3s;
line-height: @nav-header-height;
background: @layout-sider-background;
svg, img, h1 {
display: inline-block;
}
svg, img {
height: 32px;
width: 32px;
vertical-align: middle;
}
h1 {
color: @white;
font-size: 20px;
margin: 0 0 0 12px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
font-weight: 600;
vertical-align: middle;
}
}
&-sider {
position: relative;
z-index: 10;
min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
&.fix-sider-bar {
position: fixed;
top: 0;
left: 0;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.ant-menu-root {
height: ~'calc(100vh - @{nav-header-height})';
overflow-y: auto;
}
.ant-menu-inline {
border-right: 0;
.ant-menu-item,
.ant-menu-submenu-title {
width: 100%;
}
}
}
&.light {
background-color: white;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.@{sider-menu-prefix-cls}-logo {
background: white;
box-shadow: 1px 1px 0 0 @border-color-split;
h1 {
color: @primary-color;
}
}
.ant-menu-light {
border-right-color: transparent;
}
}
}
&-icon {
width: 14px;
vertical-align: baseline;
}
.top-nav-menu li.ant-menu-item {
height: @nav-header-height;
line-height: @nav-header-height;
}
.drawer .drawer-content {
background: @layout-sider-background;
}
.ant-menu-inline-collapsed {
& > .ant-menu-item .sider-menu-item-img + span,
&
> .ant-menu-item-group
> .ant-menu-item-group-list
> .ant-menu-item
.sider-menu-item-img
+ span,
&
> .ant-menu-submenu
> .ant-menu-submenu-title
.sider-menu-item-img
+ span {
display: inline-block;
max-width: 0;
opacity: 0;
}
}
.ant-menu-item .sider-menu-item-img + span,
.ant-menu-submenu-title .sider-menu-item-img + span {
opacity: 1;
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
}
.ant-menu-item .anticon + span,
.ant-menu-submenu-title .anticon + span{
line-height: 21px;
vertical-align: middle;
}
.ant-drawer-body {
padding: 0;
}
}

View File

@@ -0,0 +1,15 @@
import RouteMenu from './RouteMenu'
import SiderMenuWrapper, { SiderMenu, SiderMenuProps } from './SiderMenu'
import PageHeaderWrapper from './PageHeaderWrapper'
import GlobalFooter from './GlobalFooter'
import VueFragment from './Fragment'
export {
RouteMenu,
SiderMenu,
SiderMenuProps,
SiderMenuWrapper,
PageHeaderWrapper,
GlobalFooter,
VueFragment
}

View File

@@ -0,0 +1,9 @@
export { default, BasicLayoutProps } from './BasicLayout'
export { default as BlockLayout } from './BlockLayout'
export { default as PageHeaderWrapper } from './components/PageHeaderWrapper'
export { default as SiderMenuWrapper } from './components/SiderMenu'
export { default as GlobalFooter } from './components/GlobalFooter'
export { default as SettingDrawer } from './components/SettingDrawer'
export { default as DocumentTitle } from './components/DocumentTitle'
export { default as BaseMenu } from './components/RouteMenu'
export { updateTheme, updateColorWeak } from './utils/dynamicTheme'

View File

@@ -0,0 +1,38 @@
import client from 'webpack-theme-color-replacer/client'
import generate from '@ant-design/colors/lib/generate'
// import { message } from 'ant-design-vue'
export const themeColor = {
getAntdSerials (color) {
// 淡化即less的tint
const lightens = new Array(9).fill().map((t, i) => {
return client.varyColor.lighten(color, i / 10)
})
// colorPalette 变换得到颜色值
const colorPalettes = generate(color)
const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',')
return lightens.concat(colorPalettes).concat(rgb)
},
changeColor (newColor) {
const options = {
newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
changeUrl (cssUrl) {
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
}
}
return client.changer.changeColor(options, Promise)
}
}
export const updateTheme = newPrimaryColor => {
// const hideMessage = message.loading('正在切换主题', 0)
themeColor.changeColor(newPrimaryColor).then(r => {
// hideMessage()
})
}
export const updateColorWeak = colorWeak => {
// document.body.className = colorWeak ? 'colorWeak' : '';
const app = document.body.querySelector('#app')
colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak')
}

View File

@@ -0,0 +1,62 @@
import request, { extend } from 'umi-request'
import { notification } from 'ant-design-vue'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
const errorHandler = (error) => {
const { response = {} } = error
const errortext = codeMessage[response.status] || response.statusText
const { status, url } = response
notification.error({
message: `请求错误 ${status}: ${url}`,
description: errortext
})
}
export const BASE_URL = process.env.VUE_APP_API_URL || '/api/v1'
const customRequest = extend({
prefix: BASE_URL,
timeout: 1000,
errorHandler
})
// request 拦截器
customRequest.interceptors.request.use((url, options) => {
return (
{
url: `${url}&interceptors=yes`,
options: { ...options, interceptors: true }
}
)
})
// response 拦截器
customRequest.interceptors.response.use((response, options) => {
response.headers.append('interceptors', 'yes yo')
return response
})
export {
request,
extend
}
export default customRequest

View File

@@ -0,0 +1,64 @@
import triggerEvent from 'ant-design-vue/es/_util/triggerEvent'
import { inBrowser } from 'ant-design-vue/es/_util/env'
const getComponentFromProp = (instance, prop) => {
const slots = instance.slots && instance.slots()
return slots[prop] || instance.props[prop]
}
const isFun = (func) => {
return typeof func === 'function'
}
// 兼容 0.3.4~0.3.8
export const contentWidthCheck = (contentWidth) => {
return Object.prototype.toString.call(contentWidth) === '[object Boolean]'
? contentWidth === true && 'Fixed' || 'Fluid'
: contentWidth
}
export const layoutContentWidth = (contentType) => {
return contentType !== 'Fluid'
}
const themeConfig = {
daybreak: 'geekblue',
'#2F54EB': 'geekblue',
'#1890ff': 'daybreak',
'#F5222D': 'dust',
'#FA541C': 'volcano',
'#FAAD14': 'sunset',
'#13C2C2': 'cyan',
'#52C41A': 'green',
'#722ED1': 'purple'
}
const invertKeyValues = (obj) =>
Object.keys(obj).reduce((acc, key) => {
acc[obj[key]] = key
return acc
}, {})
/**
* #1890ff -> daybreak
* @param val
*/
export function genThemeToString (val) {
return val && themeConfig[val] ? themeConfig[val] : val
}
/**
* daybreak-> #1890ff
* @param val
*/
export function genStringToTheme (val) {
const stringConfig = invertKeyValues(themeConfig)
return val && stringConfig[val] ? stringConfig[val] : val
}
export {
triggerEvent,
inBrowser,
getComponentFromProp,
isFun
}

View File

@@ -0,0 +1,63 @@
import { Select } from 'ant-design-vue'
import './index.less'
const GlobalSearch = {
name: 'GlobalSearch',
data () {
return {
visible: false
}
},
mounted () {
const keyboardHandle = (e) => {
e.preventDefault()
e.stopPropagation()
const { ctrlKey, shiftKey, altKey, keyCode } = e
console.log('keyCode:', e.keyCode, e)
// key is `K` and hold ctrl
if (keyCode === 75 && ctrlKey && !shiftKey && !altKey) {
this.visible = !this.visible
}
}
document.addEventListener('keydown', keyboardHandle)
},
render () {
const { visible } = this
const handleSearch = (e) => {
this.$emit('search', e)
}
const handleChange = (e) => {
this.$emit('change', e)
}
if (!visible) {
return null
}
return (
<div class={'global-search global-search-wrapper'}>
<div class={'global-search-box'}>
<Select
size={'large'}
showSearch
placeholder="Input search text.."
style={{ width: '100%' }}
defaultActiveFirstOption={false}
showArrow={false}
filterOption={false}
onSearch={handleSearch}
onChange={handleChange}
notFoundContent={null}
>
</Select>
<div class={'global-search-tips'}>Open with Ctrl/ + K</div>
</div>
</div>
)
}
}
GlobalSearch.install = function (Vue) {
Vue.component(GlobalSearch.name, GlobalSearch)
}
export default GlobalSearch

View File

@@ -0,0 +1,25 @@
@import "~ant-design-vue/es/style/themes/default";
.global-search-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: @zindex-modal-mask;
background: @modal-mask-bg;
.global-search-box {
position: absolute;
top: 20%;
left: 50%;
width: 450px;
transform: translate(-50%, -50%);
.global-search-tips {
color: @white;
font-size: @font-size-lg;
text-align: right;
}
}
}

View File

@@ -0,0 +1,54 @@
import './index.less'
import { Icon, Menu, Dropdown } from 'ant-design-vue'
import { i18nRender } from '@/locales'
import i18nMixin from '@/store/i18n-mixin'
const locales = ['zh-CN', 'en-US']
const languageLabels = {
'zh-CN': '简体中文',
'en-US': 'English(暂不支持)'
}
// eslint-disable-next-line
const languageIcons = {
'zh-CN': '🇨🇳',
'en-US': '🇺🇸'
}
const SelectLang = {
props: {
prefixCls: {
type: String,
default: 'ant-pro-drop-down'
}
},
name: 'SelectLang',
mixins: [i18nMixin],
render () {
const { prefixCls } = this
const changeLang = ({ key }) => {
this.setLang(key)
}
const langMenu = (
<Menu class={['menu', 'ant-pro-header-menu']} selectedKeys={[this.currentLang]} onClick={changeLang}>
{locales.map(locale => (
<Menu.Item key={locale}>
<span role="img" aria-label={languageLabels[locale]}>
{languageIcons[locale]}
</span>{' '}
{languageLabels[locale]}
</Menu.Item>
))}
</Menu>
)
return (
<Dropdown overlay={langMenu} placement="bottomRight">
<span class={prefixCls}>
<Icon type="global" title={i18nRender('navBar.lang')} />
</span>
</Dropdown>
)
}
}
export default SelectLang

View File

@@ -0,0 +1,31 @@
@import "~ant-design-vue/es/style/themes/default";
@header-menu-prefix-cls: ~'@{ant-prefix}-pro-header-menu';
@header-drop-down-prefix-cls: ~'@{ant-prefix}-pro-drop-down';
.@{header-menu-prefix-cls} {
.anticon {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 160px;
}
}
.@{header-drop-down-prefix-cls} {
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
> i {
font-size: 16px !important;
transform: none !important;
svg {
position: relative;
top: -1px;
}
}
}

View File

@@ -0,0 +1,343 @@
<template>
<div class="setting-drawer">
<a-drawer
width="300"
placement="right"
@close="onClose"
:closable="false"
:visible="visible"
:drawer-style="{ position: 'absolute' }"
style="position: absolute"
>
<div class="setting-drawer-index-content">
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">整体风格设置</h3>
<div class="setting-drawer-index-blockChecbox">
<a-tooltip>
<template slot="title">
暗色菜单风格1
</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" alt="dark">
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
<a-tooltip>
<template slot="title">
亮色菜单风格
</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" alt="light">
<div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
</div>
</div>
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">主题色</h3>
<div style="height: 20px">
<a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
<template slot="title">
{{ item.key }}
</template>
<a-tag :color="item.color" @click="changeColor(item.color)">
<a-icon type="check" v-if="item.color === primaryColor"></a-icon>
</a-tag>
</a-tooltip>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">导航模式</h3>
<div class="setting-drawer-index-blockChecbox">
<a-tooltip>
<template slot="title">
侧边栏导航
</template>
<div class="setting-drawer-index-item" @click="handleLayout('sidemenu')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" alt="sidemenu">
<div class="setting-drawer-index-selectIcon" v-if="layoutMode === 'sidemenu'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
<a-tooltip>
<template slot="title">
顶部栏导航
</template>
<div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" alt="topmenu">
<div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
</div>
<div :style="{ marginTop: '24px' }">
<a-list :split="false">
<a-list-item>
<a-tooltip slot="actions">
<template slot="title">
该设定仅 [顶部栏导航] 时有效
</template>
<a-select size="small" style="width: 80px;" :defaultValue="contentWidth" @change="handleContentWidthChange">
<a-select-option value="Fixed">固定</a-select-option>
<a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'">流式</a-select-option>
</a-select>
</a-tooltip>
<a-list-item-meta>
<div slot="title">内容区域宽度</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" />
<a-list-item-meta>
<div slot="title">固定 Header</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch slot="actions" size="small" :disabled="!fixedHeader" :defaultChecked="autoHideHeader" @change="handleFixedHeaderHidden" />
<a-list-item-meta>
<a-tooltip slot="title" placement="left">
<template slot="title">固定 Header 时可配置</template>
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
</a-tooltip>
</a-list-item-meta>
</a-list-item>
<a-list-item >
<a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :defaultChecked="fixSiderbar" @change="handleFixSiderbar" />
<a-list-item-meta>
<div slot="title" :style="{ textDecoration: layoutMode === 'topmenu' ? 'line-through' : 'unset' }">固定侧边菜单</div>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">其他设置</h3>
<div>
<a-list :split="false">
<a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="colorWeak" @change="onColorWeak" />
<a-list-item-meta>
<div slot="title">色弱模式</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="multiTab" @change="onMultiTab" />
<a-list-item-meta>
<div slot="title">多页签模式</div>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<a-button
@click="doCopy"
icon="copy"
block
>拷贝设置</a-button>
<a-alert type="warning" :style="{ marginTop: '24px' }">
<span slot="message">
配置栏只在开发环境用于预览生产环境不会展现请手动修改配置文件修改配置文件后需要清空本地缓存和LocalStorage
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/defaultSettings.js" target="_blank">src/config/defaultSettings.js</a>
</span>
</a-alert>
</div>
</div>
<div class="setting-drawer-index-handle" @click="toggle" slot="handle">
<a-icon type="setting" v-if="!visible"/>
<a-icon type="close" v-else/>
</div>
</a-drawer>
</div>
</template>
<script>
import SettingItem from './SettingItem'
import config from '@/config/defaultSettings'
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
export default {
components: {
SettingItem
},
mixins: [],
data () {
return {
visible: false,
colorList
}
},
watch: {
},
mounted () {
updateTheme(this.primaryColor)
if (this.colorWeak !== config.colorWeak) {
updateColorWeak(this.colorWeak)
}
},
methods: {
showDrawer () {
this.visible = true
},
onClose () {
this.visible = false
},
toggle () {
this.visible = !this.visible
},
onColorWeak (checked) {
this.$store.dispatch('ToggleWeak', checked)
updateColorWeak(checked)
},
onMultiTab (checked) {
this.$store.dispatch('ToggleMultiTab', checked)
},
handleMenuTheme (theme) {
this.$store.dispatch('ToggleTheme', theme)
},
doCopy () {
// get current settings from mixin or this.$store.state.app, pay attention to the property name
const text = `export default {
primaryColor: '${this.primaryColor}', // primary color of ant design
navTheme: '${this.navTheme}', // theme for nav menu
layout: '${this.layoutMode}', // nav menu position: sidemenu or topmenu
contentWidth: '${this.contentWidth}', // layout of content: Fluid or Fixed, only works when layout is topmenu
fixedHeader: ${this.fixedHeader}, // sticky header
fixSiderbar: ${this.fixSiderbar}, // sticky siderbar
autoHideHeader: ${this.autoHideHeader}, // auto hide header
colorWeak: ${this.colorWeak},
multiTab: ${this.multiTab},
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true'
}`
this.$copyText(text).then(message => {
console.log('copy', message)
this.$message.success('复制完毕')
}).catch(err => {
console.log('copy.err', err)
this.$message.error('复制失败')
})
},
handleLayout (mode) {
this.$store.dispatch('ToggleLayoutMode', mode)
// 因为顶部菜单不能固定左侧菜单栏,所以强制关闭
this.handleFixSiderbar(false)
},
handleContentWidthChange (type) {
this.$store.dispatch('ToggleContentWidth', type)
},
changeColor (color) {
if (this.primaryColor !== color) {
this.$store.dispatch('ToggleColor', color)
updateTheme(color)
}
},
handleFixedHeader (fixed) {
this.$store.dispatch('ToggleFixedHeader', fixed)
},
handleFixedHeaderHidden (autoHidden) {
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
},
handleFixSiderbar (fixed) {
if (this.layoutMode === 'topmenu') {
this.$store.dispatch('ToggleFixSiderbar', false)
return
}
this.$store.dispatch('ToggleFixSiderbar', fixed)
}
}
}
</script>
<style lang="less" scoped>
.setting-drawer-index-content {
.setting-drawer-index-blockChecbox {
display: flex;
.setting-drawer-index-item {
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
img {
width: 48px;
}
.setting-drawer-index-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: #1890ff;
font-size: 14px;
font-weight: 700;
}
}
}
.setting-drawer-theme-color-colorBlock {
width: 20px;
height: 20px;
border-radius: 2px;
float: left;
cursor: pointer;
margin-right: 8px;
padding-left: 0px;
padding-right: 0px;
text-align: center;
color: #fff;
font-weight: 700;
i {
font-size: 14px;
}
}
}
.setting-drawer-index-handle {
position: absolute;
top: 240px;
background: #1890ff;
width: 48px;
height: 48px;
right: 300px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
pointer-events: auto;
z-index: 1001;
text-align: center;
font-size: 16px;
border-radius: 4px 0 0 4px;
i {
color: rgb(255, 255, 255);
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="setting-drawer-index-item">
<h3 class="setting-drawer-index-title">{{ title }}</h3>
<slot></slot>
<a-divider v-if="divider"/>
</div>
</template>
<script>
export default {
name: 'SettingItem',
props: {
title: {
type: String,
default: ''
},
divider: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
.setting-drawer-index-item {
margin-bottom: 24px;
.setting-drawer-index-title {
font-size: 14px;
color: rgba(0, 0, 0, .85);
line-height: 22px;
margin-bottom: 12px;
}
}
</style>

View File

@@ -0,0 +1,2 @@
import SettingDrawer from './SettingDrawer'
export default SettingDrawer

View File

@@ -0,0 +1,48 @@
import message from 'ant-design-vue/es/message'
// import defaultSettings from '../defaultSettings';
import themeColor from './themeColor.js'
// let lessNodesAppended
const colorList = [
{
key: '薄暮', color: '#F5222D'
},
{
key: '火山', color: '#FA541C'
},
{
key: '日暮', color: '#FAAD14'
},
{
key: '明青', color: '#13C2C2'
},
{
key: '极光绿', color: '#52C41A'
},
{
key: '拂晓蓝(默认)', color: '#1890FF'
},
{
key: '极客蓝', color: '#2F54EB'
},
{
key: '酱紫', color: '#722ED1'
}
]
const updateTheme = newPrimaryColor => {
const hideMessage = message.loading('正在切换主题!', 0)
themeColor.changeColor(newPrimaryColor).finally(() => {
setTimeout(() => {
hideMessage()
}, 10)
})
}
const updateColorWeak = colorWeak => {
// document.body.className = colorWeak ? 'colorWeak' : '';
const app = document.body.querySelector('#app')
colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak')
}
export { updateTheme, colorList, updateColorWeak }

View File

@@ -0,0 +1,24 @@
import client from 'webpack-theme-color-replacer/client'
import generate from '@ant-design/colors/lib/generate'
export default {
getAntdSerials (color) {
// 淡化即less的tint
const lightens = new Array(9).fill().map((t, i) => {
return client.varyColor.lighten(color, i / 10)
})
// colorPalette变换得到颜色值
const colorPalettes = generate(color)
const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',')
return lightens.concat(colorPalettes).concat(rgb)
},
changeColor (newColor) {
var options = {
newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
changeUrl (cssUrl) {
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
}
}
return client.changer.changeColor(options, Promise)
}
}

View File

@@ -0,0 +1,122 @@
<template>
<div :class="[prefixCls, lastCls, blockCls, gridCls]">
<div v-if="title" class="antd-pro-components-standard-form-row-index-label">
<span>{{ title }}</span>
</div>
<div class="antd-pro-components-standard-form-row-index-content">
<slot></slot>
</div>
</div>
</template>
<script>
const classes = [
'antd-pro-components-standard-form-row-index-standardFormRowBlock',
'antd-pro-components-standard-form-row-index-standardFormRowGrid',
'antd-pro-components-standard-form-row-index-standardFormRowLast'
]
export default {
name: 'StandardFormRow',
props: {
prefixCls: {
type: String,
default: 'antd-pro-components-standard-form-row-index-standardFormRow'
},
title: {
type: String,
default: undefined
},
last: {
type: Boolean
},
block: {
type: Boolean
},
grid: {
type: Boolean
}
},
computed: {
lastCls () {
return this.last ? classes[2] : null
},
blockCls () {
return this.block ? classes[0] : null
},
gridCls () {
return this.grid ? classes[1] : null
}
}
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-standard-form-row-index-standardFormRow {
display: flex;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px dashed @border-color-split;
/deep/ .ant-form-item {
margin-right: 24px;
}
/deep/ .ant-form-item-label label {
margin-right: 0;
color: @text-color;
}
/deep/ .ant-form-item-label,
.ant-form-item-control {
padding: 0;
line-height: 32px;
}
.antd-pro-components-standard-form-row-index-label {
flex: 0 0 auto;
margin-right: 24px;
color: @heading-color;
font-size: @font-size-base;
text-align: right;
& > span {
display: inline-block;
height: 32px;
line-height: 32px;
&::after {
content: '';
}
}
}
.antd-pro-components-standard-form-row-index-content {
flex: 1 1 0;
/deep/ .ant-form-item:last-child {
margin-right: 0;
}
}
&.antd-pro-components-standard-form-row-index-standardFormRowLast {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
&.antd-pro-components-standard-form-row-index-standardFormRowBlock {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
}
&.antd-pro-components-standard-form-row-index-standardFormRowGrid {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
/deep/ .ant-form-item-label {
float: left;
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
import StandardFormRow from './StandardFormRow'
export default StandardFormRow

View File

@@ -0,0 +1,45 @@
import { Tag } from 'ant-design-vue'
const { CheckableTag } = Tag
export default {
name: 'TagSelectOption',
props: {
prefixCls: {
type: String,
default: 'ant-pro-tag-select-option'
},
value: {
type: [String, Number, Object],
default: ''
},
checked: {
type: Boolean,
default: false
}
},
data () {
return {
localChecked: this.checked || false
}
},
watch: {
'checked' (val) {
this.localChecked = val
},
'$parent.items': {
handler: function (val) {
this.value && val.hasOwnProperty(this.value) && (this.localChecked = val[this.value])
},
deep: true
}
},
render () {
const { $slots, value } = this
const onChange = (checked) => {
this.$emit('change', { value, checked })
}
return (<CheckableTag key={value} vModel={this.localChecked} onChange={onChange}>
{$slots.default}
</CheckableTag>)
}
}

View File

@@ -0,0 +1,113 @@
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import Option from './TagSelectOption.jsx'
import { filterEmpty } from '@/components/_util/util'
export default {
Option,
name: 'TagSelect',
model: {
prop: 'checked',
event: 'change'
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-tag-select'
},
defaultValue: {
type: PropTypes.array,
default: null
},
value: {
type: PropTypes.array,
default: null
},
expandable: {
type: Boolean,
default: false
},
hideCheckAll: {
type: Boolean,
default: false
}
},
data () {
return {
expand: false,
localCheckAll: false,
items: this.getItemsKey(filterEmpty(this.$slots.default)),
val: this.value || this.defaultValue || []
}
},
methods: {
onChange (checked) {
const key = Object.keys(this.items).filter(key => key === checked.value)
this.items[key] = checked.checked
const bool = Object.values(this.items).lastIndexOf(false)
if (bool === -1) {
this.localCheckAll = true
} else {
this.localCheckAll = false
}
},
onCheckAll (checked) {
Object.keys(this.items).forEach(v => {
this.items[v] = checked.checked
})
this.localCheckAll = checked.checked
},
getItemsKey (items) {
const totalItem = {}
items.forEach(item => {
totalItem[item.componentOptions.propsData && item.componentOptions.propsData.value] = false
})
return totalItem
},
// CheckAll Button
renderCheckAll () {
const props = {
on: {
change: (checked) => {
this.onCheckAll(checked)
checked.value = 'total'
this.$emit('change', checked)
}
}
}
const checkAllElement = <Option key={'total'} checked={this.localCheckAll} {...props}>All</Option>
return !this.hideCheckAll && checkAllElement || null
},
// expandable
renderExpandable () {
},
// render option
renderTags (items) {
const listeners = {
change: (checked) => {
this.onChange(checked)
this.$emit('change', checked)
}
}
return items.map(vnode => {
const options = vnode.componentOptions
options.listeners = listeners
return vnode
})
}
},
render () {
const { $props: { prefixCls } } = this
const classString = {
[`${prefixCls}`]: true
}
const tagItems = filterEmpty(this.$slots.default)
return (
<div class={classString}>
{this.renderCheckAll()}
{this.renderTags(tagItems)}
</div>
)
}
}

View File

@@ -0,0 +1,69 @@
import './style.less'
import { getStrFullLength, cutStrByFullLength } from '../_util/util'
import Input from 'ant-design-vue/es/input'
const TextArea = Input.TextArea
export default {
name: 'LimitTextArea',
model: {
prop: 'value',
event: 'change'
},
props: Object.assign({}, TextArea.props, {
prefixCls: {
type: String,
default: 'ant-textarea-limit'
},
// eslint-disable-next-line
value: {
type: String
},
limit: {
type: Number,
default: 200
}
}),
data () {
return {
currentLimit: 0
}
},
watch: {
value (val) {
this.calcLimitNum(val)
}
},
created () {
this.calcLimitNum(this.value)
},
methods: {
handleChange (e) {
const value = e.target.value
const len = getStrFullLength(value)
if (len <= this.limit) {
this.currentLimit = len
this.$emit('change', value)
return
} else {
const str = cutStrByFullLength(value, this.limit)
this.currentLimit = getStrFullLength(str)
this.$emit('change', str)
}
console.error('limit out! currentLimit:', this.currentLimit)
},
calcLimitNum (val) {
const len = getStrFullLength(val)
this.currentLimit = len
}
},
render () {
const { prefixCls, ...props } = this.$props
return (
<div class={this.prefixCls}>
<TextArea {...{ props }} value={this.value} onChange={this.handleChange}>
</TextArea>
<span class="limit">{this.currentLimit}/{this.limit}</span>
</div>
)
}
}

View File

@@ -0,0 +1,12 @@
.ant-textarea-limit {
position: relative;
.limit {
position: absolute;
color: #909399;
background: #fff;
font-size: 12px;
bottom: 5px;
right: 10px;
}
}

View File

@@ -0,0 +1,124 @@
import { Menu, Icon, Input } from 'ant-design-vue'
const { Item, ItemGroup, SubMenu } = Menu
const { Search } = Input
export default {
name: 'Tree',
props: {
dataSource: {
type: Array,
required: true
},
openKeys: {
type: Array,
default: () => []
},
search: {
type: Boolean,
default: false
}
},
created () {
this.localOpenKeys = this.openKeys.slice(0)
},
data () {
return {
localOpenKeys: []
}
},
methods: {
handlePlus (item) {
this.$emit('add', item)
},
handleTitleClick (...args) {
this.$emit('titleClick', { args })
},
renderSearch () {
return (
<Search
placeholder="input search text"
style="width: 100%; margin-bottom: 1rem"
/>
)
},
renderIcon (icon) {
return icon && (<Icon type={icon} />) || null
},
renderMenuItem (item) {
return (
<Item key={item.key}>
{ this.renderIcon(item.icon) }
{ item.title }
<a class="btn" style="width: 20px;z-index:1300" {...{ on: { click: () => this.handlePlus(item) } }}><a-icon type="plus"/></a>
</Item>
)
},
renderItem (item) {
return item.children ? this.renderSubItem(item, item.key) : this.renderMenuItem(item, item.key)
},
renderItemGroup (item) {
const childrenItems = item.children.map(o => {
return this.renderItem(o, o.key)
})
return (
<ItemGroup key={item.key}>
<template slot="title">
<span>{ item.title }</span>
<a-dropdown>
<a class="btn"><a-icon type="ellipsis" /></a>
<a-menu slot="overlay">
<a-menu-item key="1">新增</a-menu-item>
<a-menu-item key="2">合并</a-menu-item>
<a-menu-item key="3">移除</a-menu-item>
</a-menu>
</a-dropdown>
</template>
{ childrenItems }
</ItemGroup>
)
},
renderSubItem (item, key) {
const childrenItems = item.children && item.children.map(o => {
return this.renderItem(o, o.key)
})
const title = (
<span slot="title">
{ this.renderIcon(item.icon) }
<span>{ item.title }</span>
</span>
)
if (item.group) {
return this.renderItemGroup(item)
}
// titleClick={this.handleTitleClick(item)}
return (
<SubMenu key={key}>
{ title }
{ childrenItems }
</SubMenu>
)
}
},
render () {
const { dataSource, search } = this.$props
// this.localOpenKeys = openKeys.slice(0)
const list = dataSource.map(item => {
return this.renderItem(item)
})
return (
<div class="tree-wrapper">
{ search ? this.renderSearch() : null }
<Menu mode="inline" class="custom-tree" {...{ on: { click: item => this.$emit('click', item), 'update:openKeys': val => { this.localOpenKeys = val } } }} openKeys={this.localOpenKeys}>
{ list }
</Menu>
</div>
)
}
}

View File

@@ -0,0 +1,41 @@
<template>
<div :class="[prefixCls, reverseColor && 'reverse-color' ]">
<span>
<slot name="term"></slot>
<span class="item-text">
<slot></slot>
</span>
</span>
<span :class="[flag]"><a-icon :type="`caret-${flag}`"/></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
prefixCls: {
type: String,
default: 'ant-pro-trend'
},
/**
* 上升下降标识up|down
*/
flag: {
type: String,
required: true
},
/**
* 颜色反转
*/
reverseColor: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

View File

@@ -0,0 +1,3 @@
import Trend from './Trend.vue'
export default Trend

View File

@@ -0,0 +1,42 @@
@import "../index";
@trend-prefix-cls: ~"@{ant-pro-prefix}-trend";
.@{trend-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
.up,
.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.item-text {
display: inline-block;
margin-left: 8px;
color: rgba(0,0,0,.85);
}
.up {
color: @red-6;
}
.down {
color: @green-6;
top: -1px;
}
&.reverse-color .up {
color: @green-6;
}
&.reverse-color .down {
color: @red-6;
}
}

View File

@@ -0,0 +1,45 @@
# Trend 趋势标记
趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。
引用方式:
```javascript
import Trend from '@/components/Trend'
export default {
components: {
Trend
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<trend flag="up">5%</trend>
```
```html
<trend flag="up">
<span slot="term">工资</span>
5%
</trend>
```
```html
<trend flag="up" term="工资">5%</trend>
```
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| flag | 上升下降标识:`up|down` | string | - |
| reverseColor | 颜色反转 | Boolean | false |

View File

@@ -0,0 +1,46 @@
/**
* components util
*/
/**
* 清理空值,对象
* @param children
* @returns {*[]}
*/
export function filterEmpty (children = []) {
return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
}
/**
* 获取字符串长度,英文字符 长度1中文字符长度2
* @param {*} str
*/
export const getStrFullLength = (str = '') =>
str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
return pre + 1
}
return pre + 2
}, 0)
/**
* 截取字符串,根据 maxLength 截取后返回
* @param {*} str
* @param {*} maxLength
*/
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
showLength += 2
}
if (showLength <= maxLength) {
return pre + cur
}
return pre
}, '')
}

View File

@@ -0,0 +1,37 @@
// pro components
import AvatarList from '@/components/AvatarList'
import Ellipsis from '@/components/Ellipsis'
import FooterToolbar from '@/components/FooterToolbar'
import NumberInfo from '@/components/NumberInfo'
import Tree from '@/components/Tree/Tree'
import Trend from '@/components/Trend'
import MultiTab from '@/components/MultiTab'
import IconSelector from '@/components/IconSelector'
import TagSelect from '@/components/TagSelect'
import StandardFormRow from '@/components/StandardFormRow'
import ArticleListContent from '@/components/ArticleListContent'
import Dialog from '@/components/Dialog'
import SettingDrawer from '@/components/SettingDrawer'
import ProLayout from '@/components/ProLayout'
export {
AvatarList,
Trend,
Ellipsis,
FooterToolbar,
NumberInfo,
Tree,
MultiTab,
IconSelector,
TagSelect,
StandardFormRow,
ArticleListContent,
Dialog,
SettingDrawer,
ProLayout
}

View File

@@ -0,0 +1,6 @@
@import "~ant-design-vue/lib/style/index";
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;
@ant-global-sider-zindex : 106;
@ant-global-header-zindex : 105;

View File

@@ -0,0 +1,3 @@
<template>
<div>加载失败</div>
</template>

Some files were not shown because too many files have changed in this diff Show More