mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-21 16:36:48 +08:00
tt
This commit is contained in:
@@ -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>
|
||||
3
hotgo-web/src/components/ArticleListContent/index.js
Normal file
3
hotgo-web/src/components/ArticleListContent/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ArticleListContent from './ArticleListContent'
|
||||
|
||||
export default ArticleListContent
|
||||
24
hotgo-web/src/components/AvatarList/Item.jsx
Normal file
24
hotgo-web/src/components/AvatarList/Item.jsx
Normal 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
|
||||
72
hotgo-web/src/components/AvatarList/List.jsx
Normal file
72
hotgo-web/src/components/AvatarList/List.jsx
Normal 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
|
||||
9
hotgo-web/src/components/AvatarList/index.js
Normal file
9
hotgo-web/src/components/AvatarList/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import AvatarList from './List'
|
||||
import Item from './Item'
|
||||
|
||||
export {
|
||||
AvatarList,
|
||||
Item as AvatarListItem
|
||||
}
|
||||
|
||||
export default AvatarList
|
||||
60
hotgo-web/src/components/AvatarList/index.less
Normal file
60
hotgo-web/src/components/AvatarList/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
hotgo-web/src/components/AvatarList/index.md
Normal file
64
hotgo-web/src/components/AvatarList/index.md
Normal 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 | - |
|
||||
|
||||
113
hotgo-web/src/components/Dialog.js
Normal file
113
hotgo-web/src/components/Dialog.js
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
64
hotgo-web/src/components/Ellipsis/Ellipsis.vue
Normal file
64
hotgo-web/src/components/Ellipsis/Ellipsis.vue
Normal 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>
|
||||
3
hotgo-web/src/components/Ellipsis/index.js
Normal file
3
hotgo-web/src/components/Ellipsis/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export default Ellipsis
|
||||
38
hotgo-web/src/components/Ellipsis/index.md
Normal file
38
hotgo-web/src/components/Ellipsis/index.md
Normal 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 | -
|
||||
47
hotgo-web/src/components/FooterToolbar/FooterToolBar.vue
Normal file
47
hotgo-web/src/components/FooterToolbar/FooterToolBar.vue
Normal 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>
|
||||
4
hotgo-web/src/components/FooterToolbar/index.js
Normal file
4
hotgo-web/src/components/FooterToolbar/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import FooterToolBar from './FooterToolBar'
|
||||
import './index.less'
|
||||
|
||||
export default FooterToolBar
|
||||
23
hotgo-web/src/components/FooterToolbar/index.less
Normal file
23
hotgo-web/src/components/FooterToolbar/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
48
hotgo-web/src/components/FooterToolbar/index.md
Normal file
48
hotgo-web/src/components/FooterToolbar/index.md
Normal 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 | -
|
||||
|
||||
21
hotgo-web/src/components/GlobalFooter/index.vue
Normal file
21
hotgo-web/src/components/GlobalFooter/index.vue
Normal 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>
|
||||
83
hotgo-web/src/components/GlobalHeader/AvatarDropdown.vue
Normal file
83
hotgo-web/src/components/GlobalHeader/AvatarDropdown.vue
Normal 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>
|
||||
115
hotgo-web/src/components/GlobalHeader/PlatformVersion.vue
Normal file
115
hotgo-web/src/components/GlobalHeader/PlatformVersion.vue
Normal 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>
|
||||
251
hotgo-web/src/components/GlobalHeader/RightContent.vue
Normal file
251
hotgo-web/src/components/GlobalHeader/RightContent.vue
Normal 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>
|
||||
44
hotgo-web/src/components/GridContent/index.js
Normal file
44
hotgo-web/src/components/GridContent/index.js
Normal 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
|
||||
14
hotgo-web/src/components/GridContent/index.less
Normal file
14
hotgo-web/src/components/GridContent/index.less
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
187
hotgo-web/src/components/IconSelector/IconDetail.vue
Normal file
187
hotgo-web/src/components/IconSelector/IconDetail.vue
Normal 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>
|
||||
109
hotgo-web/src/components/IconSelector/IconSelector.vue
Normal file
109
hotgo-web/src/components/IconSelector/IconSelector.vue
Normal 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>
|
||||
48
hotgo-web/src/components/IconSelector/README.md
Normal file
48
hotgo-web/src/components/IconSelector/README.md
Normal 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 | - |
|
||||
36
hotgo-web/src/components/IconSelector/icons.js
Normal file
36
hotgo-web/src/components/IconSelector/icons.js
Normal 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']
|
||||
}
|
||||
]
|
||||
2
hotgo-web/src/components/IconSelector/index.js
Normal file
2
hotgo-web/src/components/IconSelector/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import IconSelector from './IconSelector'
|
||||
export default IconSelector
|
||||
175
hotgo-web/src/components/MultiTab/MultiTab.vue
Normal file
175
hotgo-web/src/components/MultiTab/MultiTab.vue
Normal 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>
|
||||
2
hotgo-web/src/components/MultiTab/events.js
Normal file
2
hotgo-web/src/components/MultiTab/events.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Vue from 'vue'
|
||||
export default new Vue()
|
||||
40
hotgo-web/src/components/MultiTab/index.js
Normal file
40
hotgo-web/src/components/MultiTab/index.js
Normal 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
|
||||
31
hotgo-web/src/components/MultiTab/index.less
Normal file
31
hotgo-web/src/components/MultiTab/index.less
Normal 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%);
|
||||
}
|
||||
76
hotgo-web/src/components/NProgress/nprogress.less
Normal file
76
hotgo-web/src/components/NProgress/nprogress.less
Normal 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); }
|
||||
}
|
||||
|
||||
90
hotgo-web/src/components/NoticeIcon/NoticeIcon.vue
Normal file
90
hotgo-web/src/components/NoticeIcon/NoticeIcon.vue
Normal 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>
|
||||
2
hotgo-web/src/components/NoticeIcon/index.js
Normal file
2
hotgo-web/src/components/NoticeIcon/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import NoticeIcon from './NoticeIcon'
|
||||
export default NoticeIcon
|
||||
54
hotgo-web/src/components/NumberInfo/NumberInfo.vue
Normal file
54
hotgo-web/src/components/NumberInfo/NumberInfo.vue
Normal 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>
|
||||
3
hotgo-web/src/components/NumberInfo/index.js
Normal file
3
hotgo-web/src/components/NumberInfo/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import NumberInfo from './NumberInfo'
|
||||
|
||||
export default NumberInfo
|
||||
55
hotgo-web/src/components/NumberInfo/index.less
Normal file
55
hotgo-web/src/components/NumberInfo/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
hotgo-web/src/components/NumberInfo/index.md
Normal file
43
hotgo-web/src/components/NumberInfo/index.md
Normal 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
|
||||
114
hotgo-web/src/components/Other/CarbonAds.vue
Normal file
114
hotgo-web/src/components/Other/CarbonAds.vue
Normal 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>
|
||||
106
hotgo-web/src/components/PageLoading/index.jsx
Normal file
106
hotgo-web/src/components/PageLoading/index.jsx
Normal 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
|
||||
}
|
||||
187
hotgo-web/src/components/ProLayout/BasicLayout.jsx
Normal file
187
hotgo-web/src/components/ProLayout/BasicLayout.jsx
Normal 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
|
||||
101
hotgo-web/src/components/ProLayout/BasicLayout.less
Normal file
101
hotgo-web/src/components/ProLayout/BasicLayout.less
Normal 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;
|
||||
}
|
||||
}
|
||||
9
hotgo-web/src/components/ProLayout/BlockLayout.jsx
Normal file
9
hotgo-web/src/components/ProLayout/BlockLayout.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const BlockLayout = {
|
||||
name: 'BlockLayout',
|
||||
functional: true,
|
||||
render (createElement, content) {
|
||||
return content.children
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockLayout
|
||||
109
hotgo-web/src/components/ProLayout/Header.jsx
Normal file
109
hotgo-web/src/components/ProLayout/Header.jsx
Normal 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
|
||||
91
hotgo-web/src/components/ProLayout/Header.less
Normal file
91
hotgo-web/src/components/ProLayout/Header.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
hotgo-web/src/components/ProLayout/PageView.jsx
Normal file
14
hotgo-web/src/components/ProLayout/PageView.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PageHeaderWrapper } from './components'
|
||||
|
||||
const PageView = {
|
||||
name: 'PageView',
|
||||
render () {
|
||||
return (
|
||||
<PageHeaderWrapper>
|
||||
<router-view />
|
||||
</PageHeaderWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PageView
|
||||
44
hotgo-web/src/components/ProLayout/WrapContent.jsx
Normal file
44
hotgo-web/src/components/ProLayout/WrapContent.jsx
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
name: 'VueFragment',
|
||||
functional: true,
|
||||
render (h, ctx) {
|
||||
return ctx.children.length > 1 ? h('div', {}, ctx.children) : ctx.children
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
import BaseMenu from './BaseMenu'
|
||||
export default BaseMenu
|
||||
@@ -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
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
15
hotgo-web/src/components/ProLayout/components/index.js
Normal file
15
hotgo-web/src/components/ProLayout/components/index.js
Normal 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
|
||||
}
|
||||
9
hotgo-web/src/components/ProLayout/index.js
Normal file
9
hotgo-web/src/components/ProLayout/index.js
Normal 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'
|
||||
38
hotgo-web/src/components/ProLayout/utils/dynamicTheme.js
Normal file
38
hotgo-web/src/components/ProLayout/utils/dynamicTheme.js
Normal 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')
|
||||
}
|
||||
62
hotgo-web/src/components/ProLayout/utils/request.js
Normal file
62
hotgo-web/src/components/ProLayout/utils/request.js
Normal 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
|
||||
64
hotgo-web/src/components/ProLayout/utils/util.js
Normal file
64
hotgo-web/src/components/ProLayout/utils/util.js
Normal 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
|
||||
}
|
||||
63
hotgo-web/src/components/Search/GlobalSearch.jsx
Normal file
63
hotgo-web/src/components/Search/GlobalSearch.jsx
Normal 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
|
||||
25
hotgo-web/src/components/Search/index.less
Normal file
25
hotgo-web/src/components/Search/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
hotgo-web/src/components/SelectLang/index.jsx
Normal file
54
hotgo-web/src/components/SelectLang/index.jsx
Normal 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
|
||||
31
hotgo-web/src/components/SelectLang/index.less
Normal file
31
hotgo-web/src/components/SelectLang/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
343
hotgo-web/src/components/SettingDrawer/SettingDrawer.vue
Normal file
343
hotgo-web/src/components/SettingDrawer/SettingDrawer.vue
Normal 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>
|
||||
38
hotgo-web/src/components/SettingDrawer/SettingItem.vue
Normal file
38
hotgo-web/src/components/SettingDrawer/SettingItem.vue
Normal 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>
|
||||
2
hotgo-web/src/components/SettingDrawer/index.js
Normal file
2
hotgo-web/src/components/SettingDrawer/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import SettingDrawer from './SettingDrawer'
|
||||
export default SettingDrawer
|
||||
48
hotgo-web/src/components/SettingDrawer/settingConfig.js
Normal file
48
hotgo-web/src/components/SettingDrawer/settingConfig.js
Normal 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 }
|
||||
24
hotgo-web/src/components/SettingDrawer/themeColor.js
Normal file
24
hotgo-web/src/components/SettingDrawer/themeColor.js
Normal 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)
|
||||
}
|
||||
}
|
||||
122
hotgo-web/src/components/StandardFormRow/StandardFormRow.vue
Normal file
122
hotgo-web/src/components/StandardFormRow/StandardFormRow.vue
Normal 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>
|
||||
3
hotgo-web/src/components/StandardFormRow/index.js
Normal file
3
hotgo-web/src/components/StandardFormRow/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import StandardFormRow from './StandardFormRow'
|
||||
|
||||
export default StandardFormRow
|
||||
45
hotgo-web/src/components/TagSelect/TagSelectOption.jsx
Normal file
45
hotgo-web/src/components/TagSelect/TagSelectOption.jsx
Normal 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>)
|
||||
}
|
||||
}
|
||||
113
hotgo-web/src/components/TagSelect/index.jsx
Normal file
113
hotgo-web/src/components/TagSelect/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
69
hotgo-web/src/components/TextArea/index.jsx
Normal file
69
hotgo-web/src/components/TextArea/index.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
12
hotgo-web/src/components/TextArea/style.less
Normal file
12
hotgo-web/src/components/TextArea/style.less
Normal file
@@ -0,0 +1,12 @@
|
||||
.ant-textarea-limit {
|
||||
position: relative;
|
||||
|
||||
.limit {
|
||||
position: absolute;
|
||||
color: #909399;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
bottom: 5px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
124
hotgo-web/src/components/Tree/Tree.jsx
Normal file
124
hotgo-web/src/components/Tree/Tree.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
41
hotgo-web/src/components/Trend/Trend.vue
Normal file
41
hotgo-web/src/components/Trend/Trend.vue
Normal 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>
|
||||
3
hotgo-web/src/components/Trend/index.js
Normal file
3
hotgo-web/src/components/Trend/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Trend from './Trend.vue'
|
||||
|
||||
export default Trend
|
||||
42
hotgo-web/src/components/Trend/index.less
Normal file
42
hotgo-web/src/components/Trend/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
45
hotgo-web/src/components/Trend/index.md
Normal file
45
hotgo-web/src/components/Trend/index.md
Normal 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 |
|
||||
|
||||
46
hotgo-web/src/components/_util/util.js
Normal file
46
hotgo-web/src/components/_util/util.js
Normal 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
|
||||
}, '')
|
||||
}
|
||||
37
hotgo-web/src/components/index.js
Normal file
37
hotgo-web/src/components/index.js
Normal 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
|
||||
}
|
||||
6
hotgo-web/src/components/index.less
Normal file
6
hotgo-web/src/components/index.less
Normal 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;
|
||||
3
hotgo-web/src/components/pt/asyncComponent/Error.vue
Normal file
3
hotgo-web/src/components/pt/asyncComponent/Error.vue
Normal 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
Reference in New Issue
Block a user