JEECG-BOOT 2.0.2版本发布

This commit is contained in:
zhangdaihao
2019-07-05 15:38:38 +08:00
parent 2917239a9d
commit 31422b1ea8
424 changed files with 34593 additions and 20808 deletions

View File

@ -0,0 +1,46 @@
<template>
<tooltip v-if="tips !== ''">
<template slot="title">{{ tips }}</template>
<avatar :size="avatarSize" :src="src" />
</tooltip>
<avatar v-else :size="avatarSize" :src="src" />
</template>
<script>
import Avatar from 'ant-design-vue/es/avatar'
import Tooltip from 'ant-design-vue/es/tooltip'
export default {
name: "AvatarItem",
components: {
Avatar,
Tooltip
},
props: {
tips: {
type: String,
default: '',
required: false
},
src: {
type: String,
default: ''
}
},
data () {
return {
size: this.$parent.size
}
},
computed: {
avatarSize () {
return this.size !== 'mini' && this.size || 20
}
},
watch: {
'$parent.size' (val) {
this.size = val
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<!--
<template>
<div :class="[prefixCls]">
<ul>
<slot></slot>
<template v-for="item in filterEmpty($slots.default).slice(0, 3)"></template>
<template v-if="maxLength > 0 && filterEmpty($slots.default).length > maxLength">
<avatar-item :size="size">
<avatar :size="size !== 'mini' && size || 20" :style="excessItemsStyle">{{ `+${maxLength}` }}</avatar>
</avatar-item>
</template>
</ul>
</div>
</template>
-->
<script>
import Avatar from 'ant-design-vue/es/avatar'
import AvatarItem from './Item'
import { filterEmpty } from '@/components/_util/util'
export default {
AvatarItem,
name: "AvatarList",
components: {
Avatar,
AvatarItem
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-avatar-list'
},
/**
* 头像大小 类型: large、small 、mini, default
* 默认值: default
*/
size: {
type: [String, Number],
default: 'default'
},
/**
* 要显示的最大项目
*/
maxLength: {
type: Number,
default: 0
},
/**
* 多余的项目风格
*/
excessItemsStyle: {
type: Object,
default: () => {
return {
color: '#f56a00',
backgroundColor: '#fde3cf'
}
}
}
},
data () {
return {}
},
methods: {
getItems(items) {
const classString = {
[`${this.prefixCls}-item`]: true,
[`${this.size}`]: true
}
if (this.maxLength > 0) {
items = items.slice(0, this.maxLength)
items.push((<Avatar size={ this.size } style={ this.excessItemsStyle }>{`+${this.maxLength}`}</Avatar>))
}
const itemList = items.map((item) => (
<li class={ classString }>{ item }</li>
))
return itemList
}
},
render () {
const { prefixCls, size } = this.$props
const classString = {
[`${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={ classString }>
{ itemsDom }
</div>
)
}
}
</script>

View File

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

View File

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

View File

@ -0,0 +1,111 @@
<template>
<a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">{{ title }}</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total"><span>{{ total }}</span></div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot></slot>
</div>
</div>
<div class="chart-card-footer">
<div class="field">
<slot name="footer"></slot>
</div>
</div>
</a-card>
</template>
<script>
export default {
name: "ChartCard",
props: {
title: {
type: String,
default: ''
},
total: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="scss" scoped>
.chart-card-header {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, .45);
font-size: 14px;
line-height: 22px;
}
}
.chart-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.chart-card-footer {
border-top: 1px solid #e8e8e8;
padding-top: 9px;
margin-top: 8px;
> * {
position: relative;
}
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
}
}
.chart-card-content {
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
.content-fix {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<span>
{{ lastTime | format }}
</span>
</template>
<script>
function fixedZero(val) {
return val * 1 < 10 ? `0${val}` : val;
}
export default {
name: "CountDown",
props: {
format: {
type: Function,
default: undefined
},
target: {
type: [Date, Number],
required: true,
},
onEnd: {
type: Function,
default: () => {
}
}
},
data() {
return {
dateTime: '0',
originTargetTime: 0,
lastTime: 0,
timer: 0,
interval: 1000
}
},
filters: {
format(time) {
const hours = 60 * 60 * 1000;
const minutes = 60 * 1000;
const h = Math.floor(time / hours);
const m = Math.floor((time - h * hours) / minutes);
const s = Math.floor((time - h * hours - m * minutes) / 1000);
return `${fixedZero(h)}:${fixedZero(m)}:${fixedZero(s)}`
}
},
created() {
this.initTime()
this.tick()
},
methods: {
initTime() {
let lastTime = 0;
let targetTime = 0;
this.originTargetTime = this.target
try {
if (Object.prototype.toString.call(this.target) === '[object Date]') {
targetTime = this.target
} else {
targetTime = new Date(this.target).getTime()
}
} catch (e) {
throw new Error('invalid target prop')
}
lastTime = targetTime - new Date().getTime();
this.lastTime = lastTime < 0 ? 0 : lastTime
},
tick() {
const {onEnd} = this
this.timer = setTimeout(() => {
if (this.lastTime < this.interval) {
clearTimeout(this.timer)
this.lastTime = 0
if (typeof onEnd === 'function') {
onEnd();
}
} else {
this.lastTime -= this.interval
this.tick()
}
}, this.interval)
}
},
beforeUpdate () {
if (this.originTargetTime !== this.target) {
this.initTime()
}
},
beforeDestroy() {
clearTimeout(this.timer)
}
}
</script>
<style scoped>
</style>

View File

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

View File

@ -0,0 +1,63 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/StringUtil'
/*
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) {
return (
<span>{ cutStrByFullLength(str, this.length) + '...' }</span>
)
},
getTooltip ( fullStr) {
return (
<Tooltip>
<template slot="title">{ fullStr }</template>
{ this.getStrDom(fullStr) }
</Tooltip>
)
}
},
render () {
const { tooltip, length } = this.$props
let str = this.$slots.default.map(vNode => vNode.text).join("")
const strDom = tooltip && getStrFullLength(str) > length ? this.getTooltip(str) : this.getStrDom(str);
return (
strDom
)
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
####1._util包:存放自定义函数 详细见代码注释
####2.AvatarList:显示头像群并支持tip用法参考src\views\Home.vue如下图
![输入图片说明](https://static.oschina.net/uploads/img/201904/12181253_O0Xi.png "在这里输入图片标题")
####3.chart包:存放各种图表相关的组件,条形图柱形图折线图等等 具体用法参考首页
####4.countDown包:一个倒计时组件用法参考home页,简单描述,该组件有3个属性,
target(时间/毫秒数)必填,
format(function,该方法接收一个毫秒数的参数,用于格式化显示当前倒计时时间)非必填,
onEnd倒计时结束触发函数
![输入图片说明](https://static.oschina.net/uploads/img/201904/12182046_mwqJ.png "在这里输入图片标题")
####5.dict包数据字典专用用法参考文件夹下readme文件
####6.Ellipsis包字符串截取组件,可以指定字符串的显示长度,并将全部内容显示到tip中,简单使用参考src\views\system\PermissionList.vue
####7.jeecg包该包下自定义了很多列表/表单中用到的组件 参考包下readme文件
####8.jeecgbiz包该包下定义了一些业务相关的组件比如选择用户弹框,根据部门选择用户等等
####9.layouts+page包系统页面布局相关组件比如登陆进去之后页面顶部显示什么底部显示什么菜单点击触发多个tab的布局等等 一般情况不需要修改
####10.menun包菜单组件俩个一个折叠菜单一个正常显示的菜单
####11.NumberInfo:数字信息显示组件 如下图
![输入图片说明](https://static.oschina.net/uploads/img/201904/12185858_uvJ5.png "在这里输入图片标题")
####12.online包该包下封装了online表单的相关组件,用于展示表单各种控件,验证表单等等,相关用法参考readme
####13.setting包该包下封装了首页风格切换等功能如下图
![输入图片说明](https://static.oschina.net/uploads/img/201904/12190520_jySG.png "在这里输入图片标题")
####14.table包一个二次封装的table组件,用于展示列表参考readme
####15.tools包
Breadcrumb.vue面包屑二次封装,支持路由跳转
DetailList.vue详情展示用法参考src\views\profile\advanced\Advanced.vue(效果如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12193954_Uar6.png "在这里输入图片标题")
````
个人认为该页面代码有两点值得学习:
1.vue provide/inject的使用
2.该页面css定义方式,只定义一个顶层class,其余样式都定义在其下,这样只要顶层class不和别的页面冲突,整个页面的样式都是唯一生效的
````
FooterToolBar.vue:fixed定位的底部通过是否定义内部控件的属性slot="extra"决定是左浮动或是右浮动
HeaderNotice.vue:首页通知(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12195340_fPe0.png "在这里输入图片标题")
HeaderInfo.vue:上下文字布局(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12195638_dG5o.png "在这里输入图片标题")
Logo.vue:首页左上侧的log图
![输入图片说明](https://static.oschina.net/uploads/img/201904/12200908_ihv3.png "在这里输入图片标题")
UserMenu.vue:首页右上侧的内容
![输入图片说明](https://static.oschina.net/uploads/img/201904/12201226_laQK.png "在这里输入图片标题")
####16.trend包 趋势显示组件(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12201600_Wo8K.png "在这里输入图片标题")

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
/**
* 获取字符串的长度ascii长度为1 中文长度为2
* @param str
* @returns {number}
*/
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)
/**
* 给定一个字符串和一个长度,将此字符串按指定长度截取
* @param str
* @param maxLength
* @returns {string}
*/
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
showLength += 2
}
if (showLength <= maxLength) {
return pre + cur
}
return pre
}, '')
}

View File

@ -0,0 +1,12 @@
/**
* components util
*/
/**
* 清理空值,对象
* @param children
* @returns {*[]}
*/
export function filterEmpty (children = []) {
return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
}

View File

@ -0,0 +1,88 @@
<template>
<div :style="{ padding: '0' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart ref="chart" :forceFit="true" :height="height" :data="dataSource" :scale="scale">
<v-tooltip :shared="false"/>
<v-axis/>
<v-line position="x*y" :size="lineSize" :color="lineColor"/>
<v-area position="x*y" :color="color"/>
</v-chart>
</div>
</template>
<script>
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
name: 'AreaChartTy',
props: {
// 图表数据
dataSource: {
type: Array,
required: true
},
// 图表标题
title: {
type: String,
default: ''
},
// x 轴别名
x: {
type: String,
default: 'x'
},
// y 轴别名
y: {
type: String,
default: 'y'
},
// Y轴最小值
min: {
type: Number,
default: 0
},
// Y轴最大值
max: {
type: Number,
default: null
},
// 图表高度
height: {
type: Number,
default: 254
},
// 线的粗细
lineSize: {
type: Number,
default: 2
},
// 面积的颜色
color: {
type: String,
default: ''
},
// 线的颜色
lineColor: {
type: String,
default: ''
}
},
computed: {
scale() {
return [
{ dataKey: 'x', title: this.x, alias: this.x },
{ dataKey: 'y', title: this.y, alias: this.y, min: this.min, max: this.max }
]
}
},
mounted() {
triggerWindowResizeEvent()
}
}
</script>
<style lang="scss" scoped>
@import "chart";
</style>

View File

@ -0,0 +1,50 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :forceFit="true" :height="height" :data="dataSource" :scale="scale" :padding="padding">
<v-tooltip/>
<v-axis/>
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
name: 'Bar',
props: {
dataSource: {
type: Array,
required: true
},
yaxisText: {
type: String,
default: 'y'
},
title: {
type: String,
default: ''
},
height: {
type: Number,
default: 254
}
},
data() {
return { padding: ['auto', 'auto', '40', '50'] }
},
computed: {
scale() {
return [{
dataKey: 'y',
alias: this.yaxisText
}]
}
},
mounted() {
triggerWindowResizeEvent()
}
}
</script>

View File

@ -0,0 +1,57 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :forceFit="true" :height="height" :data="data" :scale="scale">
<v-tooltip/>
<v-legend/>
<v-axis/>
<v-bar position="type*bar"/>
<v-line position="type*line" color="#2fc25b" :size="3"/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'BarMultid',
props: {
title: {
type: String,
default: ''
},
dataSource: {
type: Array,
default: () => [
{ type: '10:10', bar: 2, line: 2 },
{ type: '10:15', bar: 6, line: 3 },
{ type: '10:20', bar: 2, line: 5 },
{ type: '10:25', bar: 9, line: 1 },
{ type: '10:30', bar: 2, line: 3 },
{ type: '10:35', bar: 2, line: 1 },
{ type: '10:40', bar: 1, line: 2 }
]
},
height: {
type: Number,
default: 400
}
},
data() {
return {
scale: [{
dataKey: 'bar',
min: 0
}, {
dataKey: 'line',
min: 0
}]
}
},
computed: {
data() {
return this.dataSource
}
}
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :forceFit="true" :height="height" :data="data">
<v-tooltip />
<v-axis />
<v-legend />
<v-bar position="x*y" color="type" :adjust="adjust" />
</v-chart>
</div>
</template>
<script>
import { DataSet } from '@antv/data-set'
export default {
name: 'BarMultid',
props: {
title: {
type: String,
default: ''
},
dataSource:{
type: Array,
default: () => [
{ type: 'Jeecg', 'Jan.': 18.9, 'Feb.': 28.8, 'Mar.': 39.3, 'Apr.': 81.4, 'May': 47, 'Jun.': 20.3, 'Jul.': 24, 'Aug.': 35.6 },
{ type: 'Jeebt', 'Jan.': 12.4, 'Feb.': 23.2, 'Mar.': 34.5, 'Apr.': 99.7, 'May': 52.6, 'Jun.': 35.5, 'Jul.': 37.4, 'Aug.': 42.4 }
]
},
fields:{
type: Array,
default: () => ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.']
},
height: {
type: Number,
default: 254
}
},
data() {
return {
adjust: [{
type: 'dodge',
marginRatio: 1 / 32
}]
}
},
computed: {
data() {
const dv = new DataSet.View().source(this.dataSource)
dv.transform({
type: 'fold',
fields: this.fields,
key: 'x',
value: 'y'
})
// bar 使用不了 - 和 / 所以替换下
return dv.rows.map(row => {
row.x = row.x.replace(/[-/]/g, '_')
return row
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,187 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<v-chart :forceFit="true" :height="350" :data="chartData" :scale="scale">
<v-coord type="polar" :startAngle="-202.5" :endAngle="22.5" :radius="0.75"></v-coord>
<v-axis
dataKey="value"
:zIndex="2"
:line="null"
:label="axisLabel"
:subTickCount="4"
:subTickLine="axisSubTickLine"
:tickLine="axisTickLine"
:grid="null"
></v-axis>
<v-axis dataKey="1" :show="false"></v-axis>
<v-series
gemo="point"
position="value*1"
shape="pointer"
color="#1890FF"
:active="false"
></v-series>
<v-guide
type="arc"
:zIndex="0"
:top="false"
:start="arcGuide1Start"
:end="arcGuide1End"
:vStyle="arcGuide1Style"
></v-guide>
<v-guide
type="arc"
:zIndex="1"
:start="arcGuide2Start"
:end="getArcGuide2End"
:vStyle="arcGuide2Style"
></v-guide>
<v-guide
type="html"
:position="htmlGuidePosition"
:html="getHtmlGuideHtml()"
></v-guide>
</v-chart>
</div>
</template>
<script>
import { registerShape } from 'viser-vue';
registerShape('point', 'pointer', {
draw(cfg, container) {
let point = cfg.points[0];
point = this.parsePoint(point);
const center = this.parsePoint({
x: 0,
y: 0,
});
container.addShape('line', {
attrs: {
x1: center.x,
y1: center.y,
x2: point.x,
y2: point.y + 15,
stroke: cfg.color,
lineWidth: 5,
lineCap: 'round',
}
});
return container.addShape('circle', {
attrs: {
x: center.x,
y: center.y,
r: 9.75,
stroke: cfg.color,
lineWidth: 4.5,
fill: '#fff',
}
});
}
});
const scale = [{
dataKey: 'value',
min: 0,
max: 9,
tickInterval: 1,
nice: false,
}];
const data = [
{ value: 7.0 },
];
export default {
name:"DashChartDemo",
props:{
datasource:{
type: Number,
default:7
},
title: {
type: String,
default: ''
}
},
created(){
if(!this.datasource){
this.chartData = data;
}else{
this.chartData = [
{ value: this.datasource },
];
}
this.getChartData()
},
watch: {
'datasource': function (val) {
this.chartData = [
{ value: val},
];
this.getChartData();
}
},
methods:{
getChartData(){
if(this.chartData && this.chartData.length>0){
this.abcd = this.chartData[0].value * 10
}else{
this.abcd = 70
}
},
getHtmlGuideHtml(){
return '<div style="width: 300px;text-align: center;">\n' +
'<p style="font-size: 14px;color: #545454;margin: 0;">'+this.title+'</p>\n' +
'<p style="font-size: 36px;color: #545454;margin: 0;">'+this.abcd+'%</p>\n' +
'</div>'
},
getArcGuide2End(){
return [this.chartData[0].value, 0.945]
}
},
data() {
return {
chartData:[],
height: 400,
scale: scale,
abcd:70,
axisLabel: {
offset: -16,
textStyle: {
fontSize: 18,
textAlign: 'center',
textBaseline: 'middle'
}
},
axisSubTickLine: {
length: -8,
stroke: '#fff',
strokeOpacity: 1,
},
axisTickLine: {
length: -17,
stroke: '#fff',
strokeOpacity: 1,
},
arcGuide1Start: [0, 0.945],
arcGuide1End: [9, 0.945],
arcGuide1Style: {
stroke: '#CBCBCB',
lineWidth: 18,
},
arcGuide2Start: [0, 0.945],
arcGuide2Style: {
stroke: '#1890FF',
lineWidth: 18,
},
htmlGuidePosition: ['50%', '100%'],
htmlGuideHtml: `
<div style="width: 300px;text-align: center;">
<p style="font-size: 14px;color: #545454;margin: 0;">${this.title}</p>
<p style="font-size: 36px;color: #545454;margin: 0;">${this.abcd}%</p>
</div>
`,
};
},
};
</script>

View File

@ -0,0 +1,77 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :force-fit="true" :height="height" :data="data" :scale="scale">
<v-tooltip/>
<v-axis/>
<v-legend/>
<v-line position="type*y" color="x"/>
<v-point position="type*y" color="x" :size="4" :v-style="style" :shape="'circle'"/>
</v-chart>
</div>
</template>
<script>
import { DataSet } from '@antv/data-set'
export default {
name: 'LineChartMultid',
props: {
title: {
type: String,
default: ''
},
dataSource: {
type: Array,
default: () => [
{ type: 'Jan', jeecg: 7.0, jeebt: 3.9 },
{ type: 'Feb', jeecg: 6.9, jeebt: 4.2 },
{ type: 'Mar', jeecg: 9.5, jeebt: 5.7 },
{ type: 'Apr', jeecg: 14.5, jeebt: 8.5 },
{ type: 'May', jeecg: 18.4, jeebt: 11.9 },
{ type: 'Jun', jeecg: 21.5, jeebt: 15.2 },
{ type: 'Jul', jeecg: 25.2, jeebt: 17.0 },
{ type: 'Aug', jeecg: 26.5, jeebt: 16.6 },
{ type: 'Sep', jeecg: 23.3, jeebt: 14.2 },
{ type: 'Oct', jeecg: 18.3, jeebt: 10.3 },
{ type: 'Nov', jeecg: 13.9, jeebt: 6.6 },
{ type: 'Dec', jeecg: 9.6, jeebt: 4.8 }
]
},
fields: {
type: Array,
default: () => ['jeecg', 'jeebt']
},
height: {
type: Number,
default: 254
}
},
data() {
return {
scale: [{
dataKey: 'x',
min: 0,
max: 1
}],
style: { stroke: '#fff', lineWidth: 1 }
}
},
computed: {
data() {
const dv = new DataSet.View().source(this.dataSource)
dv.transform({
type: 'fold',
fields: this.fields,
key: 'x',
value: 'y'
})
return dv.rows
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,80 @@
<template>
<div>
<v-chart
:forceFit="true"
:height="height"
:width="width"
:data="data"
:scale="scale"
:padding="0">
<v-tooltip/>
<v-interval
:shape="['liquid-fill-gauge']"
position="transfer*value"
color=""
:v-style="{
lineWidth: 8,
opacity: 0.75
}"
:tooltip="[
'transfer*value',
(transfer, value) => {
return {
name: transfer,
value,
};
},
]"
></v-interval>
<v-guide
v-for="(row, index) in data"
:key="index"
type="text"
:top="true"
:position="{
gender: row.transfer,
value: 45
}"
:content="row.value + '%'"
:v-style="{
fontSize: 100,
textAlign: 'center',
opacity: 0.75,
}"
/>
</v-chart>
</div>
</template>
<script>
const sourceDataConst = [
{ transfer: '一月', value: 813 },
{ transfer: '二月', value: 233 },
{ transfer: '三月', value: 561 }
]
export default {
name: 'Liquid',
props: {
height: {
type: Number,
default: 0
},
width: {
type: Number,
default: 0
}
},
data() {
return {
data: sourceDataConst,
scale: []
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,69 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :scale="scale" :padding="[36, 0, 18, 0]">
<v-tooltip/>
<v-smooth-area position="x*y"/>
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'dayjs'
const sourceData = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
sourceData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
export default {
name: 'MiniArea',
props: {
dataSource: {
type: Array,
default: () => []
},
// x 轴别名
x: {
type: String,
default: 'x'
},
// y 轴别名
y: {
type: String,
default: 'y'
}
},
data() {
return {
data: [],
height: 100
}
},
computed: {
scale() {
return [
{ dataKey: 'x', title: this.x, alias: this.x },
{ dataKey: 'y', title: this.y, alias: this.y }
]
}
},
created() {
if (this.dataSource.length === 0) {
this.data = sourceData
} else {
this.data = this.dataSource
}
}
}
</script>
<style lang="scss" scoped>
@import "chart";
</style>

View File

@ -0,0 +1,76 @@
<template>
<div :style="{'width':width==null?'auto':width+'px'}">
<v-chart :forceFit="width==null" :height="height" :data="data" padding="0">
<v-tooltip/>
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
import moment from 'dayjs'
const sourceData = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
sourceData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 30
}]
export default {
name: 'MiniBar',
props: {
dataSource: {
type: Array,
default: () => []
},
width: {
type: Number,
default: null
},
height: {
type: Number,
default: 200
}
},
created() {
if (this.dataSource.length === 0) {
this.data = sourceData
} else {
this.data = this.dataSource
}
},
data() {
return {
tooltip,
data: [],
scale
}
}
}
</script>
<style lang="scss" scoped>
@import "chart";
</style>

View File

@ -0,0 +1,75 @@
<template>
<div class="chart-mini-progress">
<div class="target" :style="{ left: target + '%'}">
<span :style="{ backgroundColor: color }"/>
<span :style="{ backgroundColor: color }"/>
</div>
<div class="progress-wrapper">
<div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height+'px' }"></div>
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: {
target: {
type: Number,
default: 0
},
height: {
type: Number,
default: 10
},
color: {
type: String,
default: '#13C2C2'
},
percentage: {
type: Number,
default: 0
}
}
}
</script>
<style lang="scss" scoped>
.chart-mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
&:last-child {
top: auto;
bottom: 0;
}
}
}
.progress-wrapper {
background-color: #f5f5f5;
position: relative;
.progress {
transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: #1890ff;
width: 0;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<v-chart :forceFit="true" :height="height" :data="data" :scale="scale">
<v-tooltip :showTitle="false" dataKey="item*percent"/>
<v-axis/>
<v-legend dataKey="item"/>
<v-pie position="percent" color="item" :v-style="pieStyle" :label="labelConfig"/>
<v-coord type="theta"/>
</v-chart>
</template>
<script>
const DataSet = require('@antv/data-set')
export default {
props: {
title: {
type: String,
default: ''
},
height: {
type: Number,
default: 254
},
dataSource: {
type: Array,
default: () => [
{ item: '示例一', count: 40 },
{ item: '示例二', count: 21 },
{ item: '示例三', count: 17 },
{ item: '示例四', count: 13 },
{ item: '示例五', count: 9 }
]
}
},
data() {
return {
scale: [{
dataKey: 'percent',
min: 0,
formatter: '.0%'
}],
pieStyle: {
stroke: '#fff',
lineWidth: 1
},
labelConfig: ['percent', {
formatter: (val, item) => {
return item.point.item + ': ' + val
}
}]
}
},
computed: {
data() {
let dv = new DataSet.View().source(this.dataSource)
// 计算数据百分比
dv.transform({
type: 'percent',
field: 'count',
dimension: 'item',
as: 'percent'
})
return dv.rows
}
}
}
</script>

View File

@ -0,0 +1,367 @@
# 报表组件文档
## 柱状图
##### 引用方式
```js
import Bar from '@/components/chart/Bar'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| dataSource | array | ✔️ | 报表数据源 |
| height | number | | 报表高度默认254 |
##### dataSource 示例
```json
[
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
```
##### 代码示例
```html
<template>
<bar title="柱状图" :dataSource="dataSource" :height="420"/>
</template>
<script>
import Bar from '@/components/chart/Bar'
export default {
name: 'ChartDemo',
components: {
Bar
},
data() {
return {
dataSource: [
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
}
}
}
</script>
<style></style>
```
## 多列柱状图
##### 引用方式
```js
import BarMultid from '@/components/chart/BarMultid'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| fields | array | | 主列字段列表 |
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度默认254 |
##### fields 示例
```json
["Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug."]
```
##### dataSource 示例
```json
[
{
"type": "Jeecg", // 列名
"Jan.": 18.9,
"Feb.": 28.8,
"Mar.": 39.3,
"Apr.": 81.4,
"May": 47,
"Jun.": 20.3,
"Jul.": 24,
"Aug.": 35.6
},
{
"type": "Jeebt",
"Jan.": 12.4,
"Feb.": 23.2,
"Mar.": 34.5,
"Apr.": 99.7,
"May": 52.6,
"Jun.": 35.5,
"Jul.": 37.4,
"Aug.": 42.4
}
]
```
## 迷你柱状图
不带标题和数据轴的柱状图
##### 引用方式
```js
import MiniBar from '@/components/chart/MiniBar'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|---------------|
| width | number | | 报表宽度度,默认自适应宽度 |
| height | number | | 报表高度默认200 |
| dataSource | array | | 报表数据源 |
##### dataSource 示例
```json
[
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
```
## 面积图
##### 引用方式
```js
import AreaChartTy from '@/components/chart/AreaChartTy'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| dataSource | array | ✔️ | 报表数据源 |
| height | number | | 报表高度默认254 |
| lineSize | number | | 线的粗细默认2 |
##### dataSource 示例
```json
[
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
```
## 多行折线图
##### 引用方式
```js
import LineChartMultid from '@/components/chart/LineChartMultid'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| fields | array | | 主列字段列表 |
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度默认254 |
##### fields 示例
```json
["jeecg", "jeebt"]
```
##### dataSource 示例
```json
[
{
"type": "Jan", // 列名
"jeecg": 7,
"jeebt": 3.9
},
{ "type": "Feb", "jeecg": 6.9, "jeebt": 4.2 },
{ "type": "Mar", "jeecg": 9.5, "jeebt": 5.7 },
{ "type": "Apr", "jeecg": 14.5, "jeebt": 8.5 },
{ "type": "May", "jeecg": 18.4, "jeebt": 11.9 },
{ "type": "Jun", "jeecg": 21.5, "jeebt": 15.2 },
{ "type": "Jul", "jeecg": 25.2, "jeebt": 17 },
{ "type": "Aug", "jeecg": 26.5, "jeebt": 16.6 },
{ "type": "Sep", "jeecg": 23.3, "jeebt": 14.2 },
{ "type": "Oct", "jeecg": 18.3, "jeebt": 10.3 },
{ "type": "Nov", "jeecg": 13.9, "jeebt": 6.6 },
{ "type": "Dec", "jeecg": 9.6, "jeebt": 4.8 }
]
```
## 饼状图
##### 引用方式
```js
import Pie from '@/components/chart/Pie'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度默认254 |
##### dataSource 示例
```json
[
// 所有的 percent 相加等于 100
{ "item": "一月", "percent": 40 },
{ "item": "二月", "percent": 21 },
{ "item": "三月", "percent": 17 },
{ "item": "四月", "percent": 13 },
{ "item": "五月", "percent": 9 }
]
```
## 雷达图
##### 引用方式
```js
import Radar from '@/components/chart/Radar'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度默认254 |
##### dataSource 示例
```json
[
// score 最小值为 0最大值为 100
{ "item": "一月", "score": 40 },
{ "item": "二月", "score": 20 },
{ "item": "三月", "score": 67 },
{ "item": "四月", "score": 43 },
{ "item": "五月", "score": 90 }
]
```
## 进度条
##### 引用方式
```js
import MiniProgress from '@/components/chart/MiniProgress'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|-------------------|
| percentage | number | | 当前进度百分比默认0最高100 |
| target | number | | 目标值默认10 |
| height | number | | 进度条高度默认10 |
| color | string | | 进度条颜色,默认 #13C2C2 |
## 仪表盘
##### 引用方式
```js
import DashChartDemo from '@/components/chart/DashChartDemo'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|----|----------------|
| title | string | | 报表标题 |
| value | number | | 当前值默认6.7最大为9 |
| height | number | | 报表高度默认254 |
## 排名列表
##### 引用方式
```js
import RankList from '@/components/chart/RankList'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|----|--------------|
| title | string | | 报表标题 |
| list | array | | 排名列表数据 |
| height | number | | 报表高度,默认自适应高度 |
##### list 示例
```json
[
{
"name": "北京朝阳 1 号店",
"total": 1981
},
{ "name": "北京朝阳 2 号店", "total": 1359 },
{ "name": "北京朝阳 3 号店", "total": 1354 },
{ "name": "北京朝阳 4 号店", "total": 263 },
{ "name": "北京朝阳 5 号店", "total": 446 },
{ "name": "北京朝阳 6 号店", "total": 796 }
]
```

View File

@ -0,0 +1,90 @@
<template>
<v-chart :forceFit="true" :height="height" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip></v-tooltip>
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid"/>
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid"/>
<v-legend dataKey="user" marker="circle" :offset="30"/>
<v-coord type="polar" radius="0.8"/>
<v-line position="item*score" color="user" :size="2"/>
<v-point position="item*score" color="user" :size="4" shape="circle"/>
</v-chart>
</template>
<script>
const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}
const scale = [
{
dataKey: 'score',
min: 0,
max: 100
}, {
dataKey: 'user',
alias: '类型'
}
]
const sourceData = [
{ item: '示例一', score: 40 },
{ item: '示例二', score: 20 },
{ item: '示例三', score: 67 },
{ item: '示例四', score: 43 },
{ item: '示例五', score: 90 }
]
export default {
name: 'Radar',
props: {
height: {
type: Number,
default: 254
},
dataSource: {
type: Array,
default: () => []
}
},
data() {
return {
axis1Opts,
axis2Opts,
scale,
data: sourceData
}
},
watch: {
dataSource(newVal) {
if (newVal.length === 0) {
this.data = sourceData
} else {
this.data = newVal
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="rank">
<h4 class="title">{{ title }}</h4>
<ul class="list" :style="{height:height?`${height}px`:'auto',overflow:'auto'}">
<li :key="index" v-for="(item, index) in list">
<span :class="index < 3 ? 'active' : null">{{ index + 1 }}</span>
<span>{{ item.name }}</span>
<span>{{ item.total }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "RankList",
// ['title', 'list']
props: {
title: {
type: String,
default: ''
},
list: {
type: Array,
default: null
},
height: {
type: Number,
default: null
}
}
}
</script>
<style lang="scss" scoped>
.rank {
padding: 0 32px 32px 72px;
.list {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
margin-top: 16px;
span {
color: rgba(0, 0, 0, .65);
font-size: 14px;
line-height: 22px;
&:first-child {
background-color: #f5f5f5;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
&.active {
background-color: #314659;
color: #fff;
}
&:last-child {
float: right;
}
}
}
}
}
.mobile .rank {
padding: 0 32px 32px 32px;
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
:height="height"
:data="data"
:scale="scale"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip/>
<v-axis/>
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
},
x: {
type: String,
default: 'x'
},
y: {
type: String,
default: 'y'
},
data: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 254
}
},
data() {
return {}
},
computed: {
scale() {
return [
{ dataKey: 'x', title: this.x, alias: this.x },
{ dataKey: 'y', title: this.y, alias: this.y }
]
}
},
created() {
// this.getMonthBar()
},
methods: {
// getMonthBar() {
// this.$http.get('/analysis/month-bar')
// .then(res => {
// this.data = res.result
// })
// }
}
}
</script>

View File

@ -0,0 +1,84 @@
<template>
<div class="chart-trend">
{{ term }}
<span>{{ rate }}%</span>
<span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
</div>
</template>
<script>
export default {
name: "Trend",
props: {
// 同title
term: {
type: String,
default: '',
required: true
},
// 百分比
percentage: {
type: Number,
default: null
},
type: {
type: Boolean,
default: null
},
target: {
type: Number,
default: 0
},
value: {
type: Number,
default: 0
},
fixed: {
type: Number,
default: 2
}
},
data () {
return {
trend: this.type && 'up' || 'down',
rate: this.percentage
}
},
created () {
let type = this.type === null ? this.value >= this.target : this.type
this.trend = type ? 'up' : 'down';
this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
}
}
</script>
<style lang="scss" scoped>
.chart-trend {
display: inline-block;
font-size: 14px;
line-height: 22px;
.trend-icon {
font-size: 12px;
&.up, &.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(.83);
}
}
&.up {
color: #f5222d;
}
&.down {
color: #52c41a;
top: -1px;
}
}
}
</style>

View File

@ -0,0 +1,13 @@
.antv-chart-mini {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
/* margin: 0 -5px;
overflow: hidden;*/
}
}

View File

@ -0,0 +1,80 @@
<template>
<a-radio-group v-if="tagType=='radio'" @change="handleInput" :value="value" :disabled="disabled">
<a-radio v-for="(item, key) in dictOptions" :key="key" :value="item.value">{{ item.text }}</a-radio>
</a-radio-group>
<a-select v-else-if="tagType=='select'" :placeholder="placeholder" :disabled="disabled" :value="value" @change="handleInput">
<a-select-option value="">请选择</a-select-option>
<a-select-option v-for="(item, key) in dictOptions" :key="key" :value="item.value">
<span style="display: inline-block;width: 100%" :title=" item.text || item.label ">
{{ item.text || item.label }}
</span>
</a-select-option>
</a-select>
</template>
<script>
import {ajaxGetDictItems} from '@/api/api'
export default {
name: "JDictSelectTag",
props: {
dictCode: String,
placeholder: String,
triggerChange: Boolean,
disabled: Boolean,
value: String,
type: String
},
data() {
return {
dictOptions: [],
tagType:""
}
},
created() {
console.log(this.dictCode);
if(!this.type || this.type==="list"){
this.tagType = "select"
}else{
this.tagType = this.type
}
//获取字典数据
this.initDictData();
},
methods: {
initDictData() {
//根据字典Code, 初始化字典数组
ajaxGetDictItems(this.dictCode, null).then((res) => {
if (res.success) {
// console.log(res.result);
this.dictOptions = res.result;
}
})
},
handleInput(e) {
let val;
if(this.tagType=="radio"){
val = e.target.value
}else{
val = e
}
console.log(val);
if(this.triggerChange){
this.$emit('change', val);
}else{
this.$emit('input', val);
}
},
setCurrentDictOptions(dictOptions){
this.dictOptions = dictOptions
},
getCurrentDictOptions(){
return this.dictOptions
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,87 @@
/**
* 字典 util
* author: scott
* date: 20190109
*/
import {ajaxGetDictItems} from '@/api/api'
import {getAction} from '@/api/manage'
/**
* 获取字典数组
* @param dictCode 字典Code
* @return List<Map>
*/
export async function initDictOptions(dictCode) {
if (!dictCode) {
return '字典Code不能为空!';
}
//获取字典数组
let res = await ajaxGetDictItems(dictCode);
return res;
}
/**
* 字典值替换文本通用方法
* @param dictOptions 字典数组
* @param text 字典值
* @return String
*/
export function filterDictText(dictOptions, text) {
let re = "";
dictOptions.forEach(function (option) {
if (text === option.value) {
re = option.text;
}
});
return re;
}
/**
* 字典值替换文本通用方法(多选)
* @param dictOptions 字典数组
* @param text 字典值
* @return String
*/
export function filterMultiDictText(dictOptions, text) {
if(!text){
return ""
}
let re = "";
let arr = text.split(",")
dictOptions.forEach(function (option) {
for(let i=0;i<arr.length;i++){
if (arr[i] === option.value) {
re += option.text+",";
break;
}
}
});
if(re==""){
return "";
}
return re.substring(0,re.length-1);
}
/**
* 翻译字段值对应的文本
* @param children
* @returns string
*/
export async function ajaxFilterDictText(dictCode, key) {
if (!dictCode) {
return '字典Code不能为空!';
}
//console.log(`key : ${key}`);
if (!key) {
return '';
}
//通过请求读取字典文本
let res = await getAction(`/sys/dict/getDictText/${dictCode}/${key}`);
if (res.success) {
// console.log('restult: '+ res.result);
return res.result;
} else {
return '';
}
}

View File

@ -0,0 +1,94 @@
<template>
<a-checkbox-group v-if="tagType=='checkbox'" @change="onChange" :value="arrayValue" :disabled="disabled">
<a-checkbox v-for="(item, key) in dictOptions" :key="key" :value="item.value">{{ item.text || item.label }}</a-checkbox>
</a-checkbox-group>
<a-select
v-else-if="tagType=='select'"
:value="arrayValue"
@change="onChange"
:disabled="disabled"
mode="multiple"
:placeholder="placeholder">
<a-select-option
v-for="(item,index) in dictOptions"
:key="index"
:value="item.value">
<span style="display: inline-block;width: 100%" :title=" item.text || item.label ">
{{ item.text || item.label }}
</span>
</a-select-option>
</a-select>
</template>
<script>
import {ajaxGetDictItems} from '@/api/api'
export default {
name: 'JMultiSelectTag',
props: {
dictCode: String,
placeholder: String,
disabled: Boolean,
value: String,
type: String,
options:Array
},
data() {
return {
dictOptions: [],
tagType:"",
arrayValue:!this.value?[]:this.value.split(",")
}
},
created() {
if(!this.type || this.type==="list_multi"){
this.tagType = "select"
}else{
this.tagType = this.type
}
//获取字典数据
this.initDictData();
},
watch:{
options: function(val){
this.setCurrentDictOptions(val);
},
value (val) {
if(!val){
this.arrayValue = []
}else{
this.arrayValue = this.value.split(",")
}
}
},
methods: {
initDictData() {
if(this.options && this.options.length>0){
this.dictOptions = [...this.options]
}else{
//根据字典Code, 初始化字典数组
ajaxGetDictItems(this.dictCode, null).then((res) => {
if (res.success) {
this.dictOptions = res.result;
}
})
}
},
onChange (selectedValue) {
this.$emit('change', selectedValue.join(","));
},
setCurrentDictOptions(dictOptions){
this.dictOptions = dictOptions
},
getCurrentDictOptions(){
return this.dictOptions
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>

View File

@ -0,0 +1,174 @@
<template>
<a-select
v-if="async"
showSearch
labelInValue
@search="loadData"
:placeholder="placeholder"
v-model="selectedAsyncValue"
style="width: 100%"
:filterOption="false"
@change="handleAsyncChange"
:notFoundContent="loading ? undefined : null"
>
<a-spin v-if="loading" slot="notFoundContent" size="small"/>
<a-select-option v-for="d in options" :key="d.value" :value="d.value">{{ d.text }}</a-select-option>
</a-select>
<a-select
v-else
showSearch
:placeholder="placeholder"
optionFilterProp="children"
style="width: 100%"
@change="handleChange"
:filterOption="filterOption"
v-model="selectedValue"
:notFoundContent="loading ? undefined : null">
<a-spin v-if="loading" slot="notFoundContent" size="small"/>
<a-select-option v-for="d in options" :key="d.value" :value="d.value">{{ d.text }}</a-select-option>
</a-select>
</template>
<script>
import { ajaxGetDictItems } from '@/api/api'
import debounce from 'lodash/debounce';
import { getAction } from '../../api/manage'
export default {
name: 'JSearchSelectTag',
props:{
disabled: Boolean,
value: String,
dict: String,
dictOptions: Array,
async: Boolean,
placeholder:{
type:String,
default:"请选择",
required:false
}
},
data(){
this.loadData = debounce(this.loadData, 800);//消抖
this.lastLoad = 0;
return {
loading:false,
selectedValue:[],
selectedAsyncValue:[],
options: [],
}
},
created(){
this.initDictData();
},
watch:{
"value":{
immediate:true,
handler(val){
if(!val){
this.selectedValue=[]
this.selectedAsyncValue=[]
}else{
this.initSelectValue()
}
}
},
"dict":{
handler(){
this.initDictData()
}
}
},
methods:{
initSelectValue(){
if(this.async){
if(!this.selectedAsyncValue || !this.selectedAsyncValue.key || this.selectedAsyncValue.key!=this.value){
console.log("这才请求后台")
getAction(`/sys/dict/loadDictItem/${this.dict}`,{key:this.value}).then(res=>{
if(res.success){
let obj = {
key:this.value,
label:res.result
}
this.selectedAsyncValue = {...obj}
}
})
}
}else{
this.selectedValue = this.value
}
},
loadData(value){
console.log("数据加载",value)
this.lastLoad +=1
const currentLoad = this.lastLoad
this.options = []
this.loading=true
// 字典code格式table,text,code
getAction(`/sys/dict/loadDict/${this.dict}`,{keyword:value}).then(res=>{
this.loading=false
if(res.success){
if(currentLoad!=this.lastLoad){
return
}
this.options = res.result
console.log("我是第一个",res)
}else{
this.$message.warning(res.message)
}
})
},
initDictData(){
if(!this.async){
//如果字典项集合有数据
if(this.dictOptions && this.dictOptions.length>0){
this.options = [...this.dictOptions]
}else{
//根据字典Code, 初始化字典数组
ajaxGetDictItems(this.dict, null).then((res) => {
if (res.success) {
this.options = res.result;
}
})
}
}
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
handleChange (selectedValue) {
console.log("selectedValue",selectedValue)
this.selectedValue = selectedValue
this.callback()
},
handleAsyncChange(selectedObj){
this.selectedAsyncValue = selectedObj
this.selectedValue = selectedObj.key
this.callback()
},
callback(){
this.$emit('change', this.selectedValue);
},
setCurrentDictOptions(dictOptions){
this.options = dictOptions
},
getCurrentDictOptions(){
return this.options
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,181 @@
# JDictSelectTag 组件用法
----
- 从字典表获取数据,dictCode格式说明: 字典code
```html
<j-dict-select-tag v-model="queryParam.sex" placeholder="请输入用户性别"
dictCode="sex"/>
```
v-decorator用法
```html
<j-dict-select-tag v-decorator="['sex', {}]" :triggerChange="true" placeholder="请输入用户性别"
dictCode="sex"/>
```
- 从数据库表获取字典数据dictCode格式说明: 表名,文本字段,取值字段
```html
<j-dict-select-tag v-model="queryParam.username" placeholder="请选择用户名称"
dictCode="sys_user,realname,id"/>
```
# JDictSelectUtil.js 列表字典函数用法
----
- 第一步: 引入依赖方法
```html
import {initDictOptions, filterDictText} from '@/components/dict/JDictSelectUtil'
```
- 第二步: 在created()初始化方法执行字典配置方法
```html
//初始化字典配置
this.initDictConfig();
```
- 第三步: 实现initDictConfig方法加载列表所需要的字典(列表上有多个字典项就执行多次initDictOptions方法)
```html
initDictConfig() {
//初始化字典 - 性别
initDictOptions('sex').then((res) => {
if (res.success) {
this.sexDictOptions = res.result;
}
});
},
```
- 第四步: 实现字段的customRender方法
```html
customRender: (text, record, index) => {
//字典值替换通用方法
return filterDictText(this.sexDictOptions, text);
}
```
# JMultiSelectTag 多选组件
下拉/checkbox
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| disabled |Boolean | | 是否禁用 |
| type |string | | 多选类型 select/checkbox 默认是select |
| dictCode |string | | 数据字典编码或者表名,显示字段名,存储字段名拼接而成的字符串,如果提供了options参数 则此参数可不填|
| options |Array | | 多选项,如果dictCode参数未提供,可以设置此参数加载多选项 |
使用示例
----
```vue
<template>
<a-form>
<a-form-item label="下拉多选" style="width: 300px">
<j-multi-select-tag
v-model="selectValue"
:options="dictOptions"
placeholder="请做出你的选择">
</j-multi-select-tag>
{{ selectValue }}
</a-form-item>
<a-form-item label="checkbox">
<j-multi-select-tag
v-model="checkboxValue"
:options="dictOptions"
type="checkbox">
</j-multi-select-tag>
{{ checkboxValue }}
</a-form-item>
</a-form >
</template>
<script>
import JMultiSelectTag from '@/components/dict/JMultiSelectTag'
export default {
components: {JMultiSelectTag},
data() {
return {
selectValue:"",
checkboxValue:"",
dictOptions:[{
label:"选项一",
value:"1"
},{
label:"选项二",
value:"2"
},{
label:"选项三",
value:"3"
}]
}
}
}
</script>
```
# JSearchSelectTag 字典表的搜索组件
下拉搜索组件,支持异步加载,异步加载用于大数据量的字典表
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| disabled |Boolean | | 是否禁用 |
| dict |string | | 表名,显示字段名,存储字段名拼接而成的字符串,如果提供了dictOptions参数 则此参数可不填|
| dictOptions |Array | | 多选项,如果dict参数未提供,可以设置此参数加载多选项 |
| async |Boolean | | 是否支持异步加载,设置成true,则通过输入的内容加载远程数据,否则在本地过滤数据,默认false|
使用示例
----
```vue
<template>
<a-form>
<a-form-item label="下拉搜索" style="width: 300px">
<j-search-select-tag
placeholder="请做出你的选择"
v-model="selectValue"
:dictOptions="dictOptions">
</j-search-select-tag>
{{ selectValue }}
</a-form-item>
<a-form-item label="异步加载" style="width: 300px">
<j-search-select-tag
placeholder="请做出你的选择"
v-model="asyncSelectValue"
dict="sys_depart,depart_name,id"
:async="true">
</j-search-select-tag>
{{ asyncSelectValue }}
</a-form-item>
</a-form >
</template>
<script>
import JSearchSelectTag from '@/components/dict/JSearchSelectTag'
export default {
components: {JSearchSelectTag},
data() {
return {
selectValue:"",
asyncSelectValue:"",
dictOptions:[{
text:"选项一",
value:"1"
},{
text:"选项二",
value:"2"
},{
text:"选项三",
value:"3"
}]
}
}
}
</script>
```

View File

@ -0,0 +1,7 @@
import T from './JDictSelectTag.vue'
const JDictSelectTag = {
install: function (Vue) {
Vue.component('JDictSelectTag',T);
}
}
export default JDictSelectTag;

View File

@ -0,0 +1,4 @@
@import "~ant-design-vue/lib/style/index";
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;

View File

@ -0,0 +1,43 @@
<template>
<a-checkbox-group :options="options" :value="checkboxArray" @change="onChange" />
</template>
<script>
export default {
name: 'JCheckbox',
props: {
value:{
type: String,
required: false
},
/*label value*/
options:{
type: Array,
required: true
}
},
data(){
return {
checkboxArray:!this.value?[]:this.value.split(",")
}
},
watch:{
value (val) {
if(!val){
this.checkboxArray = []
}else{
this.checkboxArray = this.value.split(",")
}
}
},
methods:{
onChange (checkedValues) {
this.$emit('change', checkedValues.join(","));
},
},
model: {
prop: 'value',
event: 'change'
}
}
</script>

View File

@ -0,0 +1,409 @@
<template>
<div v-bind="fullScreenParentProps">
<a-icon v-if="fullScreen" class="full-screen-icon" type="fullscreen" @click="()=>fullCoder=!fullCoder"/>
<div class="code-editor-cust full-screen-child">
<textarea ref="textarea"></textarea>
<span @click="nullTipClick" class="null-tip" :class="{'null-tip-hidden':hasCode}" :style="nullTipStyle">{{ placeholderShow }}</span>
<template v-if="languageChange">
<a-select v-model="mode" size="small" class="code-mode-select" @change="changeMode" placeholder="请选择主题">
<a-select-option
v-for="mode in modes"
:key="mode.value"
:value="mode.value">
{{ mode.label }}
</a-select-option>
</a-select>
</template>
</div>
</div>
</template>
<script type="text/ecmascript-6">
// 引入全局实例
import _CodeMirror from 'codemirror'
// 核心样式
import 'codemirror/lib/codemirror.css'
// 引入主题后还需要在 options 中指定主题才会生效 darcula gruvbox-dark hopscotch monokai
import 'codemirror/theme/panda-syntax.css'
//提示css
import "codemirror/addon/hint/show-hint.css";
// 需要引入具体的语法高亮库才会有对应的语法高亮效果
// codemirror 官方其实支持通过 /addon/mode/loadmode.js 和 /mode/meta.js 来实现动态加载对应语法高亮库
// 但 vue 貌似没有无法在实例初始化后再动态加载对应 JS ,所以此处才把对应的 JS 提前引入
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/css/css.js'
import 'codemirror/mode/xml/xml.js'
import 'codemirror/mode/clike/clike.js'
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/python/python.js'
import 'codemirror/mode/r/r.js'
import 'codemirror/mode/shell/shell.js'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/mode/swift/swift.js'
import 'codemirror/mode/vue/vue.js'
// 尝试获取全局实例
const CodeMirror = window.CodeMirror || _CodeMirror
export default {
name: 'JCodeEditor',
props: {
// 外部传入的内容,用于实现双向绑定
value: {
type: String,
default: ''
},
// 外部传入的语法类型
language: {
type: String,
default: null
},
languageChange:{
type: Boolean,
default:false,
required:false
},
placeholder: {
type: String,
default: null
},
// 显示行号
lineNumbers: {
type: Boolean,
default: true
},
// 是否显示全屏按钮
fullScreen: {
type: Boolean,
default: false
},
// 全屏以后的z-index
zIndex: {
type: [Number, String],
default: 999
}
},
data () {
return {
// 内部真实的内容
code: '',
hasCode:false,
// 默认的语法类型
mode: 'javascript',
// 编辑器实例
coder: null,
// 默认配置
options: {
// 缩进格式
tabSize: 2,
// 主题,对应主题库 JS 需要提前引入
theme: 'panda-syntax',
line: true,
// extraKeys: {'Ctrl': 'autocomplete'},//自定义快捷键
hintOptions: {
tables: {
users: ['name', 'score', 'birthDate'],
countries: ['name', 'population', 'size']
}
},
},
// 支持切换的语法高亮类型,对应 JS 已经提前引入
// 使用的是 MIME-TYPE ,不过作为前缀的 text/ 在后面指定时写死了
modes: [{
value: 'css',
label: 'CSS'
}, {
value: 'javascript',
label: 'Javascript'
}, {
value: 'html',
label: 'XML/HTML'
}, {
value: 'x-java',
label: 'Java'
}, {
value: 'x-objectivec',
label: 'Objective-C'
}, {
value: 'x-python',
label: 'Python'
}, {
value: 'x-rsrc',
label: 'R'
}, {
value: 'x-sh',
label: 'Shell'
}, {
value: 'x-sql',
label: 'SQL'
}, {
value: 'x-swift',
label: 'Swift'
}, {
value: 'x-vue',
label: 'Vue'
}, {
value: 'markdown',
label: 'Markdown'
}],
// code 编辑器 是否全屏
fullCoder: false
}
},
watch: {
// value: {
// immediate: false,
// handler(value) {
// this._getCoder().then(() => {
// this.coder.setValue(value)
// })
// }
// },
language: {
immediate: true,
handler(language) {
this._getCoder().then(() => {
// 尝试从父容器获取语法类型
if (language) {
// 获取具体的语法类型对象
let modeObj = this._getLanguage(language)
// 判断父容器传入的语法是否被支持
if (modeObj) {
this.mode = modeObj.label
this.coder.setOption('mode', `text/${modeObj.value}`)
}
}
})
}
}
},
computed: {
placeholderShow() {
if (this.placeholder == null) {
return `请在此输入${this.language}代码`
} else {
return this.placeholder
}
},
nullTipStyle(){
if (this.lineNumbers) {
return { left: '36px' }
} else {
return { left: '12px' }
}
},
// coder 配置
coderOptions() {
return {
tabSize: this.options.tabSize,
theme: this.options.theme,
lineNumbers: this.lineNumbers,
line: true,
hintOptions: this.options.hintOptions
}
},
fullScreenParentProps(){
let props = {
class: ['full-screen-parent', this.fullCoder ? 'full-screen' : ''],
style: {}
}
if (this.fullCoder) {
props.style['z-index'] = this.zIndex
}
return props
}
},
mounted () {
// 初始化
this._initialize()
},
methods: {
// 初始化
_initialize () {
// 初始化编辑器实例,传入需要被实例化的文本域对象和默认配置
this.coder = CodeMirror.fromTextArea(this.$refs.textarea, this.coderOptions)
// 编辑器赋值
this.coder.setValue(this.value || this.code)
if(this.value||this.code){
this.hasCode=true
}else{
this.hasCode=false
}
// 支持双向绑定
this.coder.on('change', (coder) => {
this.code = coder.getValue()
if(this.code){
this.hasCode=true
}else{
this.hasCode=false
}
if (this.$emit) {
this.$emit('input', this.code)
}
})
this.coder.on('focus', () => {
this.hasCode=true
})
this.coder.on('blur', () => {
if(this.code){
this.hasCode=true
}else{
this.hasCode=false
}
})
/* this.coder.on('cursorActivity',()=>{
this.coder.showHint()
})*/
},
getCodeContent(){
return this.code
},
setCodeContent(val){
this.coder.setValue(val)
},
// 获取当前语法类型
_getLanguage (language) {
// 在支持的语法类型列表中寻找传入的语法类型
return this.modes.find((mode) => {
// 所有的值都忽略大小写,方便比较
let currentLanguage = language.toLowerCase()
let currentLabel = mode.label.toLowerCase()
let currentValue = mode.value.toLowerCase()
// 由于真实值可能不规范,例如 java 的真实值是 x-java ,所以讲 value 和 label 同时和传入语法进行比较
return currentLabel === currentLanguage || currentValue === currentLanguage
})
},
_getCoder() {
let _this = this
return new Promise((resolve) => {
(function get() {
if (_this.coder) {
resolve(_this.coder)
} else {
setTimeout(get, 10)
}
})()
})
},
// 更改模式
changeMode (val) {
// 修改编辑器的语法配置
this.coder.setOption('mode', `text/${val}`)
// 获取修改后的语法
let label = this._getLanguage(val).label.toLowerCase()
// 允许父容器通过以下函数监听当前的语法值
this.$emit('language-change', label)
},
nullTipClick(){
this.coder.focus()
}
}
}
</script>
<style lang="less">
.code-editor-cust{
flex-grow:1;
display:flex;
position:relative;
height:100%;
.CodeMirror{
flex-grow:1;
z-index:1;
.CodeMirror-code{
line-height:19px;
}
}
.code-mode-select{
position:absolute;
z-index:2;
right:10px;
top:10px;
max-width:130px;
}
.CodeMirror{
height: auto;
min-height:100%;
}
.null-tip{
position: absolute;
top: 4px;
left: 36px;
z-index: 10;
color: #ffffffc9;
line-height: initial;
}
.null-tip-hidden{
display: none;
}
}
/* 全屏样式 */
.full-screen-parent {
position: relative;
.full-screen-icon {
opacity: 0;
color: black;
width: 20px;
height: 20px;
line-height: 24px;
background-color: white;
position: absolute;
top: 2px;
right: 2px;
z-index: 9;
cursor: pointer;
transition: opacity 0.3s;
}
&:hover {
.full-screen-icon {
opacity: 1;
&:hover {
background-color: rgba(255, 255, 255, 0.88);
}
}
}
&.full-screen {
position: fixed;
top: 10px;
left: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
background-color: #f5f5f5;
.full-screen-icon {
top: 12px;
right: 12px;
}
.full-screen-child {
height: 100%;
max-height: 100%;
min-height: 100%;
}
}
.full-screen-child {
min-height: 120px;
max-height: 320px;
}
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<a-date-picker
:disabled="disabled || readOnly"
:placeholder="placeholder"
@change="handleDateChange"
:value="momVal"
:showTime="showTime"
:format="dateFormat"
:getCalendarContainer="getCalendarContainer"
/>
</template>
<script>
import moment from 'moment'
export default {
name: 'JDate',
props: {
placeholder:{
type: String,
default: '',
required: false
},
value:{
type: String,
required: false
},
dateFormat:{
type: String,
default: 'YYYY-MM-DD',
required: false
},
//此属性可以被废弃了
triggerChange:{
type: Boolean,
required: false,
default: false
},
readOnly:{
type: Boolean,
required: false,
default: false
},
disabled:{
type: Boolean,
required: false,
default: false
},
showTime:{
type: Boolean,
required: false,
default: false
},
getCalendarContainer: {
type: Function,
default: () => document.body
}
},
data () {
let dateStr = this.value;
return {
decorator:"",
momVal:!dateStr?null:moment(dateStr,this.dateFormat)
}
},
watch: {
value (val) {
if(!val){
this.momVal = null
}else{
this.momVal = moment(val,this.dateFormat)
}
}
},
methods: {
moment,
handleDateChange(mom,dateStr){
this.$emit('change', dateStr);
}
},
//2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型 这个牛逼
model: {
prop: 'value',
event: 'change'
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
<template>
<div class="tinymce-editor">
<editor
v-model="myValue"
:init="init"
:disabled="disabled"
@onClick="onClick">
</editor>
</div>
</template>
<script>
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver/theme'
import 'tinymce/plugins/image'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/contextmenu'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/colorpicker'
import 'tinymce/plugins/textcolor'
import 'tinymce/plugins/fullscreen'
export default {
components: {
Editor
},
props: {
value: {
type: String,
required:false
},
triggerChange:{
type: Boolean,
default: false,
required:false
},
disabled: {
type: Boolean,
default: false
},
plugins: {
type: [String, Array],
default: 'lists image media table textcolor wordcount contextmenu fullscreen'
},
toolbar: {
type: [String, Array],
default: 'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | lists image media table | removeformat | fullscreen'
}
},
data() {
return {
//初始化配置
init: {
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/lightgray',
height: 300,
plugins: this.plugins,
toolbar: this.toolbar,
branding: false,
menubar: false,
images_upload_handler: (blobInfo, success) => {
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
success(img)
}
},
myValue: this.value
}
},
mounted() {
tinymce.init({})
},
methods: {
onClick(e) {
this.$emit('onClick', e, tinymce)
},
//可以添加一些自己的自定义事件,如清空内容
clear() {
this.myValue = ''
}
},
watch: {
value(newValue) {
this.myValue = newValue
},
myValue(newValue) {
console.log(newValue)
if(this.triggerChange){
console.log(1)
this.$emit('change', newValue)
}else{
console.log(2)
this.$emit('input', newValue)
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,29 @@
<template>
<a-tooltip placement="topLeft">
<template slot="title">
<span>{{value}}</span>
</template>
{{ value | ellipsis(length) }}
</a-tooltip>
</template>
<script>
export default {
name: 'JEllipsis',
props: {
value: {
type: String,
required: false,
},
length: {
type: Number,
required: false,
default: 25,
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,48 @@
<template>
<div v-if="disabled" class="jeecg-form-container-disabled">
<fieldset disabled>
<slot></slot>
</fieldset>
</div>
<div v-else>
<slot></slot>
</div>
</template>
<script>
/**
* 使用方法
* 在form下直接写这个组件就行了
*<a-form layout="inline" :form="form" >
* <j-form-container :disabled="true">
* <!-- 表单内容省略..... -->
* </j-form-container>
*</a-form>
*/
export default {
name: 'JFormContainer',
props:{
disabled:{
type:Boolean,
default:false,
required:false
}
},
mounted(){
console.log("我是表单禁用专用组件,但是我并不支持表单中iframe的内容禁用")
}
}
</script>
<style>
.jeecg-form-container-disabled{
cursor: not-allowed;
}
.jeecg-form-container-disabled fieldset[disabled] {
-ms-pointer-events: none;
pointer-events: none;
}
.jeecg-form-container-disabled .ant-select{
-ms-pointer-events: none;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<div class="gc-canvas" @click="reloadPic">
<canvas id="gc-canvas" :width="contentWidth" :height="contentHeight"></canvas>
</div>
</template>
<script>
export default {
name: 'JGraphicCode',
props: {
length:{
type: Number,
default: 4
},
fontSizeMin: {
type: Number,
default: 20
},
fontSizeMax: {
type: Number,
default: 45
},
backgroundColorMin: {
type: Number,
default: 180
},
backgroundColorMax: {
type: Number,
default: 240
},
colorMin: {
type: Number,
default: 50
},
colorMax: {
type: Number,
default: 160
},
lineColorMin: {
type: Number,
default: 40
},
lineColorMax: {
type: Number,
default: 180
},
dotColorMin: {
type: Number,
default: 0
},
dotColorMax: {
type: Number,
default: 255
},
contentWidth: {
type: Number,
default:136
},
contentHeight: {
type: Number,
default: 38
}
},
methods: {
// 生成一个随机数
randomNum (min, max) {
return Math.floor(Math.random() * (max - min) + min)
},
// 生成一个随机的颜色
randomColor (min, max) {
let r = this.randomNum(min, max)
let g = this.randomNum(min, max)
let b = this.randomNum(min, max)
return 'rgb(' + r + ',' + g + ',' + b + ')'
},
drawPic () {
this.randomCode()
let canvas = document.getElementById('gc-canvas')
let ctx = canvas.getContext('2d')
ctx.textBaseline = 'bottom'
// 绘制背景
ctx.fillStyle = this.randomColor(this.backgroundColorMin, this.backgroundColorMax)
ctx.fillRect(0, 0, this.contentWidth, this.contentHeight)
// 绘制文字
for (let i = 0; i < this.code.length; i++) {
this.drawText(ctx, this.code[i], i)
}
this.drawLine(ctx)
this.drawDot(ctx)
this.$emit("success",this.code)
},
drawText (ctx, txt, i) {
ctx.fillStyle = this.randomColor(this.colorMin, this.colorMax)
let fontSize = this.randomNum(this.fontSizeMin, this.fontSizeMax)
ctx.font = fontSize + 'px SimHei'
let padding = 10;
let offset = (this.contentWidth-40)/(this.code.length-1)
let x=padding;
if(i>0){
x = padding+(i*offset)
}
//let x = (i + 1) * (this.contentWidth / (this.code.length + 1))
let y = this.randomNum(this.fontSizeMax, this.contentHeight - 5)
if(fontSize>40){
y=40
}
var deg = this.randomNum(-10,10)
// 修改坐标原点和旋转角度
ctx.translate(x, y)
ctx.rotate(deg * Math.PI / 180)
ctx.fillText(txt, 0, 0)
// 恢复坐标原点和旋转角度
ctx.rotate(-deg * Math.PI / 180)
ctx.translate(-x, -y)
},
drawLine (ctx) {
// 绘制干扰线
for (let i = 0; i <1; i++) {
ctx.strokeStyle = this.randomColor(this.lineColorMin, this.lineColorMax)
ctx.beginPath()
ctx.moveTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.lineTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.stroke()
}
},
drawDot (ctx) {
// 绘制干扰点
for (let i = 0; i < 100; i++) {
ctx.fillStyle = this.randomColor(0, 255)
ctx.beginPath()
ctx.arc(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight), 1, 0, 2 * Math.PI)
ctx.fill()
}
},
reloadPic(){
this.drawPic()
},
randomCode(){
let random = ''
//去掉了I l i o O
let str = "QWERTYUPLKJHGFDSAZXCVBNMqwertyupkjhgfdsazxcvbnm1234567890"
for(let i = 0; i < this.length; i++) {
let index = Math.floor(Math.random()*57);
random += str[index];
}
this.code = random
}
},
mounted () {
this.drawPic()
},
data(){
return {
code:""
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,110 @@
<template>
<a-modal
title="导入EXCEL"
:width="600"
:visible="visible"
:confirmLoading="uploading"
@cancel="handleClose">
<a-upload
name="file"
:multiple="true"
accept=".xls,.xlsx"
:fileList="fileList"
:remove="handleRemove"
:beforeUpload="beforeUpload">
<a-button>
<a-icon type="upload" />
选择导入文件
</a-button>
</a-upload>
<template slot="footer">
<a-button @click="handleClose">关闭</a-button>
<a-button
type="primary"
@click="handleImport"
:disabled="fileList.length === 0"
:loading="uploading">
{{ uploading ? '上传中...' : '开始上传' }}
</a-button>
</template>
</a-modal>
</template>
<script>
import { postAction } from '@/api/manage'
export default {
name: 'JImportModal',
props:{
url:{
type: String,
default: '',
required: false
}
},
data(){
return {
visible:false,
uploading:false,
fileList:[],
uploadAction:''
}
},
watch: {
url (val) {
if(val){
this.uploadAction = window._CONFIG['domianURL']+val
}
}
},
created () {
this.uploadAction = window._CONFIG['domianURL']+this.url
},
methods:{
handleClose(){
this.visible=false
},
show(){
this.fileList = []
this.uploading = false
this.visible = true
},
handleRemove(file) {
const index = this.fileList.indexOf(file);
const newFileList = this.fileList.slice();
newFileList.splice(index, 1);
this.fileList = newFileList
},
beforeUpload(file) {
this.fileList = [...this.fileList, file]
return false;
},
handleImport() {
const { fileList } = this;
const formData = new FormData();
fileList.forEach((file) => {
formData.append('files[]', file);
});
this.uploading = true
postAction(this.uploadAction, formData).then((res) => {
this.uploading = false
if(res.success){
this.$message.success(res.message)
this.visible=false
this.$emit('ok')
}else{
this.$message.warning(res.message)
}
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,66 @@
<template>
<a-select :value="arrayValue" @change="onChange" mode="multiple" :placeholder="placeholder">
<a-select-option
v-for="(item,index) in options"
:key="index"
:value="item.value">
{{ item.text || item.label }}
</a-select-option>
</a-select>
</template>
<script>
//option {label:,value:}
export default {
name: 'JSelectMultiple',
props: {
placeholder:{
type: String,
default:'',
required: false
},
value:{
type: String,
required: false
},
readOnly:{
type: Boolean,
required: false,
default: false
},
options:{
type: Array,
required: true
},
triggerChange:{
type: Boolean,
required: false,
default: false
}
},
data(){
return {
arrayValue:!this.value?[]:this.value.split(",")
}
},
watch:{
value (val) {
if(!val){
this.arrayValue = []
}else{
this.arrayValue = this.value.split(",")
}
}
},
methods:{
onChange (selectedValue) {
if(this.triggerChange){
this.$emit('change', selectedValue.join(","));
}else{
this.$emit('input', selectedValue.join(","));
}
},
},
}
</script>

View File

@ -0,0 +1,111 @@
<template>
<div class="drag" ref="dragDiv">
<div class="drag_bg"></div>
<div class="drag_text">{{confirmWords}}</div>
<div ref="moveDiv" @mousedown="mousedownFn($event)" :class="{'handler_ok_bg':confirmSuccess}" class="handler handler_bg" style="border: 0.5px solid #fff;height: 34px;position: absolute;top: 0px;left: 0px;"></div>
</div>
</template>
<script>
export default {
name:"JSlider",
data(){
return {
beginClientX:0, /*距离屏幕左端距离*/
mouseMoveStata:false, /*触发拖动状态 判断*/
maxwidth:'', /*拖动最大宽度,依据滑块宽度算出来的*/
confirmWords:'拖动滑块验证', /*滑块文字*/
confirmSuccess:false /*验证成功判断*/
}
},
methods: {
isSuccess(){
return this.confirmSuccess
},
mousedownFn:function (e) {
if(!this.confirmSuccess){
e.preventDefault && e.preventDefault(); //阻止文字选中等 浏览器默认事件
this.mouseMoveStata = true;
this.beginClientX = e.clientX;
}
}, //mousedoen 事件
successFunction(){
this.confirmSuccess = true
this.confirmWords = '验证通过';
if(window.addEventListener){
document.getElementsByTagName('html')[0].removeEventListener('mousemove',this.mouseMoveFn);
document.getElementsByTagName('html')[0].removeEventListener('mouseup',this.moseUpFn);
}else {
document.getElementsByTagName('html')[0].removeEventListener('mouseup',()=>{});
}
document.getElementsByClassName('drag_text')[0].style.color = '#fff'
document.getElementsByClassName('handler')[0].style.left = this.maxwidth + 'px';
document.getElementsByClassName('drag_bg')[0].style.width = this.maxwidth + 'px';
this.$emit("onSuccess",true)
}, //验证成功函数
mouseMoveFn(e){
if(this.mouseMoveStata){
let width = e.clientX - this.beginClientX;
if(width>0 && width<=this.maxwidth){
document.getElementsByClassName('handler')[0].style.left = width + 'px';
document.getElementsByClassName('drag_bg')[0].style.width = width + 'px';
}else if(width>this.maxwidth){
this.successFunction();
}
}
}, //mousemove事件
moseUpFn(e){
this.mouseMoveStata = false;
var width = e.clientX - this.beginClientX;
if(width<this.maxwidth){
document.getElementsByClassName('handler')[0].style.left = 0 + 'px';
document.getElementsByClassName('drag_bg')[0].style.width = 0 + 'px';
}
} //mouseup事件
},
mounted(){
this.maxwidth = this.$refs.dragDiv.clientWidth - this.$refs.moveDiv.clientWidth;
document.getElementsByTagName('html')[0].addEventListener('mousemove',this.mouseMoveFn);
document.getElementsByTagName('html')[0].addEventListener('mouseup',this.moseUpFn)
}
}
</script>
<style scoped>
.drag{
position: relative;
background-color: #e8e8e8;
width: 100%;
height: 34px;
line-height: 34px;
text-align: center;
}
.handler{
width: 40px;
height: 32px;
border: 1px solid #ccc;
cursor: move;
}
.handler_bg{
background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTEyNTVEMURGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTEyNTVEMUNGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2MTc5NzNmZS02OTQxLTQyOTYtYTIwNi02NDI2YTNkOWU5YmUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YiRG4AAAALFJREFUeNpi/P//PwMlgImBQkA9A+bOnfsIiBOxKcInh+yCaCDuByoswaIOpxwjciACFegBqZ1AvBSIS5OTk/8TkmNEjwWgQiUgtQuIjwAxUF3yX3xyGIEIFLwHpKyAWB+I1xGSwxULIGf9A7mQkBwTlhBXAFLHgPgqEAcTkmNCU6AL9d8WII4HOvk3ITkWJAXWUMlOoGQHmsE45ViQ2KuBuASoYC4Wf+OUYxz6mQkgwAAN9mIrUReCXgAAAABJRU5ErkJggg==") no-repeat center;
}
.handler_ok_bg{
background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDlBRDI3NjVGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDlBRDI3NjRGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDphNWEzMWNhMC1hYmViLTQxNWEtYTEwZS04Y2U5NzRlN2Q4YTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+k+sHwwAAASZJREFUeNpi/P//PwMyKD8uZw+kUoDYEYgloMIvgHg/EM/ptHx0EFk9I8wAoEZ+IDUPiIMY8IN1QJwENOgj3ACo5gNAbMBAHLgAxA4gQ5igAnNJ0MwAVTsX7IKyY7L2UNuJAf+AmAmJ78AEDTBiwGYg5gbifCSxFCZoaBMCy4A4GOjnH0D6DpK4IxNSVIHAfSDOAeLraJrjgJp/AwPbHMhejiQnwYRmUzNQ4VQgDQqXK0ia/0I17wJiPmQNTNBEAgMlQIWiQA2vgWw7QppBekGxsAjIiEUSBNnsBDWEAY9mEFgMMgBk00E0iZtA7AHEctDQ58MRuA6wlLgGFMoMpIG1QFeGwAIxGZo8GUhIysmwQGSAZgwHaEZhICIzOaBkJkqyM0CAAQDGx279Jf50AAAAAABJRU5ErkJggg==") no-repeat center;
}
.drag_bg{
background-color: #7ac23c;
height: 34px;
width: 0px;
}
.drag_text{
position: absolute;
top: 0px;
width: 100%;text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
-o-user-select:none;
-ms-user-select:none;
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<a-modal
title="高级查询构造器"
:width="800"
:visible="visible"
:confirmLoading="confirmLoading"
@cancel="handleCancel"
:mask="false"
wrapClassName="ant-modal-cust-warp"
style="top:5%;max-height: 95%;">
<template slot="footer">
<a-button @click="handleCancel"> </a-button>
<a-button @click="handleReset" style="float: left"> </a-button>
<a-button type="primary" @click="handleOk"> </a-button>
</template>
<a-spin :spinning="confirmLoading">
<a-form>
<div>
<a-row type="flex" style="margin-bottom:10px" :gutter="16" v-for="(item, index) in queryParamsModel" :key="index">
<a-col :span="6">
<a-select placeholder="选择查询字段" v-model="item.field" @select="(val,option)=>handleSelected(option,item)">
<a-select-option v-for="(f,fIndex) in fieldList" :key=" 'field'+fIndex" :value="f.value" :data-type="f.type">{{ f.text }}</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-select placeholder="选择匹配规则" v-model="item.rule">
<a-select-option value="eq">等于</a-select-option>
<a-select-option value="ne">不等于</a-select-option>
<a-select-option value="gt">大于</a-select-option>
<a-select-option value="ge">大于等于</a-select-option>
<a-select-option value="lt">小于</a-select-option>
<a-select-option value="le">小于等于</a-select-option>
<a-select-option value="right_like">..开始</a-select-option>
<a-select-option value="left_like">..结尾</a-select-option>
<a-select-option value="like">包含</a-select-option>
<a-select-option value="in">...</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<j-date v-if=" item.type=='date' " v-model="item.val" placeholder="请选择日期"></j-date>
<j-date v-else-if=" item.type=='datetime' " v-model="item.val" placeholder="请选择时间" :show-time="true" date-format="YYYY-MM-DD HH:mm:ss"></j-date>
<a-input-number v-else-if=" item.type=='int'||item.type=='number' " style="width: 100%" placeholder="请输入数值" v-model="item.val"/>
<a-input v-else v-model="item.val" placeholder="请输入值" />
</a-col>
<a-col :span="6">
<a-button @click="handleAdd" icon="plus"></a-button>&nbsp;
<a-button @click="handleDel( index )" icon="minus"></a-button>
</a-col>
</a-row>
</div>
</a-form>
</a-spin>
</a-modal>
</template>
<script>
import ACol from 'ant-design-vue/es/grid/Col'
import JDate from '@/components/jeecg/JDate.vue';
export default {
name: 'JSuperQuery',
components: {
ACol,
JDate
},
data(){
return {
visible:false,
confirmLoading:false,
queryParamsModel:[{}]
}
},
props:{
/* fieldList:[{value:'',text:'',type:''}]
* type:date datetime int number string
* */
fieldList:{
type:Array,
required:true
},
/*
* 这个回调函数接收一个数组参数 即查询条件
* */
callback:{
type:String,
required:false,
default:'handleSuperQuery'
}
},
methods:{
show(){
if(!this.queryParamsModel ||this.queryParamsModel.length==0){
this.queryParamsModel = [{}]
}
this.visible = true;
},
handleOk(){
console.log("---高级查询参数--->",this.queryParamsModel)
if(!this.isNullArray()){
this.$emit(this.callback, this.queryParamsModel)
}else{
this.$emit(this.callback)
}
},
handleCancel(){
this.close()
},
close () {
this.$emit('close');
this.visible = false;
},
handleAdd () {
this.queryParamsModel.push({});
},
handleDel (index) {
this.queryParamsModel.splice(index,1);
this.$message.warning("请关闭后重新打开")
},
handleSelected(option,item){
item['type'] = option.data.attrs['data-type']
},
handleReset(){
this.queryParamsModel=[{}]
this.$emit(this.callback)
},
isNullArray(){
//判断是不是空数组对象
if(!this.queryParamsModel || this.queryParamsModel.length==0){
return true
}
if(this.queryParamsModel.length==1){
let obj = this.queryParamsModel[0]
if(!obj.field || !obj.val || !obj.rule){
return true
}
}
return false;
}
}
}
</script>
<style >
</style>

View File

@ -0,0 +1,195 @@
<template>
<a-tree-select
allowClear
labelInValue
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="placeholder"
:loadData="asyncLoadTreeData"
:value="treeValue"
:treeData="treeData"
@change="onChange"
@search="onSearch">
</a-tree-select>
</template>
<script>
import { getAction } from '@/api/manage'
export default {
name: 'JTreeDict',
data(){
return {
treeData:[],
treeValue:"",
url_root:"/sys/category/loadTreeRoot",
url_children:"/sys/category/loadTreeChildren",
url_view:'/sys/category/loadOne',
}
},
props:{
value:{
type: String,
required: false
},
placeholder:{
type: String,
default: '请选择',
required: false
},
parentCode:{
type: String,
default: '',
required: false
},
field:{
type: String,
default: 'id',
required: false
},
root:{
type:Object,
required:false,
default:()=>{
return {
pid:'0'
}
}
},
async:{
type:Boolean,
default:false,
required:false
},
disabled:{
type:Boolean,
default:false,
required:false
}
},
watch:{
root:{
handler(val){
console.log("root-change",val)
},
deep:true
},
parentCode:{
handler(){
this.loadRoot()
}
},
value:{
handler(){
this.loadViewInfo()
}
}
},
created(){
this.loadRoot()
this.loadViewInfo()
},
model: {
prop: 'value',
event: 'change'
},
methods:{
loadViewInfo(){
if(!this.value || this.value=="0"){
this.treeValue = ""
}else{
let param = {
field:this.field,
val:this.value
}
getAction(this.url_view,param).then(res=>{
if(res.success){
this.treeValue = {
value:this.value,
label:res.result.name
}
}
})
}
},
loadRoot(){
let param = {
async:this.async,
pcode:this.parentCode
}
getAction(this.url_root,param).then(res=>{
if(res.success){
this.handleTreeNodeValue(res.result)
console.log("aaaa",res.result)
this.treeData = [...res.result]
}else{
this.$message.error(res.message)
}
})
},
asyncLoadTreeData (treeNode) {
return new Promise((resolve) => {
if(!this.async){
resolve()
return
}
if (treeNode.$vnode.children) {
resolve()
return
}
let pid = treeNode.$vnode.key
let param = {
pid:pid
}
getAction(this.url_children,param).then(res=>{
if(res.success){
this.handleTreeNodeValue(res.result)
this.addChildren(pid,res.result,this.treeData)
this.treeData = [...this.treeData]
}
resolve()
})
})
},
addChildren(pid,children,treeArray){
if(treeArray && treeArray.length>0){
for(let item of treeArray){
if(item.key == pid){
if(!children || children.length==0){
item.leaf = true
}else{
item.children = children
}
break
}else{
this.addChildren(pid,children,item.children)
}
}
}
},
handleTreeNodeValue(result){
let storeField = this.field=='code'?'code':'key'
for(let i of result){
i.value = i[storeField]
i.isLeaf = (!i.leaf)?false:true
if(i.children && i.children.length>0){
this.handleTreeNodeValue(i.children)
}
}
},
onChange(value){
console.log(value)
this.$emit('change', value.value);
this.treeValue = value
},
onSearch(value){
console.log(value)
},
getCurrTreeData(){
return this.treeData
}
}
}
</script>

View File

@ -0,0 +1,206 @@
<template>
<a-tree-select
allowClear
labelInValue
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="placeholder"
:loadData="asyncLoadTreeData"
:value="treeValue"
:treeData="treeData"
@change="onChange"
@search="onSearch">
</a-tree-select>
</template>
<script>
/*
* 异步树加载组件 通过传入表名 显示字段 存储字段 加载一个树控件
* <j-tree-select dict="aa_tree_test,aad,id" pid-field="pid" ></j-tree-select>
* */
import { getAction } from '@/api/manage'
export default {
name: 'JTreeSelect',
props: {
value:{
type: String,
required: false
},
placeholder:{
type: String,
default: '请选择',
required: false
},
dict:{
type: String,
default: '',
required: false
},
pidField:{
type: String,
default: 'pid',
required: false
},
pidValue:{
type: String,
default: '0',
required: false
},
disabled:{
type:Boolean,
default:false,
required:false
},
hasChildField:{
type: String,
default: '',
required: false
}
},
data () {
return {
treeValue:"",
treeData:[],
url:"/sys/dict/loadTreeData",
view:'/sys/dict/loadDictItem/',
tableName:"",
text:"",
code:"",
}
},
watch: {
value () {
this.loadItemByCode()
},
dict(){
this.initDictInfo()
this.loadRoot();
}
},
created(){
this.initDictInfo()
this.loadRoot()
this.loadItemByCode()
},
methods: {
loadItemByCode(){
if(!this.value || this.value=="0"){
this.treeValue = ""
}else{
getAction(`${this.view}${this.dict}`,{key:this.value}).then(res=>{
if(res.success){
this.treeValue = {
key:this.value,
value:this.value,
label:res.result
}
}
})
}
},
initDictInfo(){
let arr = this.dict.split(",")
this.tableName = arr[0]
this.text = arr[1]
this.code = arr[2]
},
asyncLoadTreeData (treeNode) {
return new Promise((resolve) => {
if (treeNode.$vnode.children) {
resolve()
return
}
let pid = treeNode.$vnode.key
let param = {
pid:pid,
tableName:this.tableName,
text:this.text,
code:this.code,
pidField:this.pidField,
hasChildField:this.hasChildField
}
getAction(this.url,param).then(res=>{
if(res.success){
for(let i of res.result){
i.value = i.key
if(i.leaf==false){
i.isLeaf=false
}else if(i.leaf==true){
i.isLeaf=true
}
}
this.addChildren(pid,res.result,this.treeData)
this.treeData = [...this.treeData]
}
resolve()
})
})
},
addChildren(pid,children,treeArray){
if(treeArray && treeArray.length>0){
for(let item of treeArray){
if(item.key == pid){
if(!children || children.length==0){
item.isLeaf=true
}else{
item.children = children
}
break
}else{
this.addChildren(pid,children,item.children)
}
}
}
},
loadRoot(){
let param = {
pid:this.pidValue,
tableName:this.tableName,
text:this.text,
code:this.code,
pidField:this.pidField,
hasChildField:this.hasChildField
}
getAction(this.url,param).then(res=>{
if(res.success && res.result){
for(let i of res.result){
i.value = i.key
if(i.leaf==false){
i.isLeaf=false
}else if(i.leaf==true){
i.isLeaf=true
}
}
this.treeData = [...res.result]
}else{
console.log("数根节点查询结果-else",res)
}
})
},
onChange(value){
if(!value){
this.$emit('change', '');
this.treeValue = ''
}else{
this.$emit('change', value.value);
this.treeValue = value
}
},
onSearch(value){
console.log(value)
},
getCurrTreeData(){
return this.treeData
}
},
//2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型 这个牛逼
model: {
prop: 'value',
event: 'change'
}
}
</script>

View File

@ -0,0 +1,140 @@
<template>
<a-table
:rowKey="rowKey"
:columns="columns"
:dataSource="dataSource"
v-bind="tableProps"
@expand="handleExpand">
<template v-for="(slotItem) of slots" :slot="slotItem" slot-scope="text, record, index">
<slot :name="slotItem" v-bind="{text,record,index}"></slot>
</template>
</a-table>
</template>
<script>
import { getAction } from '@/api/manage'
export default {
name: 'JTreeTable',
props: {
rowKey: {
type: String,
default: 'id'
},
// 根据什么查询,如果传递 id 就根据 id 查询
queryKey: {
type: String,
default: 'parentId'
},
queryParams: {
type: Object,
default: () => {
}
},
// 查询顶级时的值如果顶级为0则传0
topValue: {
type: String,
default: null
},
columns: {
type: Array,
required: true
},
url: {
type: String,
required: true
},
childrenUrl: {
type: String,
default: null
},
tableProps: {
type: Object,
default: () => {
}
}
},
data() {
return {
dataSource: []
}
},
computed: {
getChildrenUrl() {
if (this.childrenUrl) {
return this.childrenUrl
} else {
return this.url
}
},
slots() {
let slots = []
for (let column of this.columns) {
if (column.scopedSlots && column.scopedSlots.customRender) {
slots.push(column.scopedSlots.customRender)
}
}
return slots
}
},
watch: {
queryParams: {
deep: true,
handler() {
this.loadData()
}
}
},
created() {
this.loadData()
},
methods: {
/** 加载数据*/
loadData(id = this.topValue, first = true, url = this.url) {
let params = Object.assign({}, this.queryParams || {})
params[this.queryKey] = id
return getAction(url, params).then(res => {
let dataSource = res.result.map(item => {
// 判断是否标记了带有子级
if (item.hasChildren === true) {
// 定义默认展开时显示的loading子级实际子级数据只在展开时加载
let loadChild = { id: `${item.id}_loadChild`, name: 'loading...', isLoading: true }
item.children = [loadChild]
}
return item
})
if (first) {
this.dataSource = dataSource
}
return Promise.resolve(dataSource)
})
},
/** 点击展开图标时触发 */
handleExpand(expanded, record) {
// 判断是否是展开状态
if (expanded) {
// 判断子级的首个项的标记是否是“正在加载中”,如果是就加载数据
if (record.children[0].isLoading === true) {
this.loadData(record.id, false, this.getChildrenUrl).then(dataSource => {
// 处理好的数据可直接赋值给children
if (dataSource.length === 0) {
record.children = null
} else {
record.children = dataSource
}
})
}
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,173 @@
<template>
<a-upload
name="file"
:multiple="true"
:action="uploadAction"
:headers="headers"
:data="{'isup':1,'bizPath':bizPath}"
:fileList="fileList"
:beforeUpload="beforeUpload"
@change="handleChange">
<a-button>
<a-icon type="upload" />{{ text }}
</a-button>
</a-upload>
</template>
<script>
import Vue from 'vue'
import { ACCESS_TOKEN } from "@/store/mutation-types"
const FILE_TYPE_ALL = "all"
const FILE_TYPE_IMG = "image"
const FILE_TYPE_TXT = "file"
const uidGenerator=()=>{
return '-'+parseInt(Math.random()*10000+1,10);
}
const getFileName=(path)=>{
if(path.lastIndexOf("\\")>=0){
let reg=new RegExp("\\\\","g");
path = path.replace(reg,"/");
}
return path.substring(path.lastIndexOf("/")+1);
}
export default {
name: 'JUpload',
data(){
return {
uploadAction:window._CONFIG['domianURL']+"/sys/common/upload",
urlDownload:window._CONFIG['domianURL'] + "/sys/common/download/",
headers:{},
fileList: []
}
},
props:{
text:{
type:String,
required:false,
default:"点击上传"
},
fileType:{
type:String,
required:false,
default:FILE_TYPE_ALL
},
/*这个属性用于控制文件上传的业务路径*/
bizPath:{
type:String,
required:false,
default:"temp"
},
value:{
type:String,
required:false
},
//此属性被废弃了
triggerChange:{
type: Boolean,
required: false,
default: false
},
},
watch:{
value(val){
this.initFileList(val)
}
},
created(){
const token = Vue.ls.get(ACCESS_TOKEN);
this.headers = {"X-Access-Token":token}
},
methods:{
initFileList(paths){
if(!paths || paths.length==0){
return [];
}
let fileList = [];
let arr = paths.split(",")
for(var a=0;a<arr.length;a++){
fileList.push({
uid:uidGenerator(),
name:getFileName(arr[a]),
status: 'done',
url: this.urlDownload+arr[a],
response:{
status:"history",
message:arr[a]
}
})
}
this.fileList = fileList
},
handlePathChange(){
let uploadFiles = this.fileList
let path = ''
if(!uploadFiles || uploadFiles.length==0){
path = ''
}
let arr = [];
for(var a=0;a<uploadFiles.length;a++){
arr.push(uploadFiles[a].response.message)
}
if(arr.length>0){
path = arr.join(",")
}
this.$emit('change', path);
},
beforeUpload(file){
var fileType = file.type;
if(fileType===FILE_TYPE_IMG){
if(fileType.indexOf('image')<0){
this.$message.warning('请上传图片');
return false;
}
}else if(fileType===FILE_TYPE_TXT){
if(fileType.indexOf('image')>=0){
this.$message.warning('请上传文件');
return false;
}
}
//TODO 扩展功能验证文件大小
return true
},
handleChange(info) {
console.log("--文件列表改变--")
let fileList = info.fileList
if(info.file.status==='done'){
if(info.file.response.success){
fileList = fileList.map((file) => {
if (file.response) {
file.url = this.urlDownload+file.response.message;
}
return file;
});
}
this.$message.success(`${info.file.name} 上传成功!`);
}else if (info.file.status === 'error') {
this.$message.error(`${info.file.name} 上传失败.`);
}else if(info.file.status === 'removed'){
this.handleDelete(info.file)
}
this.fileList = fileList
if(info.file.status==='done' || info.file.status === 'removed'){
this.handlePathChange()
}
},
handleDelete(file){
//如有需要新增 删除逻辑
console.log(file)
},
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,511 @@
# JDate 日期组件 使用文档
###### 说明: antd-vue日期组件需要用moment中转一下用起来不是很方便特二次封装使用时只需要传字符串即可
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| readOnly | boolean | | true/false 默认false |
| value | string | | 绑定v-model或是v-decorator后不需要设置 |
| showTime | boolean | | 是否展示时间true/false 默认false |
| dateFormat | string | |日期格式 默认'YYYY-MM-DD' 若showTime设置为true则需要将其设置成对应的时间格式(如:YYYY-MM-DD HH:mm:ss) |
| triggerChange | string | |触发组件值改变的事件是否是change,当使用v-decorator时且没有设置decorator的option.trigger为input需要设置该值为true |
使用示例
----
1.组件带有v-model的使用方法
```vue
<j-date v-model="dateStr"></j-date>
```
2.组件带有v-decorator的使用方法
a).设置trigger-change属性为true
```vue
<j-date :trigger-change="true" v-decorator="['dateStr',{}]"></j-date>
```
b).设置decorator的option.trigger为input
```vue
<j-date v-decorator="['dateStr',{trigger:'input'}]"></j-date>
```
3.其他使用
添加style
```vue
<j-date v-model="dateStr" style="width:100%"></j-date>
```
添加placeholder
```vue
<j-date v-model="dateStr" placeholder="请输入dateStr"></j-date>
```
添加readOnly
```vue
<j-date v-model="dateStr" :read-only="true"></j-date>
```
备注:
script内需引入jdate
```vue
<script>
import JDate from '@/components/jeecg/JDate'
export default {
name: "demo",
components: {
JDate
}
//...
}
</script>
```
---
# JSuperQuery 高级查询 使用文档
## 参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------------|---------|----|----------------------|
| fieldList | array |✔| 需要查询的列集合示例如下type类型有:date/datetime/string/int/number |
| callback | array | | 回调函数名称(非必须)默认handleSuperQuery |
fieldList结构示例
```vue
const superQueryFieldList=[{
type:"date",
value:"birthday",
text:"生日"
},{
type:"string",
value:"name",
text:"用户名"
},{
type:"int",
value:"age",
text:"年龄"
}]
```
页面代码概述:
----
1.import之后再components之内声明
```vue
import JSuperQuery from '@/components/jeecg/JSuperQuery.vue';
export default {
name: "JeecgDemoList",
components: {
JSuperQuery
},
```
2.页面引用
```vue
<!-- 高级查询区域 -->
<j-super-query :fieldList="fieldList" ref="superQueryModal" @handleSuperQuery="handleSuperQuery"></j-super-query>
```
3.list页面data中需要定义三个属性
```vue
fieldList:superQueryFieldList,
superQueryFlag:false,
superQueryParams:""
```
4.list页面声明回调事件handleSuperQuery(与组件的callback对应即可)
```vue
//高级查询方法
handleSuperQuery(arg) {
if(!arg){
this.superQueryParams=''
this.superQueryFlag = false
}else{
this.superQueryFlag = true
this.superQueryParams=JSON.stringify(arg)
}
this.loadData()
},
```
5.改造list页面方法
```vue
// 获取查询条件
getQueryParams() {
let sqp = {}
if(this.superQueryParams){
sqp['superQueryParams']=encodeURI(this.superQueryParams)
}
var param = Object.assign(sqp, this.queryParam, this.isorter);
param.field = this.getQueryField();
param.pageNo = this.ipagination.current;
param.pageSize = this.ipagination.pageSize;
return filterObj(param);
},
```
6.打开弹框调用show方法
```vue
this.$refs.superQueryModal.show();
```
# JEllipsis 字符串超长截取省略号显示
###### 说明: 遇到超长文本展示,通过此标签可以截取省略号显示,鼠标放置会提示全文本
## 参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------|---------|----|----------------|
| value |string | 必填 | 字符串文本|
| length | number | 非必填 | 默认25 |
使用示例
----
1.组件带有v-model的使用方法
```vue
<j-ellipsis :value="text"/>
# Modal弹框实现最大化功能
1.定义modal的宽度
```vue
<a-modal
:width="modalWidth"
/>
```
2.自定义modal的title,居右显示切换图标
```vue
<template slot="title">
<div style="width: 100%;">
<span>{{ title }}</span>
<span style="display:inline-block;width:calc(100% - 51px);padding-right:10px;text-align: right">
<a-button @click="toggleScreen" icon="appstore" style="height:20px;width:20px;border:0px"></a-button>
</span>
</div>
</template>
```
3.定义toggleScreen事件,用于切换modal宽度
```vue
toggleScreen(){
if(this.modaltoggleFlag){
this.modalWidth = window.innerWidth;
}else{
this.modalWidth = 800;
}
this.modaltoggleFlag = !this.modaltoggleFlag;
},
```
4.data中声明上述用到的属性
```vue
data () {
return {
modalWidth:800,
modaltoggleFlag:true,
```
# <a-select/> 下拉选项滚动错位的解决方法
## 问题描述
当使用了 `a-modal` 或其他带有滚动条的组件时,使用`a-select`组件并打开下拉框时滚动滚动条,就会导致错位的问题产生。
## 解决方法
大多数情况下,在 `a-select` 上添加一个 `getPopupContainer` 属性,值为`node => node.parentNode`即可解决。
但是如果遇到 `a-select` 标签层级过深的情况,可能仍然会显示异常,只需要多加几个`.parentNode` node => node.parentNode.parentNode.parentNode多尝试几次直到解决问题即可。
### 代码示例
```html
<a-select
placeholder="请选择展示模板"
:options="dicts.displayTemplate"
:getPopupContainer="node => node.parentNode"
/>
```
# JAsyncTreeList 异步数列表组件使用说明
## 引入组件
```js
import JTreeTable from '@/components/jeecg/JTreeTable'
export default {
components: { JTreeTable }
}
```
## 所需参数
| 参数 | 类型 | 必填 | 说明 |
|-------------|--------|--------|--------------------------------------------------------------|
| rowKey | String | 非必填 | 表格行 key 的取值,默认为"id" |
| columns | Array | 必填 | 表格列的配置描述具体见Antd官方文档 |
| url | String | 必填 | 数据查询url |
| childrenUrl | String | 非必填 | 查询子级时的url若不填则使用url参数查询子级 |
| queryKey | String | 非必填 | 根据某个字段查询,如果传递 id 就根据 id 查询默认为parentId |
| queryParams | Object | 非必填 | 查询参数,当查询参数改变的时候会自动重新查询,默认为{} |
| topValue | String | 非必填 | 查询顶级时的值如果顶级为0则传0默认为null |
| tableProps | Object | 非必填 | 自定义给内部table绑定的props |
## 代码示例
```html
<template>
<a-card :bordered="false">
<j-tree-table :url="url" :columns="columns" :tableProps="tableProps"/>
</a-card>
</template>
<script>
import JTreeTable from '@/components/jeecg/JTreeTable'
export default {
name: 'AsyncTreeTable',
components: { JTreeTable },
data() {
return {
url: '/api/asynTreeList',
columns: [
{ title: '菜单名称', dataIndex: 'name' },
{ title: '组件', dataIndex: 'component' },
{ title: '排序', dataIndex: 'orderNum' }
],
selectedRowKeys: []
}
},
computed: {
tableProps() {
let _this = this
return {
// 列表项是否可选择
// 配置项见https://vue.ant.design/components/table-cn/#rowSelection
rowSelection: {
selectedRowKeys: _this.selectedRowKeys,
onChange: (selectedRowKeys) => _this.selectedRowKeys = selectedRowKeys
}
}
}
}
}
</script>
```
# JCheckbox 使用文档
###### 说明: antd-vue checkbox组件处理的是数组用起来不是很方便特二次封装使用时只需处理字符串即可
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| options |array |✔| checkbox需要配置的项是个数组数组中每个对象包含两个属性:label(用于显示)和value(用于存储) |
使用示例
----
```vue
<template>
<a-form :form="form">
<a-form-item label="v-model式用法">
<j-checkbox v-model="sport" :options="sportOptions"></j-checkbox><span>{{ sport }}</span>
</a-form-item>
<a-form-item label="v-decorator式用法">
<j-checkbox v-decorator="['sport']" :options="sportOptions"></j-checkbox><span>{{ getFormFieldValue('sport') }}</span>
</a-form-item>
</a-form>
</template>
<script>
import JCheckbox from '@/components/jeecg/JCheckbox'
export default {
components: {JCheckbox},
data() {
return {
form: this.$form.createForm(this),
sport:'',
sportOptions:[
{
label:"足球",
value:"1"
},{
label:"篮球",
value:"2"
},{
label:"乒乓球",
value:"3"
}]
}
},
methods: {
getFormFieldValue(field){
return this.form.getFieldValue(field)
}
}
}
</script>
```
# JCodeEditor 使用文档
###### 说明: 一个简易版的代码编辑器,支持语法高亮
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| language |string | | 表示当前编写代码的类型 javascript/html/css/sql |
| placeholder |string | | placeholder |
| lineNumbers |Boolean | | 是否显示行号 |
| fullScreen |Boolean | | 是否显示全屏按钮 |
| zIndex |string | | 全屏以后的z-index |
使用示例
----
```vue
<template>
<div>
<j-code-editor
language="javascript"
v-model="editorValue"
:fullScreen="true"
style="min-height: 100px"/>
{{ editorValue }}
</div>
</template>
<script>
import JCodeEditor from '@/components/jeecg/JCodeEditor'
export default {
components: {JCodeEditor},
data() {
return {
form: this.$form.createForm(this),
editorValue:'',
}
}
}
</script>
```
# JFormContainer 使用文档
###### 说明: 暂用于表单禁用
使用示例
----
```vue
<!-- 在form下直接写这个组件设置disabled为true就能将此form中的控件禁用 -->
<a-form layout="inline" :form="form" >
<j-form-container disabled>
<!-- 表单内容省略..... -->
</j-form-container>
</a-form>
```
# JImportModal 使用文档
###### 说明: 用于列表页面导入excel功能
使用示例
----
```vue
<template>
<!-- 此处省略部分代码...... -->
<a-button @click="handleImportXls" type="primary" icon="upload">导入</a-button>
<!-- 此处省略部分代码...... -->
<j-import-modal ref="importModal" :url="getImportUrl()" @ok="importOk"></j-import-modal>
<!-- 此处省略部分代码...... -->
</template>
<script>
import JCodeEditor from '@/components/jeecg/JCodeEditor'
export default {
components: {JCodeEditor},
data() {
return {
//省略代码......
}
},
methods:{
//省略部分代码......
handleImportXls(){
this.$refs.importModal.show()
},
getImportUrl(){
return '你自己处理上传业务的后台地址'
},
importOk(){
this.loadData(1)
}
}
}
</script>
```
# JSelectMultiple 多选下拉组件
online用 实际开发请使用components/dict/JMultiSelectTag
# JSlider 滑块验证码
使用示例
----
```vue
<template>
<div style="width: 300px">
<j-slider @onSuccess="sliderSuccess"></j-slider>
</div>
</template>
<script>
import JSlider from '@/components/jeecg/JSlider'
export default {
components: {JSlider},
data() {
return {
form: this.$form.createForm(this),
editorValue:'',
}
},
methods:{
sliderSuccess(){
console.log("验证完成")
}
}
}
</script>
```
# JTreeSelect 树形下拉组件
异步加载的树形下拉组件
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| dict |string | ✔| 表名,显示字段名,存储字段名拼接的字符串 |
| pidField |string | ✔| 父ID的字段名 |
| pidValue |string | | 根节点父ID的值 默认'0' 不可以设置为空,如果想使用此组件而数据库根节点父ID为空请修改之 |
使用示例
----
```vue
<template>
<a-form>
<a-form-item label="树形下拉测试" style="width: 300px">
<j-tree-select
v-model="departId"
placeholder="请选择部门"
dict="sys_depart,depart_name,id"
pidField="parent_id">
</j-tree-select>
{{ departId }}
</a-form-item>
</a-form >
</template>
<script>
import JTreeSelect from '@/components/jeecg/JTreeSelect'
export default {
components: {JTreeSelect},
data() {
return {
departId:""
}
}
}
</script>
```

View File

@ -0,0 +1,507 @@
# JEditableTable 帮助文档
## 参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------------|---------|------|----------------------------------------------------------------|
| columns | array | ✔️ | 表格列的配置描述,具体项见下表 |
| dataSource | array | ✔️ | 表格数据 |
| loading | boolean | | 是否正在加载加载中不会显示任何行默认false |
| actionButton | boolean | | 是否显示操作按钮,包括"新增"、"删除"默认false |
| rowNumber | boolean | | 是否显示行号默认false |
| rowSelection | boolean | | 是否可选择行默认false |
| maxHeight | number | | 设定最大高度(px)默认400 |
| disabledRows | object | | 设定禁用的行,被禁用的行无法被选择和编辑,配置方法可以查看示例 |
| disabled | boolean | | 是否禁用所有行默认false |
### columns 参数详解
| 参数 | 类型 | 必填 | 说明 |
|---------------|--------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| title | string | ✔️ | 表格列头显示的问题 |
| key | string | ✔️ | 列数据在数据项中对应的 key必须是唯一的 |
| type | string | ✔️ | 表单的类型,可以通过`JEditableTableUtil.FormTypes`赋值 |
| width | string | | 列的宽度,可以是百分比,也可以是`px`或其他单位建议设置为百分比且每一列的宽度加起来不应超过100%,否则可能会不能达到预期的效果。留空会自动计算百分比 |
| placeholder | string | | 表单预期值的提示信息,可以使用`${...}`变量替换文本(详见`${...} 变量使用方式` |
| defaultValue | string | | 默认值,在新增一行时生效 |
| validateRules | array | | 表单验证规则,配置方式见[validateRules 配置规则](#validaterules-配置规则) |
| props | object | | 设置添加给表单元素的自定义属性,例如:`props:{title: 'show title'}` |
#### 当 type=checkbox 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|----------------|---------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| defaultChecked | boolean | | 默认值是否选中 |
| customValue | array | | 自定义值checkbox需要的是boolean值如果数据是其他值例如`'Y' or 'N'`)时,就会导致错误,所以提供了该属性进行转换,例:`customValue: ['Y','N']`,会将`true`转换为`'Y'``false`转换为`'N'`,反之亦然 |
#### 当 type=select 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|------------|---------|------|--------------------------------------|
| options | array | ✔️ | 下拉选项列表,详见下表 |
| allowInput | boolean | | 是否允许用户输入内容,并创建新的内容 |
##### options 所需参数
| 参数 | 类型 | 必填 | 说明 |
|-----------|------------|------|----------------------------------------------------------------------|
| text | string | ✔️ | 显示标题 |
| value | string | ✔️ | 真实值 |
| ~~title~~ | ~~string~~ | | ~~显示标题(已废弃,若同时填写了 title 和 text 那么优先使用 text~~ |
#### 当 type=upload 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|--------------|---------|------|--------------------------------------------------------------------------------------|
| action | string | ✔️ | 上传文件路径 |
| token | boolean | | 上传的时候是否传递token |
| responseName | string | ✔️ | 若要从上传成功后从response中取出返回的文件名那么这里填后台返回的包含文件名的字段名 |
#### 当 type=slot 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|----------|--------|------|------------|
| slotName | string | ✔️ | slot的名称 |
### validateRules 配置规则
`validateRules` 需要的是一个数组数组里每项都是一个规则规则是object类型规则的各个参数如下
- `required` 是否必填,可选值为`true`or`false`
- `pattern` 正则表达式验证,只有成功匹配该正则的值才能成功通过验证
- `message` 当验证未通过时显示的提示文本,可以使用`${...}`变量替换文本(详见`${...} 变量使用方式`
- 配置示例请看[示例二](#示例二)
## 事件
| 事件名 | 触发时机 | 参数 |
|-----------------|----------------------------------------------------|-------------------------------|
| added | 当添加行操作完成后触发 | |
| deleted | 当删除行操作完成后触发(批量删除操作只会触发一次) | `deleteIds` 被逻辑删除的id |
| selectRowChange | 当行被选中或取消选中时触发 | `selectedRowIds` 被选中行的id |
## 方法
关于方法的如何调用的问题,请在**FAQ**中查看[方法如何调用](#方法如何调用)
### initialize
用于初始化表格(清空表格)
- `参数:`
- `返回值:`
### resetScrollTop
重置滚动条Top位置
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|------|--------------------------------------------------------------------------------------------------------|
| top | number | | 新top位置留空则滚动到上次记录的位置用于解决切换tab选项卡时导致白屏以及自动将滚动条滚动到顶部的问题 |
- `返回值:`
### add
主动添加行,默认情况下,当用户的滚动条已经在底部的时候,会将滚动条固定在底部,即添加后无需用户手动滚动,而会自动滚动到底部
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|---------------------|---------|------|---------------------------------------------------------------------|
| num | number | | 添加几行默认为1 |
| forceScrollToBottom | boolean | | 是否在添加后无论用户的滚动条在什么位置都强制滚动到底部默认为false |
- `返回值:`
### removeRows
主动删除一行或多行
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|-----------------|------|--------------------------------------------------------------------------------------------|
| id | string 或 array | ✔️ | 被删除行的id。如果要删除一个可以直接传id如果要删除多个需要将多个id封装成一个数组传入 |
- `返回值:`
### removeSelectedRows
主动删除被选中的行
- `参数:`
- `返回值:`
### getValues
用于获取表格里所有表单的值,可进行表单验证
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|----------|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| callback | function | ✔️ | 获取值的回调方法,会传入`error``values`两个参数。`error`:未通过验证的数量,当等于`0`时代表验证通过;`values`:获取的值(即使未通过验证该字段也有数据) |
| validate | boolean | | 是否进行表单验证,默认为`true`,设为`false`则代表忽略表单验证 |
| rowIds | array | | 默认返回所有行的数据,如果传入了`rowIds`,那么就会只返回与该`rowIds`相匹配的数据,如果没有匹配的数据,就会返回空数组 |
- `返回值:`
### getValuesSync
`getValues`的同步版,会直接将获取到的数据返回
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|---------|--------|------|------------------------|
| options | object | | 选项,详见下方所需参数 |
- - `options` 所需参数
| 参数名 | 类型 | 必填 | 说明 |
|----------|---------|------|----------------------------------------------------------------------------------------------------------------------|
| validate | boolean | | 是否进行表单验证,默认为`true`,设为`false`则代表忽略表单验证 |
| rowIds | array | | 默认返回所有行的数据,如果传入了`rowIds`,那么就会只返回与该`rowIds`相匹配的数据,如果没有匹配的数据,就会返回空数组 |
- `返回值:` object
- `error` 未通过验证的数量,当等于`0`时代表验证通过
- `values` 获取的值(即使未通过验证该字段也有数据)
- `使用示例`
```js
let { error, values } = this.$refs.editableTable.getValuesSync({ validate: true, rowIds: ['rowId1', 'rowId2'] })
if (error === 0) {
console.log('表单验证通过,数据:', values);
} else {
console.log('未通过表单验证,数据:', values);
}
```
### getValuesPromise
`getValues`的promise版会在`resolve`中传入获取到的值,会在`reject`中传入失败原因,例如`VALIDATE_NO_PASSED`
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|----------|---------|------|----------------------------------------------------------------------------------------------------------------------|
| validate | boolean | | 同`getValues``validate`参数 |
| rowIds | array | | 默认返回所有行的数据,如果传入了`rowIds`,那么就会只返回与该`rowIds`相匹配的数据,如果没有匹配的数据,就会返回空数组 |
- `返回值:` Promise
### getDeleteIds
用于获取被逻辑删除的行的id返回一个数组用户可将该数组传入后台并进行批量删除
- `参数:`
- `返回值:` array
### getAll
获取所有的数据包括values、deleteIds
会在`resolve`中传入获取到的值:`{values, deleteIds}`
会在`reject`中传入失败原因,例如`VALIDATE_NO_PASSED`
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|----------|---------|------|-------------------------------|
| validate | boolean | | 同`getValues``validate`参数 |
- `返回值:` Promise
### setValues
主动设置表格中某行某列的值
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|-------|------|------------------------------------------------------------|
| values | array | | 传入一个数组,数组中的每项都是一行的新值,具体见下面的示例 |
- `返回值:`
- `示例:`
```js
setValues([
{
rowKey: id1, // 行的id
values: { // 在这里 values 中的 name 是你 columns 中配置的 key
'name': 'zhangsan',
'age': '20'
}
},
{
rowKey: id2,
values: {
'name': 'lisi',
'age': '23'
}
}
])
```
## ${...} 变量使用方式
`placeholder``message`这两个属性中可以使用`${...}`变量来替换文本
在[示例二](#示例二)中,配置了`title``名称`的一列,而`placeholder`配置成了`请输入${title}`,那么最终显示效果为`请输入名称`
这就是`${...}`变量的使用方式,在`${}`中可以使用的变量有`title``key``defaultValue`这三个属性的值
## JEditableTableUtil 使用说明
在之前配置`columns`时提到过`JEditableTableUtil`这个工具类,那么如果想要知道详细的使用说明就请看这里
### export 的常量
#### FormTypes
这是配置`columns.type`时用到的常量值,其中包括
- `normal` 默认,直接显示值,不渲染表单
- `input` 显示输入框
- `inputNumber` 显示数字输入框
- `checkbox` 显示多选框
- `select` 显示选择器(下拉框)
- `date` 日期选择器
- `datetime` 日期时间选择器
- `upload` 上传组件(文件域)
- `slot` 自定义插槽
### VALIDATE_NO_PASSED
在判断表单验证是否通过时使用,如果 reject 的值 === VALIDATE_NO_PASSED 则代表表单验证未通过,你可以做相应的其他处理,反之则可能是发生了报错,可以使用 `console.error` 输出
### 封装的方法
#### validateTables
当你的页面中存在多个JEditableTable实例的时候如果要获取每个实例的值、判断表单验证是否通过就会让代码变得极其冗余、繁琐于是我们就将该操作封装成了一个函数供你调用它可以同时获取并验证多个JEditableTable实例的值只有当所有实例的表单验证都通过后才会返回值否则将会告诉你具体哪个实例没有通过验证。具体使用方法请看下面的示例
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|-------|------|--------------------------------------------------------|
| cases | array | | 传入一个数组数组中的每项都是一个JEditableTable的实例 |
- `返回值:` Promise
- `示例:`
```js
import { validateTables, VALIDATE_NO_PASSED } from '@/utils/JEditableTableUtil'
// 封装cases
let cases = []
cases.push(this.$refs.editableTable1)
cases.push(this.$refs.editableTable2)
cases.push(this.$refs.editableTable3)
cases.push(this.$refs.editableTable4)
cases.push(this.$refs.editableTable5)
// 同时验证并获取多个实例的值
validateTables(cases).then((all) => {
// all 是一个数组每项都对应传入cases的下标包含values和deleteIds
console.log('所有实例的值:', all)
}).catch((e = {}) => {
// 判断表单验证是否未通过
if (e.error === VALIDATE_NO_PASSED) {
console.log('未通过验证的实例下标:', e.index)
} else {
console.error('发生异常:', e)
}
})
```
## FAQ
### 方法如何调用?
在[示例一](#示例一)中,设定了一个 `ref="editableTable"` 的属性那么在vue中就可以使用`this.$refs.editableTable`获取到该表格的实例,并调取其中的方法。
假如我要调取`initialize`方法,就可以这么写:`this.$refs.editableTable.initialize()`
### 如何获取表单的值?
使用`getValue`方法进行获取,详见[示例三](#示例三)
### 如何进行表单验证?
在获取值的时候默认会进行表单验证操作,用户在输入的时候也会对正在输入的表单进行验证,只要配置好规则就可以了
### 如何添加或删除一行?
该功能已封装到组件中,你只需要将 `actionButton` 设置为 `true` 即可,当然你也可以在代码中主动调用新增方法或修改,具体见上方的方法介绍。
### 为什么使用了ATab组件后切换选项卡会导致白屏或滚动条位置会归零
在ATab组件中确实会导致滚动条位置归零且不会触发`onscroll`方法,所以无法动态加载行,导致白屏的问题出现。
解决方法是在ATab组件的`onChange`事件触发时执行实例提供的`resetScrollTop()`方法即可但是需要注意的是代码主动改变ATab的`activeKey`不会触发`onChange`事件,还需要你手动调用下。
- `示例`
```html
<template>
<a-tabs @change="handleChangeTab">
<a-tab-pane tab="表格1" :forceRender="true" key="1">
<j-editable-table
ref="editableTable1"
:loading="tab1.loading"
:columns="tab1.columns"
:dataSource="tab1.dataSource"/>
</a-tab-pane>
<a-tab-pane tab="表格2" :forceRender="true" key="2">
<j-editable-table
ref="editableTable2"
:loading="tab2.loading"
:columns="tab2.columns"
:dataSource="tab2.dataSource"/>
</a-tab-pane>
</a-tabs>
</template>
```
```js
/*--- 忽略部分代码片段 ---*/
methods: {
/** 切换tab选项卡的时候重置editableTable的滚动条状态 */
handleChangeTab(key) {
this.$refs[`editableTable${key}`].resetScrollTop()
}
}
/*--- 忽略部分代码片段 ---*/
```
### slot(自定义插槽)如何使用?
代码示例请看:[示例四(slot)](#示例四(slot))
----------------------------------------------------------------------------------------
## 示例一
```html
<j-editable-table
ref="editableTable"
:loading="loading"
:columns="columns"
:dataSource="dataSource"
:rowNumber="true"
:rowSelection="true"
:actionButton="true"
style="margin-top: 8px;"
@selectRowChange="handleSelectRowChange"/>
```
## 示例二
```js
import { FormTypes } from '@/utils/JEditableTableUtil'
/*--- 忽略部分代码片断 ---*/
columns: [
{
title: '名称',
key: 'name',
type: FormTypes.input,
placeholder: '请输入${title}',
defaultValue: '称名',
// 表单验证规则
validateRules: [
{
required: true, // 必填
message: '${title}不能为空' // 提示的文本
},
{
pattern: /^[a-z|A-Z][a-z|A-Z\d_-]{0,}$/, // 正则
message: '${title}必须以字母开头,可包含数字、下划线、横杠'
}
]
},
{
title: '年龄',
key: 'age',
type: FormTypes.inputNumber,
placeholder: '请输入${title}',
defaultValue: 18,
validateRules: [{required: true, message: '${title}不能为空'}]
}
]
/*--- 忽略部分代码片断 ---*/
```
## 示例三
```js
// 获取被逻辑删除的字段id
let deleteIds = this.$refs.editableTable.getDeleteIds();
// 获取所有表单的值,并进行验证
this.$refs.editableTable.getValues((error, values) => {
// 错误数 = 0 则代表验证通过
if (error === 0) {
this.$message.success('验证通过')
// 将通过后的数组提交到后台或自行进行其他处理
console.log(deleteIds, values)
} else {
this.$message.error('验证未通过')
}
})
```
## 示例四(slot)
```html
<template>
<j-editable-table :columns="columns" :dataSource="dataSource">
<!-- 定义插槽 -->
<!-- 这种定义插槽的写法是vue推荐的新版写法https://cn.vuejs.org/v2/guide/components-slots.html#具名插槽),旧版已被废弃的写法不再支持 -->
<!-- 若webstorm这样写报错请看这篇文章https://blog.csdn.net/lxq_9532/article/details/81870651 -->
<template v-slot:action="props">
<a @click="handleDelete(props)">删除</a>
</template>
</j-editable-table>
</template>
<script>
import { FormTypes } from '@/utils/JEditableTableUtil'
import JEditableTable from '@/components/jeecg/JEditableTable'
export default {
components: { JEditableTable },
data() {
return {
columns: [
// ...
{
title: '操作',
key: 'action',
width: '8%',
type: FormTypes.slot, // 定义该列为 自定义插值列
slotName: 'action' // slot 的名称,对应 v-slot 冒号后面和等号前面的内容
}
]
}
},
methods: {
/* a 标签的点击事件,删除当前选中的行 */
handleDelete(props) {
// 参数解释
// props.text 当前值可能是defaultValue定义的值也可能是从dataSource中取出的值
// props.rowId 当前选中行的id如果是新增行则是临时id
// props.column :当前操作的列
// props.getValue 这是一个function执行后可以获取当前行的所有值禁止在template中使用
// 例const value = props.getValue()
// props.target 触发当前事件的实例可直接调用该实例内的方法禁止在template中使用
// 例target.add()
// 使用实例:删除当前操作的行
let { rowId, target } = props
target.removeRows(rowId)
}
}
}
</script>
```

View File

@ -0,0 +1,5 @@
import T from './JFormContainer.vue'
let install = function (Vue) {
Vue.component('JFormContainer',T);
}
export default { install };

View File

@ -0,0 +1,119 @@
<template>
<div class="components-input-demo-presuffix">
<!---->
<a-input @click="openModal" placeholder="请点击选择部门" v-model="departNames" readOnly :disabled="disabled">
<a-icon slot="prefix" type="cluster" title="部门选择控件"/>
<a-icon v-if="departIds" slot="suffix" type="close-circle" @click="handleEmpty" title="清空"/>
</a-input>
<j-select-depart-modal
ref="innerDepartSelectModal"
:modal-width="modalWidth"
:multi="multi"
:rootOpened="rootOpened"
:depart-id="value"
@ok="handleOK"
@initComp="initComp"/>
</div>
</template>
<script>
import JSelectDepartModal from './modal/JSelectDepartModal'
export default {
name: 'JSelectDepart',
components:{
JSelectDepartModal
},
props:{
modalWidth:{
type:Number,
default:500,
required:false
},
multi:{
type:Boolean,
default:false,
required:false
},
rootOpened:{
type:Boolean,
default:true,
required:false
},
value:{
type:String,
required:false
},
disabled:{
type: Boolean,
required: false,
default: false
}
},
data(){
return {
visible:false,
confirmLoading:false,
departNames:"",
departIds:''
}
},
mounted(){
this.departIds = this.value
},
watch:{
value(val){
this.departIds = val
}
},
methods:{
initComp(departNames){
this.departNames = departNames
},
openModal(){
this.$refs.innerDepartSelectModal.show()
},
handleOK(rows,idstr){
console.log("当前选中部门",rows)
console.log("当前选中部门ID",idstr)
if(!rows){
this.departNames = ''
this.departIds=''
}else{
let temp = ''
for(let item of rows){
temp+=','+item.departName
}
this.departNames = temp.substring(1)
this.departIds=idstr
}
this.$emit("change",this.departIds)
},
getDepartNames(){
return this.departNames
},
handleEmpty(){
this.handleOK('')
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
.components-input-demo-presuffix .anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.components-input-demo-presuffix .anticon-close-circle:hover {
color: #f5222d;
}
.components-input-demo-presuffix .anticon-close-circle:active {
color: #666;
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<div style="width: 100%;">
<a-select
mode="multiple"
placeholder="Please select"
:value="nameList"
style="width: calc(100% - 178px);">
</a-select>
<span style="display: inline-block;width:170px;float: right;overflow: hidden;">
<a-button type="primary" @click="handleSelect" icon="search" style="width: 81px">选择</a-button>
<a-button type="primary" @click="selectReset" icon="reload" style="margin-left: 8px;width: 81px">清空</a-button>
</span>
<!-- 选择多个用户支持排序 -->
<j-select-multi-user-modal ref="selectModal" @selectFinished="selectOK"/>
</div>
</template>
<script>
import JSelectMultiUserModal from './modal/JSelectMultiUserModal'
export default {
name: 'JSelectMultiUser',
components:{ JSelectMultiUserModal },
props:{
value:{
type:String,
required:false
}
},
data(){
return {
selectList: [],
}
},
computed: {
nameList: function () {
var names = [];
for (var a = 0; a < this.selectList.length; a++) {
names.push(this.selectList[a].name);
}
let nameStr = ''
if(names.length>0){
nameStr = names.join(",")
}
this.$emit("change",nameStr)
return names;
}
},
model: {
prop: 'value',
event: 'change'
},
methods:{
handleSelect: function () {
this.$refs.selectModal.add();
},
selectReset() {
this.selectList = [];
},
selectOK: function (data) {
this.selectList = data;
}
}
}
</script>

View File

@ -0,0 +1,71 @@
<template>
<div>
<a-input-search
v-model="selectedDepUsers"
placeholder="请先选择用户"
disabled
@search="onSearchDepUser">
<a-button slot="enterButton" :disabled="disabled">选择用户</a-button>
</a-input-search>
<j-select-user-by-dep-modal
ref="selectModal"
:modal-width="modalWidth"
@ok="onSearchDepUserCallBack" />
</div>
</template>
<script>
import JSelectUserByDepModal from './modal/JSelectUserByDepModal'
export default {
name: 'JSelectUserByDep',
components: { JSelectUserByDepModal },
props:{
modalWidth:{
type:Number,
default:1250,
required:false
},
value:{
type:String,
required:false
},
disabled:{
type: Boolean,
required: false,
default: false
}
},
data() {
return {
selectedDepUsers:"",
}
},
mounted(){
this.selectedDepUsers = this.value
},
watch:{
value(val){
this.selectedDepUsers = val
}
},
model: {
prop: 'value',
event: 'change'
},
methods: {
//通过组织机构筛选选择用户
onSearchDepUser() {
this.$refs.selectModal.showModal()
this.onSearchDepUserCallBack('')
},
onSearchDepUserCallBack(selectedDepUsers) {
this.selectedDepUsers = selectedDepUsers
this.$emit("change",selectedDepUsers)
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,137 @@
# JSelectDepart 部门选择组件
选择部门组件,存储部门ID,显示部门名称
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| modalWidth |Number | | 弹框宽度 默认500 |
| multi |Boolean | | 是否多选 默认false |
| rootOpened |Boolean | | 是否展开根节点 默认true |
| disabled |Boolean | | 是否禁用 默认false|
使用示例
----
```vue
<template>
<a-form :form="form">
<a-form-item label="部门选择v-decorator" style="width: 300px">
<j-select-depart v-decorator="['bumen']"/>
{{ getFormFieldValue('bumen') }}
</a-form-item>
<a-form-item label="部门选择v-model" style="width: 300px">
<j-select-depart v-model="bumen"/>
{{ bumen }}
</a-form-item>
<a-form-item label="部门多选v-model" style="width: 300px">
<j-select-depart v-model="bumens" :multi="true"/>
{{ bumens }}
</a-form-item>
</a-form >
</template>
<script>
import JSelectDepart from '@/components/jeecgbiz/JSelectDepart'
export default {
components: {JSelectDepart},
data() {
return {
form: this.$form.createForm(this),
bumen:"",
bumens:""
}
},
methods:{
getFormFieldValue(field){
return this.form.getFieldValue(field)
}
}
}
</script>
```
# JSelectMultiUser 用户多选组件
使用示例
----
```vue
<template>
<a-form :form="form">
<a-form-item label="用户选择v-decorator" style="width: 500px">
<j-select-multi-user v-decorator="['users']"/>
{{ getFormFieldValue('users') }}
</a-form-item>
<a-form-item label="用户选择v-model" style="width: 500px">
<j-select-multi-user v-model="users" ></j-select-multi-user>
{{ users }}
</a-form-item>
</a-form >
</template>
<script>
import JSelectMultiUser from '@/components/jeecgbiz/JSelectMultiUser'
export default {
components: {JSelectMultiUser},
data() {
return {
form: this.$form.createForm(this),
users:"",
}
},
methods:{
getFormFieldValue(field){
return this.form.getFieldValue(field)
}
}
}
</script>
```
# JSelectUserByDep 根据部门选择用户
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| modalWidth |Number | | 弹框宽度 默认1250 |
| disabled |Boolean | | 是否禁用 |
使用示例
----
```vue
<template>
<a-form :form="form">
<a-form-item label="用户选择v-decorator" style="width: 500px">
<j-select-user-by-dep v-decorator="['users']"/>
{{ getFormFieldValue('users') }}
</a-form-item>
<a-form-item label="用户选择v-model" style="width: 500px">
<j-select-user-by-dep v-model="users" ></j-select-user-by-dep>
{{ users }}
</a-form-item>
</a-form >
</template>
<script>
import JSelectUserByDep from '@/components/jeecgbiz/JSelectUserByDep'
export default {
components: {JSelectUserByDep},
data() {
return {
form: this.$form.createForm(this),
users:"",
}
},
methods:{
getFormFieldValue(field){
return this.form.getFieldValue(field)
}
}
}
</script>
```

View File

@ -0,0 +1,216 @@
<template>
<a-modal
title="选择部门"
:width="modalWidth"
:visible="visible"
:confirmLoading="confirmLoading"
@ok="handleSubmit"
@cancel="handleCancel"
cancelText="关闭">
<a-spin tip="Loading..." :spinning="false">
<a-input-search style="margin-bottom: 1px" placeholder="请输入部门名称按回车进行搜索" @search="onSearch" />
<a-tree
checkable
:treeData="treeData"
:checkStrictly="true"
@check="onCheck"
@select="onSelect"
@expand="onExpand"
:autoExpandParent="autoExpandParent"
:expandedKeys="expandedKeys"
:checkedKeys="checkedKeys">
<template slot="title" slot-scope="{title}">
<span v-if="title.indexOf(searchValue) > -1">
{{title.substr(0, title.indexOf(searchValue))}}
<span style="color: #f50">{{searchValue}}</span>
{{title.substr(title.indexOf(searchValue) + searchValue.length)}}
</span>
<span v-else>{{title}}</span>
</template>
</a-tree>
</a-spin>
</a-modal>
</template>
<script>
import { queryDepartTreeList } from '@/api/api'
export default {
name: 'JSelectDepartModal',
props:['modalWidth','multi','rootOpened','departId'],
data(){
return {
visible:false,
confirmLoading:false,
treeData:[],
autoExpandParent:true,
expandedKeys:[],
dataList:[],
checkedKeys:[],
checkedRows:[],
searchValue:""
}
},
created(){
this.loadDepart();
},
watch:{
departId(){
this.initDepartComponent()
}
},
methods:{
show(){
this.visible=true
this.checkedRows=[]
this.checkedKeys=[]
console.log("this.multi",this.multi)
},
loadDepart(){
queryDepartTreeList().then(res=>{
if(res.success){
let arr = [...res.result]
this.reWriterWithSlot(arr)
this.treeData = arr
this.initDepartComponent()
if(this.rootOpened){
this.initExpandedKeys(res.result)
}
}
})
},
initDepartComponent(){
let names = ''
if(this.departId){
let currDepartId = this.departId
for(let item of this.dataList){
if(currDepartId.indexOf(item.key)>=0){
names+=","+item.title
}
}
if(names){
names = names.substring(1)
}
}
this.$emit("initComp",names)
},
reWriterWithSlot(arr){
for(let item of arr){
if(item.children && item.children.length>0){
this.reWriterWithSlot(item.children)
let temp = Object.assign({},item)
temp.children = {}
this.dataList.push(temp)
}else{
this.dataList.push(item)
item.scopedSlots={ title: 'title' }
}
}
},
initExpandedKeys(arr){
if(arr && arr.length>0){
let keys = []
for(let item of arr){
if(item.children && item.children.length>0){
keys.push(item.id)
}
}
this.expandedKeys=[...keys]
}else{
this.expandedKeys=[]
}
},
onCheck (checkedKeys,info) {
if(!this.multi){
let arr = checkedKeys.checked.filter(item=>{
return this.checkedKeys.indexOf(item)<0
})
this.checkedKeys = [...arr]
this.checkedRows=[info.node.dataRef]
}else{
this.checkedKeys = checkedKeys.checked
this.checkedRows.push(info.node.dataRef)
}
//this.$emit("input",this.checkedKeys.join(","))
//console.log(this.checkedKeys.join(","))
},
onSelect (selectedKeys,info) {
console.log(selectedKeys)
let keys = []
keys.push(selectedKeys[0])
if(!this.checkedKeys || this.checkedKeys.length==0 || !this.multi){
this.checkedKeys = [...keys]
this.checkedRows=[info.node.dataRef]
}else{
let currKey = info.node.dataRef.key
if(this.checkedKeys.indexOf(currKey)>=0){
this.checkedKeys = this.checkedKeys.filter(item=>{
return item !=currKey
})
this.checkedRows=this.checkedRows.filter(item=>{
return item.key !=currKey
})
}else{
this.checkedRows.push(info.node.dataRef)
this.checkedKeys.push(...keys)
}
}
},
onExpand (expandedKeys) {
this.expandedKeys = expandedKeys
this.autoExpandParent = false
},
handleSubmit(){
if(!this.checkedKeys || this.checkedKeys.length==0){
this.$emit("ok",'')
}else{
this.$emit("ok",this.checkedRows,this.checkedKeys.join(","))
}
this.handleClear()
},
handleCancel(){
this.handleClear()
},
handleClear(){
this.visible=false
this.checkedKeys=[]
},
getParentKey(currKey,treeData){
let parentKey
for (let i = 0; i < treeData.length; i++) {
const node = treeData[i]
if (node.children) {
if (node.children.some(item => item.key === currKey)) {
parentKey = node.key
} else if (this.getParentKey(currKey, node.children)) {
parentKey = this.getParentKey(currKey, node.children)
}
}
}
return parentKey
},
onSearch(value){
const expandedKeys = this.dataList.map((item) => {
if (item.title.indexOf(value) > -1) {
return this.getParentKey(item.key,this.treeData)
}
return null
}).filter((item, i, self) => item && self.indexOf(item) === i)
Object.assign(this, {
expandedKeys,
searchValue: value,
autoExpandParent: true,
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,274 @@
<template>
<a-modal
centered
:title="title"
:width="1000"
:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
cancelText="关闭">
<a-row :gutter="18">
<a-col :span="16">
<a-card title="选择人员" :bordered="true">
<!-- 查询区域 -->
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="24">
<a-col :span="10">
<a-form-item label="姓名">
<a-input placeholder="请输入姓名" v-model="queryParam.name"></a-input>
</a-form-item>
</a-col>
<a-col :span="8" >
<span style="float: left;overflow: hidden;" class="table-page-search-submitButtons">
<a-button type="primary" @click="searchQuery" icon="search">查询</a-button>
<a-button type="primary" @click="searchReset" icon="reload" style="margin-left: 8px">重置</a-button>
</span>
</a-col>
</a-row>
</a-form>
</div>
<!-- table区域-begin -->
<div>
<a-table
size="small"
bordered
rowKey="id"
:columns="columns1"
:dataSource="dataSource1"
:pagination="ipagination"
:loading="loading"
:scroll="{ y: 240 }"
:rowSelection="{selectedRowKeys: selectedRowKeys,onSelectAll:onSelectAll,onSelect:onSelect,onChange: onSelectChange}"
@change="handleTableChange">
</a-table>
</div>
<!-- table区域-end -->
</a-card>
</a-col>
<a-col :span="8">
<a-card title="用户选择" :bordered="true">
<!-- table区域-begin -->
<div>
<a-table
size="small"
bordered
rowKey="id"
:columns="columns2"
:dataSource="dataSource2"
:loading="loading"
:scroll="{ y: 240 }"
>
<span slot="action" slot-scope="text, record">
<a-button type="primary" size="small" @click="handleDelete(record)" icon="delete">删除</a-button>
</span>
</a-table>
</div>
<!-- table区域-end -->
</a-card>
</a-col>
</a-row>
</a-modal>
</template>
<script>
import { filterObj } from '@/utils/util'
import { getAction } from '@/api/manage'
export default {
name: 'JSelectMultiUserModal',
data () {
return {
title: "用户列表",
names: [],
visible: false,
placement: 'right',
description: '人员管理页面',
// 查询条件
queryParam: {},
// 表头
columns1: [
{
title: '#',
dataIndex: '',
key:'rowIndex',
width:50,
align:"center",
customRender:function (t,r,index) {
return parseInt(index)+1;
}
},
{
title: '姓名',
align:"center",
width:113,
dataIndex: 'name'
},
{
title: '年龄',
align:"center",
width:100,
dataIndex: 'age'
},
{
title: '出生日期',
align:"center",
width:100,
dataIndex: 'birthday'
}
],
columns2: [
{
title: '用户账号',
align:"center",
width:100,
dataIndex: 'name'
},
{
title: '操作',
dataIndex: 'action',
align:"center",
width:100,
scopedSlots: { customRender: 'action' },
}
],
//数据集
dataSource1:[],
dataSource2:[],
// 分页参数
ipagination:{
current: 1,
pageSize: 10,
pageSizeOptions: ['10', '20', '30'],
showTotal: (total, range) => {
return range[0] + "-" + range[1] + " " + total + ""
},
showQuickJumper: true,
showSizeChanger: true,
total: 0
},
isorter:{
column: 'createTime',
order: 'desc',
},
loading:false,
selectedRowKeys: [],
selectedRows: [],
url: {
list: "/test/jeecgDemo/list",
},
}
},
created() {
this.loadData();
},
methods: {
searchQuery(){
this.loadData(1);
},
searchReset(){
this.queryParam={};
this.loadData(1);
},
handleCancel() {
this.visible = false;
},
handleOk() {
this.$emit("selectFinished",this.dataSource2);
this.visible = false;
},
add() {
this.visible = true;
},
loadData (arg){
//加载数据 若传入参数1则加载第一页的内容
if(arg===1){
this.ipagination.current = 1;
}
var params = this.getQueryParams();//查询条件
getAction(this.url.list,params).then((res)=>{
if(res.success){
this.dataSource1 = res.result.records;
this.ipagination.total = res.result.total;
}
})
},
getQueryParams(){
var param = Object.assign({}, this.queryParam,this.isorter);
param.field = this.getQueryField();
param.pageNo = this.ipagination.current;
param.pageSize = this.ipagination.pageSize;
return filterObj(param);
},
getQueryField(){
//TODO 字段权限控制
},
onSelectAll (selected, selectedRows, changeRows) {
if(selected===true){
for(var a = 0;a<changeRows.length;a++){
this.dataSource2.push(changeRows[a]);
}
}else{
for(var b = 0;b<changeRows.length;b++){
this.dataSource2.splice(this.dataSource2.indexOf(changeRows[b]),1);
}
}
// console.log(selected, selectedRows, changeRows);
},
onSelect (record,selected) {
if(selected===true){
this.dataSource2.push(record);
}else{
var index = this.dataSource2.indexOf(record);
//console.log();
if(index >=0 ){
this.dataSource2.splice(this.dataSource2.indexOf(record),1);
}
}
},
onSelectChange (selectedRowKeys,selectedRows) {
this.selectedRowKeys = selectedRowKeys;
this.selectionRows = selectedRows;
},
onClearSelected(){
this.selectedRowKeys = [];
this.selectionRows = [];
},
handleDelete: function(record){
this.dataSource2.splice(this.dataSource2.indexOf(record),1);
},
handleTableChange(pagination, filters, sorter){
//分页、排序、筛选变化时触发
console.log(sorter);
//TODO 筛选
if (Object.keys(sorter).length>0){
this.isorter.column = sorter.field;
this.isorter.order = "ascend"==sorter.order?"asc":"desc"
}
this.ipagination = pagination;
this.loadData();
}
}
}
</script>
<style lang="less" scoped>
.ant-card-body .table-operator{
margin-bottom: 18px;
}
.ant-table-tbody .ant-table-row td{
padding-top:15px;
padding-bottom:15px;
}
.anty-row-operator button{margin: 0 5px}
.ant-btn-danger{background-color: #ffffff}
.ant-modal-cust-warp{height: 100%}
.ant-modal-cust-warp .ant-modal-body{height:calc(100% - 110px) !important;overflow-y: auto}
.ant-modal-cust-warp .ant-modal-content{height:90% !important;overflow-y: hidden}
</style>

View File

@ -0,0 +1,306 @@
<template>
<a-modal
:width="modalWidth"
:visible="visible"
:title="title"
@ok="handleSubmit"
@cancel="close"
cancelText="关闭"
style="margin-top: -70px"
wrapClassName="ant-modal-cust-warp"
>
<a-row :gutter="10" style="background-color: #ececec; padding: 10px; margin: -10px">
<a-col :md="6" :sm="24">
<a-card :bordered="false">
<!--组织机构-->
<a-directory-tree
selectable
:selectedKeys="selectedKeys"
:checkStrictly="true"
@select="this.onSelect"
:dropdownStyle="{maxHeight:'200px',overflow:'auto'}"
:treeData="departTree"
/>
</a-card>
</a-col>
<a-col :md="18" :sm="24">
<a-card :bordered="false">
用户账号:
<a-input-search
:style="{width:'150px',marginBottom:'15px'}"
placeholder="请输入用户账号"
v-model="queryParam.username"
@search="onSearch"
></a-input-search>
<a-button @click="searchReset(1)" style="margin-left: 20px" icon="redo">重置</a-button>
<!--用户列表-->
<a-table
ref="table"
:scroll="scrollTrigger"
size="middle"
rowKey="id"
:columns="columns"
:dataSource="dataSource"
:pagination="ipagination"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
@change="handleTableChange">
</a-table>
</a-card>
</a-col>
</a-row>
</a-modal>
</template>
<script>
import { filterObj } from '@/utils/util'
import { queryDepartTreeList, getUserList, queryUserByDepId, queryUserRoleMap } from '@/api/api'
export default {
name: 'JSelectUserByDepModal',
components: {},
props:['modalWidth'],
data() {
return {
queryParam: {
username:"",
},
columns: [
{
title: '用户账号',
align: 'center',
dataIndex: 'username'
},
{
title: '真实姓名',
align: 'center',
dataIndex: 'realname'
},
{
title: '角色名称',
align: 'center',
dataIndex: 'roleName'
},
{
title: '性别',
align: 'center',
dataIndex: 'sex',
customRender: function(text) {
if (text === 1) {
return '男'
} else if (text === 2) {
return '女'
} else {
return text
}
}
},
{
title: '手机号码',
align: 'center',
dataIndex: 'phone'
},
{
title: '邮箱',
align: 'center',
dataIndex: 'email'
}
],
scrollTrigger: {},
dataSource: [],
selectedKeys: [],
userNameArr: [],
departName: '',
userRolesMap: {},
title: '根据部门选择用户',
ipagination: {
current: 1,
pageSize: 10,
pageSizeOptions: ['10', '20', '30'],
showTotal: (total, range) => {
return range[0] + '-' + range[1] + ' ' + total + '条'
},
showQuickJumper: true,
showSizeChanger: true,
total: 0
},
isorter: {
column: 'createTime',
order: 'desc'
},
selectedRowKeys: [],
selectedRows: [],
departTree: [],
visible: false,
form: this.$form.createForm(this)
}
},
created() {
// 该方法触发屏幕自适应
this.resetScreenSize();
this.queryUserRoleMap();
},
methods: {
loadData(arg) {
if (arg === 1) {
this.ipagination.current = 1;
}
let params = this.getQueryParams();//查询条件
getUserList(params).then((res) => {
if (res.success) {
this.dataSource = res.result.records;
this.assignRoleName(this.dataSource);
this.ipagination.total = res.result.total;
}
})
},
queryUserRoleMap(){
queryUserRoleMap().then((res) => {
if (res.success) {
this.userRolesMap = res.result;
this.loadData();
}
})
},
// 触发屏幕自适应
resetScreenSize() {
let screenWidth = document.body.clientWidth;
if (screenWidth < 500) {
this.scrollTrigger = { x: 800 };
} else {
this.scrollTrigger = {};
}
},
showModal() {
this.visible = true;
this.assignRoleName(this.dataSource);
this.queryDepartTree();
this.form.resetFields();
},
getQueryParams() {
let param = Object.assign({}, this.queryParam, this.isorter);
param.field = this.getQueryField();
param.pageNo = this.ipagination.current;
param.pageSize = this.ipagination.pageSize;
return filterObj(param);
},
getQueryField() {
let str = 'id,';
for (let a = 0; a < this.columns.length; a++) {
str += ',' + this.columns[a].dataIndex;
}
return str;
},
searchReset(num) {
let that = this;
if(num !== 0){
that.queryParam = {};
that.loadData(1);
}
that.selectedRowKeys = [];
that.userNameArr = [];
that.selectedKeys = [];
},
close() {
this.searchReset(0);
this.visible = false;
},
handleTableChange(pagination, filters, sorter) {
//TODO 筛选
if (Object.keys(sorter).length > 0) {
this.isorter.column = sorter.field;
this.isorter.order = 'ascend' === sorter.order ? 'asc' : 'desc';
}
this.ipagination = pagination;
this.loadData();
},
handleSubmit() {
let that = this;
for (let i = 0, len = this.selectedRowKeys.length; i < len; i++) {
this.getUserNames(this.selectedRowKeys[i]);
}
that.$emit('ok', that.userNameArr.join(','));
that.close();
},
// 遍历匹配,获取用户真实姓名
getUserNames(rowId) {
let dataSource = this.dataSource;
for (let i = 0, len = dataSource.length; i < len; i++) {
if (rowId === dataSource[i].id) {
this.userNameArr.push(dataSource[i].realname);
}
}
},
// 点击树节点,筛选出对应的用户
onSelect(selectedKeys) {
if (selectedKeys[0] != null) {
this.queryUserByDepId(selectedKeys); // 调用方法根据选选择的id查询用户信息
if (this.selectedKeys[0] !== selectedKeys[0]) {
this.selectedKeys = [selectedKeys[0]];
}
}
},
onSelectChange(selectedRowKeys, selectionRows) {
this.selectedRowKeys = selectedRowKeys;
this.selectionRows = selectionRows;
},
onSearch() {
this.loadData(1);
},
// 根据选择的id来查询用户信息
queryUserByDepId(selectedKeys) {
queryUserByDepId({ id: selectedKeys.toString() }).then((res) => {
if (res.success) {
this.dataSource = res.result;
this.ipagination.total = res.result.length;
this.assignRoleName(this.dataSource);
}
})
},
// 传入用户id,找到匹配的角色名称
queryUserRole(userId) {
let map = this.userRolesMap;
let roleName = [];
for (var key in map) {
if (userId === key) {
roleName.push(map[key]);
}
}
return roleName.join(',');
},
queryDepartTree() {
queryDepartTreeList().then((res) => {
if (res.success) {
this.departTree = res.result;
}
})
},
// 为角色名称赋值
assignRoleName(data) {
let userId = '';
let role = '';
for (let i = 0, length = data.length; i < length; i++) {
userId = this.dataSource[i].id;
role = this.queryUserRole(userId);
this.dataSource[i].roleName = role;
}
},
modalFormOk() {
this.loadData();
}
}
}
</script>
<style scoped>
.ant-table-tbody .ant-table-row td {
padding-top: 10px;
padding-bottom: 10px;
}
#components-layout-demo-custom-trigger .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color .3s;
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<a-modal
title="用户列表"
:width="1000"
:visible="visible"
:confirmLoading="confirmLoading"
@ok="handleSubmit"
@cancel="handleCancel">
<a-table
ref="table"
bordered
size="middle"
rowKey="id"
:columns="columns"
:dataSource="dataSource"
:pagination="ipagination"
:loading="loading"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"></a-table>
</a-modal>
</template>
<script>
import {getUserList} from '@/api/api'
import {JeecgListMixin} from '@/mixins/JeecgListMixin'
export default {
name: "SelectUserListModal",
mixins: [JeecgListMixin],
data() {
return {
title: "操作",
visible: false,
model: {},
confirmLoading: false,
url: {
add: "/act/model/create",
list: "/sys/user/list"
},
columns: [
{
title: '用户账号',
align: "center",
dataIndex: 'username',
fixed: 'left',
width: 200
},
{
title: '用户真实姓名',
align: "center",
dataIndex: 'realname',
},
{
title: '性别',
align: "center",
dataIndex: 'sex_dictText'
},
{
title: '手机号码',
align: "center",
dataIndex: 'phone'
},
{
title: '邮箱',
align: "center",
dataIndex: 'email'
},
{
title: '状态',
align: "center",
dataIndex: 'status_dictText'
}
]
}
},
created() {
//Step.2 加载用户数据
getUserList().then((res) => {
if (res.success) {
this.dataSource = res.result.records;
this.ipagination.total = res.result.total;
}
})
},
methods: {
open() {
this.visible = true;
//Step.1 清空选中用户
this.selectedRowKeys = []
this.selectedRows = []
},
close() {
this.$emit('close');
this.visible = false;
},
handleChange(info) {
let file = info.file;
if (file.response.success) {
this.$message.success(file.response.message);
this.$emit('ok');
this.close()
} else {
this.$message.warn(file.response.message);
this.close()
}
},
handleCancel() {
this.close()
},
handleSubmit() {
this.$emit('ok', this.selectionRows);
this.close()
},
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,60 @@
<template>
<global-layout>
<transition name="page-transition">
<keep-alive v-if="keepAlive">
<router-view />
</keep-alive>
<router-view v-else />
</transition>
</global-layout>
</template>
<script>
import GlobalLayout from '@/components/page/GlobalLayout'
export default {
name: "BasicLayout",
components: {
GlobalLayout
},
data () {
return {
}
},
computed: {
keepAlive () {
return this.$route.meta.keepAlive
}
},
methods: {
},
}
</script>
<style lang="scss">
/*
* The following styles are auto-applied to elements with
* transition="page-transition" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the page transition by editing
* these styles.
*/
.page-transition-enter {
opacity: 0;
}
.page-transition-leave-active {
opacity: 0;
}
.page-transition-enter .page-transition-container,
.page-transition-leave-active .page-transition-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div>
<router-view />
</div>
</template>
<script>
export default {
name: "BlankLayout",
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,47 @@
<template>
<iframe :id="id" :src="url" frameborder="0" width="100%" height="800px" scrolling="auto" style="background-color: #fff;"></iframe>
</template>
<script>
import PageLayout from '../page/PageLayout'
import RouteView from './RouteView'
export default {
name: "IframePageContent",
data () {
return {
url: "",
id:""
}
},
created () {
this.goUrl()
},
updated () {
this.goUrl()
},
watch: {
$route(to, from) {
this.goUrl();
}
},
methods: {
goUrl () {
let url = this.$route.meta.url
let id = this.$route.path
this.id = id
//url = "http://www.baidu.com"
console.log("------url------"+url)
if (url !== null && url !== undefined) {
this.url = url;
//window.open(this.url);
}
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,47 @@
<template>
<iframe :id="id" :src="url" frameborder="0" width="100%" height="800px" scrolling="auto"></iframe>
</template>
<script>
import PageLayout from '../page/PageLayout'
import RouteView from './RouteView'
export default {
name: "IframePageContent",
data () {
return {
url: "",
id:""
}
},
created () {
this.goUrl()
},
updated () {
this.goUrl()
},
watch: {
$route(to, from) {
this.goUrl();
}
},
methods: {
goUrl () {
let url = this.$route.meta.url
let id = this.$route.path
this.id = id
//url = "http://www.baidu.com"
console.log("------url------"+url)
if (url !== null && url !== undefined) {
this.url = url;
//window.open(this.url);
}
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,85 @@
<template>
<page-layout :desc="description" :title="getTitle" :link-list="linkList" :search="search" :tabs="tabs">
<div slot="extra" class="extra-img">
<img :src="extraImage"/>
</div>
<!-- keep-alive -->
<route-view ref="content"></route-view>
</page-layout>
</template>
<script>
import PageLayout from '../page/PageLayout'
import RouteView from './RouteView'
export default {
name: "PageContent",
components: {
RouteView,
PageLayout
},
data () {
return {
title: '',
description: '',
linkList: [],
extraImage: '',
search: false,
tabs: {}
}
},
mounted () {
this.getPageHeaderInfo()
},
updated () {
this.getPageHeaderInfo()
},
computed: {
getTitle () {
return this.$route.meta.title
}
},
methods: {
getPageHeaderInfo () {
// eslint-disable-next-line
this.title = this.$route.meta.title
// 因为套用了一层 route-view 所以要取 ref 对象下的子节点的第一个对象
const content = this.$refs.content && this.$refs.content.$children[0]
if (content) {
this.description = content.description
this.linkList = content.linkList
this.extraImage = content.extraImage
this.search = content.search == true ? true : false
this.tabs = content.tabs
}
}
}
}
</script>
<style lang="scss" scoped>
.extra-img {
margin-top: -60px;
text-align: center;
width: 195px;
img {
width: 100%;
}
}
.mobile {
.extra-img{
margin-top: 0;
text-align: center;
width: 96px;
img{
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<div class="main">
<keep-alive>
<router-view v-if="keepAlive" />
</keep-alive>
<router-view v-if="!keepAlive" />
</div>
</template>
<script>
export default {
name: "RouteView",
computed: {
keepAlive () {
return this.$route.meta.keepAlive
}
},
}
</script>

View File

@ -0,0 +1,299 @@
<template>
<global-layout @dynamicRouterShow="dynamicRouterShow">
<contextmenu :itemList="menuItemList" :visible.sync="menuVisible" @select="onMenuSelect"/>
<a-tabs
@contextmenu.native="e => onContextmenu(e)"
v-if="multipage"
:active-key="activePage"
class="tab-layout-tabs"
style="height:52px"
:hide-add="true"
type="editable-card"
@change="changePage"
@edit="editPage">
<a-tab-pane :id="page.fullPath" :key="page.fullPath" v-for="page in pageList">
<span slot="tab" :pagekey="page.fullPath">{{ page.meta.title }}</span>
</a-tab-pane>
</a-tabs>
<div style="margin: 12px 12px 0;">
<transition name="page-toggle">
<keep-alive v-if="multipage">
<router-view/>
</keep-alive>
<router-view v-else/>
</transition>
</div>
</global-layout>
</template>
<script>
import GlobalLayout from '@/components/page/GlobalLayout'
import Contextmenu from '@/components/menu/Contextmenu'
import { mixin, mixinDevice } from '@/utils/mixin.js'
const indexKey = '/dashboard/analysis'
export default {
name: 'TabLayout',
components: {
GlobalLayout,
Contextmenu
},
mixins: [mixin, mixinDevice],
data() {
return {
pageList: [],
linkList: [],
activePage: '',
menuVisible: false,
menuItemList: [
{ key: '1', icon: 'arrow-left', text: '关闭左侧' },
{ key: '2', icon: 'arrow-right', text: '关闭右侧' },
{ key: '3', icon: 'close', text: '关闭其它' }
]
}
},
computed: {
multipage() {
//判断如果是手机模式,自动切换为单页面模式
if (this.isMobile()) {
return false
} else {
return this.$store.state.app.multipage
}
}
},
created() {
if (this.$route.path != indexKey) {
this.pageList.push({
name: 'dashboard-analysis',
path: indexKey,
fullPath: indexKey,
meta: {
icon: 'dashboard',
title: '首页'
}
})
this.linkList.push(indexKey)
}
this.pageList.push(this.$route)
this.linkList.push(this.$route.fullPath)
this.activePage = this.$route.fullPath
},
watch: {
'$route': function(newRoute) {
this.activePage = newRoute.fullPath
if (!this.multipage) {
this.linkList = [newRoute.fullPath]
this.pageList = [Object.assign({},newRoute)]
} else if (this.linkList.indexOf(newRoute.fullPath) < 0) {
this.linkList.push(newRoute.fullPath)
this.pageList.push(Object.assign({},newRoute))
} else if (this.linkList.indexOf(newRoute.fullPath) >= 0) {
let oldIndex = this.linkList.indexOf(newRoute.fullPath)
let oldPositionRoute = this.pageList[oldIndex]
this.pageList.splice(oldIndex, 1, Object.assign({},newRoute,{meta:oldPositionRoute.meta}))
}
},
'activePage': function(key) {
let index = this.linkList.lastIndexOf(key)
let waitRouter = this.pageList[index]
this.$router.push(Object.assign({},waitRouter));
},
'multipage': function(newVal) {
if (!newVal) {
this.linkList = [this.$route.fullPath]
this.pageList = [this.$route]
}
}
},
methods: {
changePage(key) {
this.activePage = key
},
editPage(key, action) {
this[action](key)
},
remove(key) {
if (key == indexKey) {
this.$message.warning('首页不能关闭!')
return
}
if (this.pageList.length === 1) {
this.$message.warning('这是最后一页,不能再关闭了啦')
return
}
this.pageList = this.pageList.filter(item => item.fullPath !== key)
let index = this.linkList.indexOf(key)
this.linkList = this.linkList.filter(item => item !== key)
index = index >= this.linkList.length ? this.linkList.length - 1 : index
this.activePage = this.linkList[index]
},
onContextmenu(e) {
const pagekey = this.getPageKey(e.target)
if (pagekey !== null) {
e.preventDefault()
this.menuVisible = true
}
},
getPageKey(target, depth) {
depth = depth || 0
if (depth > 2) {
return null
}
let pageKey = target.getAttribute('pagekey')
pageKey = pageKey || (target.previousElementSibling ? target.previousElementSibling.getAttribute('pagekey') : null)
return pageKey || (target.firstElementChild ? this.getPageKey(target.firstElementChild, ++depth) : null)
},
onMenuSelect(key, target) {
let pageKey = this.getPageKey(target)
switch (key) {
case '1':
this.closeLeft(pageKey)
break
case '2':
this.closeRight(pageKey)
break
case '3':
this.closeOthers(pageKey)
break
default:
break
}
},
closeOthers(pageKey) {
let index = this.linkList.indexOf(pageKey)
if (pageKey == indexKey) {
this.linkList = this.linkList.slice(index, index + 1)
this.pageList = this.pageList.slice(index, index + 1)
this.activePage = this.linkList[0]
} else {
let indexContent = this.pageList.slice(0, 1)[0]
this.linkList = this.linkList.slice(index, index + 1)
this.pageList = this.pageList.slice(index, index + 1)
this.linkList.unshift(indexKey)
this.pageList.unshift(indexContent)
this.activePage = this.linkList[1]
}
},
closeLeft(pageKey) {
if (pageKey == indexKey) {
return
}
let tempList = [...this.pageList]
let indexContent = tempList.slice(0, 1)[0]
let index = this.linkList.indexOf(pageKey)
this.linkList = this.linkList.slice(index)
this.pageList = this.pageList.slice(index)
this.linkList.unshift(indexKey)
this.pageList.unshift(indexContent)
if (this.linkList.indexOf(this.activePage) < 0) {
this.activePage = this.linkList[0]
}
},
closeRight(pageKey) {
let index = this.linkList.indexOf(pageKey)
this.linkList = this.linkList.slice(0, index + 1)
this.pageList = this.pageList.slice(0, index + 1)
if (this.linkList.indexOf(this.activePage < 0)) {
this.activePage = this.linkList[this.linkList.length - 1]
}
},
//update-begin-author:taoyan date:20190430 for:动态路由title显示配置的菜单title而不是其对应路由的title
dynamicRouterShow(key,title){
let keyIndex = this.linkList.indexOf(key)
if(keyIndex>=0){
let currRouter = this.pageList[keyIndex]
let meta = Object.assign({},currRouter.meta,{title:title})
this.pageList.splice(keyIndex, 1, Object.assign({},currRouter,{meta:meta}))
}
}
//update-end-author:taoyan date:20190430 for:动态路由title显示配置的菜单title而不是其对应路由的title
}
}
</script>
<style lang="scss">
/*
* The following styles are auto-applied to elements with
* transition="page-transition" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the page transition by editing
* these styles.
*/
.page-transition-enter {
opacity: 0;
}
.page-transition-leave-active {
opacity: 0;
}
.page-transition-enter .page-transition-container,
.page-transition-leave-active .page-transition-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
/*美化弹出Tab样式*/
.ant-tabs-nav-container {
margin-top: 4px;
}
/* 修改 ant-tabs 样式 */
.tab-layout-tabs.ant-tabs {
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
background-color: white;
padding: 0 20px;
.ant-tabs-bar {
margin: 4px 0 0;
border: none;
}
}
.ant-tabs {
&.ant-tabs-card .ant-tabs-tab {
padding: 0 24px !important;
background-color: white !important;
margin-right: 10px !important;
.ant-tabs-close-x {
width: 12px !important;
height: 12px !important;
opacity: 0 !important;
cursor: pointer !important;
font-size: 12px !important;
margin: 0 !important;
position: absolute;
top: 36%;
right: 6px;
}
&:hover .ant-tabs-close-x {
opacity: 1 !important;
}
}
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar {
.ant-tabs-tab {
border: none !important;
border-bottom: 1px solid transparent !important;
}
.ant-tabs-tab-active {
border-color: #1890ff !important;
}
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<div id="userLayout" :class="['user-layout-wrapper', device]">
<div class="container">
<div class="top">
<div class="header">
<a href="/">
<img src="~@/assets/logo.svg" class="logo" alt="logo">
<span class="title">Jeecg Boot</span>
</a>
</div>
<div class="desc">
Jeecg Boot 是中国最具影响力的 企业级 快速开发平台
</div>
</div>
<route-view></route-view>
<div class="footer">
<div class="links">
<a href="http://jeecg-boot.mydoc.io" target="_blank">帮助</a>
<a href="https://github.com/zhangdaiscott/jeecg-boot" target="_blank">隐私</a>
<a href="https://github.com/zhangdaiscott/jeecg-boot" target="_blank">条款</a>
</div>
<div class="copyright">
Copyright &copy; 2019 <a href="http://www.jeecg.org" target="_blank">JEECG开源社区</a> 出品
</div>
</div>
</div>
</div>
</template>
<script>
import RouteView from "@/components/layouts/RouteView"
import { mixinDevice } from '@/utils/mixin.js'
export default {
name: "UserLayout",
components: { RouteView },
mixins: [mixinDevice],
data () {
return {}
},
mounted () {
document.body.classList.add('userLayout')
},
beforeDestroy () {
document.body.classList.remove('userLayout')
},
}
</script>
<style lang="scss" scoped>
#userLayout.user-layout-wrapper {
height: 100%;
&.mobile {
.container {
.main {
max-width: 368px;
width: 98%;
}
}
}
.container {
width: 100%;
min-height: 100%;
background: #f0f2f5 url(~@/assets/background.svg) no-repeat 50%;
background-size: 100%;
padding: 110px 0 144px;
position: relative;
a {
text-decoration: none;
}
.top {
text-align: center;
.header {
height: 44px;
line-height: 44px;
.badge {
position: absolute;
display: inline-block;
line-height: 1;
vertical-align: middle;
margin-left: -12px;
margin-top: -10px;
opacity: 0.8;
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
border-style: none;
}
.title {
font-size: 33px;
color: rgba(0, 0, 0, .85);
font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-weight: 600;
position: relative;
top: 2px;
}
}
.desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin-top: 12px;
margin-bottom: 40px;
}
}
.main {
min-width: 260px;
width: 368px;
margin: 0 auto;
}
.footer {
position: absolute;
width: 100%;
bottom: 0;
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;
.links {
margin-bottom: 8px;
font-size: 14px;
a {
color: rgba(0, 0, 0, 0.45);
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
}
}
}
.copyright {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
}
}
</style>

View File

@ -0,0 +1,8 @@
import UserLayout from '@/components/layouts/UserLayout'
import BlankLayout from '@/components/layouts/BlankLayout'
import BasicLayout from '@/components/layouts/BasicLayout'
import RouteView from '@/components/layouts/RouteView'
import PageView from '@/components/layouts/PageView'
import TabLayout from '@/components/layouts/TabLayout'
export { UserLayout, BasicLayout, BlankLayout, RouteView, PageView, TabLayout }

View File

@ -0,0 +1,71 @@
<template>
<a-menu :style="style" class="contextmenu" v-show="visible" @click="handleClick" :selectedKeys="selectedKeys">
<a-menu-item :key="item.key" v-for="item in itemList">
<a-icon role="menuitemicon" v-if="item.icon" :type="item.icon" />{{ item.text }}
</a-menu-item>
</a-menu>
</template>
<script>
export default {
name: 'Contextmenu',
props: {
visible: {
type: Boolean,
required: false,
default: false
},
itemList: {
type: Array,
required: true,
default: () => []
}
},
data () {
return {
left: 0,
top: 0,
target: null,
selectedKeys: []
}
},
computed: {
style () {
return {
left: this.left + 'px',
top: this.top + 'px'
}
}
},
created () {
window.addEventListener('mousedown', e => this.closeMenu(e))
window.addEventListener('contextmenu', e => this.setPosition(e))
},
methods: {
closeMenu (e) {
if (['menuitemicon', 'menuitem'].indexOf(e.target.getAttribute('role')) < 0) {
this.$emit('update:visible', false)
}
},
setPosition (e) {
this.left = e.clientX
this.top = e.clientY
this.target = e.target
},
handleClick ({key}) {
this.$emit('select', key, this.target)
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="less" scoped>
.contextmenu{
position: fixed;
z-index: 1;
border: 1px solid #9e9e9e;
border-radius: 4px;
box-shadow: 2px 2px 10px #aaaaaa !important;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<a-layout-sider
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
width="200px"
:collapsible="collapsible"
v-model="collapsed"
:trigger="null">
<logo />
<s-menu
:collapsed="collapsed"
:menu="menus"
:theme="theme"
@select="onSelect"
:mode="mode"
:style="smenuStyle">
</s-menu>
</a-layout-sider>
</template>
<script>
import ALayoutSider from "ant-design-vue/es/layout/Sider"
import Logo from '../tools/Logo'
import SMenu from './index'
import { mixin, mixinDevice } from '@/utils/mixin.js'
export default {
name: "SideMenu",
components: { ALayoutSider, Logo, SMenu },
mixins: [mixin, mixinDevice],
props: {
mode: {
type: String,
required: false,
default: 'inline'
},
theme: {
type: String,
required: false,
default: 'dark'
},
collapsible: {
type: Boolean,
required: false,
default: false
},
collapsed: {
type: Boolean,
required: false,
default: false
},
menus: {
type: Array,
required: true
}
},
computed:{
smenuStyle() {
let style = { 'padding': '0' }
if (this.fixSiderbar) {
style['height'] = 'calc(100% - 59px)'
style['overflow'] = 'auto'
style['overflow-x'] = 'hidden'
}
return style
}
},
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
}
}
}
</script>
<style lang="scss" scoped>
/* update_begin author:sunjianlei date:20190509 for: 修改侧边导航栏滚动条的样式 */
.sider {
$scrollBarSize: 10px;
ul.ant-menu {
/* 定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
&::-webkit-scrollbar {
width: $scrollBarSize;
height: $scrollBarSize;
background-color: transparent;
display: none;
}
& .-o-scrollbar {
display: none;
}
/* 兼容IE */
-ms-overflow-style: none;
-ms-scroll-chaining: chained;
-ms-content-zooming: zoom;
-ms-scroll-rails: none;
-ms-content-zoom-limit-min: 100%;
-ms-content-zoom-limit-max: 500%;
-ms-scroll-snap-type: proximity;
-ms-scroll-snap-points-x: snapList(100%, 200%, 300%, 400%, 500%);
/* 定义滚动条轨道 */
&::-webkit-scrollbar-track {
background-color: transparent;
}
/* 定义滑块 */
&::-webkit-scrollbar-thumb {
border-radius: $scrollBarSize;
background-color: #eee;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1);
&:hover {
background-color: #dddddd;
}
&:active {
background-color: #bbbbbb;
}
}
}
/** 暗色系滚动条样式 */
&.dark ul.ant-menu {
&::-webkit-scrollbar-thumb {
background-color: #666666;
&:hover {
background-color: #808080;
}
&:active {
background-color: #999999;
}
}
}
}
/* update_end author:sunjianlei date:20190509 for: 修改侧边导航栏滚动条的样式 */
</style>
<!-- update_begin author:sunjianlei date:20190530 for: 选中首页的时候不显示背景颜色 -->
<style lang="scss">
.ant-menu.ant-menu-root {
& > .ant-menu-item:first-child {
background-color: white;
& > a, & > a:hover {
color: rgba(0, 0, 0, 0.65);
}
}
}
</style>
<!-- update_end author:sunjianlei date:20190530 for: 选中首页的时候不显示背景颜色 -->

View File

@ -0,0 +1,188 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const { Item, SubMenu } = Menu
export default {
name: 'SMenu',
props: {
menu: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
}
},
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
}
},
mounted () {
this.updateMenu()
},
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
},
$route: function () {
this.updateMenu()
}
},
methods: {
// select menu item
onOpenChange (openKeys) {
// 在水平模式下时执行,并且不再执行后续
if (this.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] : []
}
},
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 => {
openKeys.push(item.path)
})
}
//update-begin-author:taoyan date:20190510 for:online表单菜单点击展开的一级目录不对
if(!this.selectedKeys || this.selectedKeys[0].indexOf(":")<0){
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
}
//update-end-author:taoyan date:20190510 for:online表单菜单点击展开的一级目录不对
},
// render
renderItem (menu) {
if (!menu.hidden) {
return menu.children && !menu.alwaysShow ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
}
return null
},
renderMenuItem (menu) {
const target = menu.meta.target || null
const tag = target && 'a' || 'router-link'
let props = { to: { name: menu.name } }
if(menu.route && menu.route === '0'){
props = { to: { path: menu.path } }
}
const attrs = { href: menu.path, target: menu.meta.target }
if (menu.children && menu.alwaysShow) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
menu.children.forEach(item => {
item.meta = Object.assign(item.meta, { hidden: true })
})
}
return (
<Item {...{ key: menu.path }}>
<tag {...{ props, attrs }}>
{this.renderIcon(menu.meta.icon)}
<span>{menu.meta.title}</span>
</tag>
</Item>
)
},
renderSubMenu (menu) {
const itemArr = []
if (!menu.alwaysShow) {
menu.children.forEach(item => itemArr.push(this.renderItem(item)))
}
return (
<SubMenu {...{ key: menu.path }}>
<span slot="title">
{this.renderIcon(menu.meta.icon)}
<span>{menu.meta.title}</span>
</span>
{itemArr}
</SubMenu>
)
},
renderIcon (icon) {
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return (
<Icon {... { props } }/>
)
}
},
render () {
const { mode, theme, menu } = this
const props = {
mode: mode,
theme: theme,
openKeys: this.openKeys
}
const on = {
select: obj => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
},
openChange: this.onOpenChange
}
const menuTree = menu.map(item => {
if (item.hidden) {
return null
}
return this.renderItem(item)
})
// {...{ props, on: on }}
return (
<Menu vModel={this.selectedKeys} {...{ props, on: on }}>
{menuTree}
</Menu>
)
}
}

View File

@ -0,0 +1,51 @@
<template>
<div class="footer">
<div class="links">
<a href="http://www.jeecg.org" target="_blank">JEECG 首页</a>
<a href="https://github.com/zhangdaiscott/jeecg-boot" target="_blank">
<a-icon type="github"/>
</a>
<a href="https://ant.design/">Ant Design</a>
<a href="https://vuecomponent.github.io/ant-design-vue/docs/vue/introduce-cn/">Vue Antd</a>
</div>
<div class="copyright">
Copyright
<a-icon type="copyright"/>
2019 <span>JEECG开源社区 出品</span>
</div>
</div>
</template>
<script>
export default {
name: "LayoutFooter"
}
</script>
<style lang="scss" scoped>
.footer {
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;
.links {
margin-bottom: 8px;
a {
color: rgba(0, 0, 0, .45);
&:hover {
color: rgba(0, 0, 0, .65);
}
&:not(:last-child) {
margin-right: 40px;
}
}
}
.copyright {
color: rgba(0, 0, 0, .45);
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,218 @@
<template>
<!-- , width: fixedHeader ? `calc(100% - ${sidebarOpened ? 256 : 80}px)` : '100%' -->
<a-layout-header
v-if="!headerBarFixed"
:class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
:style="{ padding: '0' }">
<div v-if="mode === 'sidemenu'" class="header" :class="theme">
<a-icon
v-if="device==='mobile'"
class="trigger"
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
@click.native="toggle"></a-icon>
<a-icon
v-else
class="trigger"
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
@click.native="toggle"/>
<span v-if="device === 'desktop'">欢迎进入 Jeecg-Boot 企业级快速开发平台</span>
<span v-else>Jeecg-Boot</span>
<user-menu :theme="theme"/>
</div>
<!-- 顶部导航栏模式 -->
<div v-else :class="['top-nav-header-index', theme]">
<div class="header-index-wide">
<div class="header-index-left" :style="topMenuStyle.headerIndexLeft">
<logo class="top-nav-header" :show-title="device !== 'mobile'" :style="topMenuStyle.topNavHeader"/>
<div v-if="device !== 'mobile'" :style="topMenuStyle.topSmenuStyle">
<s-menu
mode="horizontal"
:menu="menus"
:theme="theme"></s-menu>
</div>
<a-icon
v-else
class="trigger"
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
@click.native="toggle"></a-icon>
</div>
<user-menu class="header-index-right" :theme="theme" :style="topMenuStyle.headerIndexRight"/>
</div>
</div>
</a-layout-header>
</template>
<script>
import UserMenu from '../tools/UserMenu'
import SMenu from '../menu/'
import Logo from '../tools/Logo'
import { mixin } from '@/utils/mixin.js'
export default {
name: 'GlobalHeader',
components: {
UserMenu,
SMenu,
Logo
},
mixins: [mixin],
props: {
mode: {
type: String,
// sidemenu, topmenu
default: 'sidemenu'
},
menus: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
collapsed: {
type: Boolean,
required: false,
default: false
},
device: {
type: String,
required: false,
default: 'desktop'
}
},
data() {
return {
headerBarFixed: false,
//update-begin--author:sunjianlei---date:20190508------for: 顶部导航栏过长时显示更多按钮-----
topMenuStyle: {
headerIndexLeft: {},
topNavHeader: {},
headerIndexRight: {},
topSmenuStyle: {}
}
}
},
watch: {
/** 监听设备变化 */
device() {
if (this.mode === 'topmenu') {
this.buildTopMenuStyle()
}
},
/** 监听导航栏模式变化 */
mode(newVal) {
if (newVal === 'topmenu') {
this.buildTopMenuStyle()
}
}
},
//update-end--author:sunjianlei---date:20190508------for: 顶部导航栏过长时显示更多按钮-----
mounted() {
window.addEventListener('scroll', this.handleScroll)
//update-begin--author:sunjianlei---date:20190508------for: 顶部导航栏过长时显示更多按钮-----
if (this.mode === 'topmenu') {
this.buildTopMenuStyle()
}
//update-end--author:sunjianlei---date:20190508------for: 顶部导航栏过长时显示更多按钮-----
},
methods: {
handleScroll() {
if (this.autoHideHeader) {
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
if (scrollTop > 100) {
this.headerBarFixed = true
} else {
this.headerBarFixed = false
}
} else {
this.headerBarFixed = false
}
},
toggle() {
this.$emit('toggle')
},
//update-begin--author:sunjianlei---date:20190508------for: 顶部导航栏过长时显示更多按钮-----
buildTopMenuStyle() {
if (this.mode === 'topmenu') {
if (this.device === 'mobile') {
// 手机端需要清空样式,否则显示会错乱
this.topMenuStyle.topNavHeader = {}
this.topMenuStyle.topSmenuStyle = {}
this.topMenuStyle.headerIndexRight = {}
this.topMenuStyle.headerIndexLeft = {}
} else {
let rightWidth = '360px'
this.topMenuStyle.topNavHeader = { 'min-width': '165px' }
this.topMenuStyle.topSmenuStyle = { 'width': 'calc(100% - 165px)' }
this.topMenuStyle.headerIndexRight = { 'min-width': rightWidth }
this.topMenuStyle.headerIndexLeft = { 'width': `calc(100% - ${rightWidth})` }
}
}
}
//update-begin--author:sunjianlei---date:20190508------for: 顶部导航栏过长时显示更多按钮-----
}
}
</script>
<style lang="scss" scoped>
/* update_begin author:scott date:20190220 for: 缩小首页布局顶部的高度*/
$height: 59px;
.layout {
.top-nav-header-index {
.header-index-wide {
margin-left: 10px;
.ant-menu.ant-menu-horizontal {
height: $height;
line-height: $height;
}
}
.trigger {
line-height: 64px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
}
}
.header {
z-index: 2;
color: white;
height: $height;
background-color: #1890ff;
transition: background 300ms;
/* dark 样式 */
&.dark {
color: #000000;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
background-color: white !important;
}
}
.header, .top-nav-header-index {
&.dark .trigger:hover {
background: rgba(0, 0, 0, 0.05);
}
}
}
.ant-layout-header {
height: $height;
line-height: $height;
}
/* update_end author:scott date:20190220 for: 缩小首页布局顶部的高度*/
</style>

View File

@ -0,0 +1,660 @@
<template>
<a-layout class="layout" :class="[device]">
<template v-if="layoutMode === 'sidemenu'">
<a-drawer
v-if="device === 'mobile'"
:wrapClassName="'drawer-sider ' + navTheme"
placement="left"
@close="() => this.collapsed = false"
:closable="false"
:visible="collapsed"
width="200px"
>
<side-menu
mode="inline"
:menus="menus"
@menuSelect="menuSelect"
:theme="navTheme"
:collapsed="false"
:collapsible="true"></side-menu>
</a-drawer>
<side-menu
v-else
mode="inline"
:menus="menus"
@menuSelect="myMenuSelect"
:theme="navTheme"
:collapsed="collapsed"
:collapsible="true"></side-menu>
</template>
<!-- 下次优化这些代码 -->
<template v-else>
<a-drawer
v-if="device === 'mobile'"
:wrapClassName="'drawer-sider ' + navTheme"
placement="left"
@close="() => this.collapsed = false"
:closable="false"
:visible="collapsed"
width="200px"
>
<side-menu
mode="inline"
:menus="menus"
@menuSelect="menuSelect"
:theme="navTheme"
:collapsed="false"
:collapsible="true"></side-menu>
</a-drawer>
</template>
<a-layout
:class="[layoutMode, `content-width-${contentWidth}`]"
:style="{ paddingLeft: fixSiderbar && isDesktop() ? `${sidebarOpened ? 200 : 80}px` : '0' }">
<!-- layout header -->
<global-header
:mode="layoutMode"
:menus="menus"
:theme="navTheme"
:collapsed="collapsed"
:device="device"
@toggle="toggle"
/>
<!-- layout content -->
<a-layout-content :style="{ height: '100%', paddingTop: fixedHeader ? '59px' : '0' }">
<slot></slot>
</a-layout-content>
<!-- layout footer -->
<a-layout-footer style="padding: 0px">
<global-footer/>
</a-layout-footer>
</a-layout>
<setting-drawer></setting-drawer>
</a-layout>
</template>
<script>
import SideMenu from '@/components/menu/SideMenu'
import GlobalHeader from '@/components/page/GlobalHeader'
import GlobalFooter from '@/components/page/GlobalFooter'
import SettingDrawer from '@/components/setting/SettingDrawer'
import { triggerWindowResizeEvent } from '@/utils/util'
import { mapState, mapActions } from 'vuex'
import { mixin, mixinDevice } from '@/utils/mixin.js'
export default {
name: 'GlobalLayout',
components: {
SideMenu,
GlobalHeader,
GlobalFooter,
SettingDrawer
},
mixins: [mixin, mixinDevice],
data() {
return {
collapsed: false,
activeMenu:{},
menus: []
}
},
computed: {
...mapState({
// 主路由
mainRouters: state => state.permission.addRouters,
// 后台菜单
permissionMenuList: state => state.user.permissionList
})
},
watch: {
sidebarOpened(val) {
this.collapsed = !val
}
},
created() {
//--update-begin----author:scott---date:20190320------for:根据后台菜单配置判断是否路由菜单字段动态选择是否生成路由为了支持参数URL菜单------
//this.menus = this.mainRouters.find((item) => item.path === '/').children;
this.menus = this.permissionMenuList
// 根据后台配置菜单,重新排序加载路由信息
console.log('----加载菜单逻辑----')
console.log(this.mainRouters)
console.log(this.permissionMenuList)
console.log('----navTheme------'+this.navTheme)
//--update-end----author:scott---date:20190320------for:根据后台菜单配置判断是否路由菜单字段动态选择是否生成路由为了支持参数URL菜单------
},
methods: {
...mapActions(['setSidebar']),
toggle() {
this.collapsed = !this.collapsed
this.setSidebar(!this.collapsed)
triggerWindowResizeEvent()
},
menuSelect() {
if (!this.isDesktop()) {
this.collapsed = false
}
},
//update-begin-author:taoyan date:20190430 for:动态路由title显示配置的菜单title而不是其对应路由的title
myMenuSelect(value){
//此处触发动态路由被点击事件
this.findMenuBykey(this.menus,value.key)
this.$emit("dynamicRouterShow",value.key,this.activeMenu.meta.title)
},
findMenuBykey(menus,key){
for(let i of menus){
if(i.path==key){
this.activeMenu = {...i}
}else if(i.children && i.children.length>0){
this.findMenuBykey(i.children,key)
}
}
}
//update-end-author:taoyan date:20190430 for:动态路由title显示配置的菜单title而不是其对应路由的title
}
}
</script>
<style lang="scss">
body {
// 打开滚动条固定显示
overflow-y: scroll;
&.colorWeak {
filter: invert(80%);
}
}
.layout {
min-height: 100vh !important;
overflow-x: hidden;
&.mobile {
.ant-layout-content {
.content {
margin: 24px 0 0;
}
}
/**
* ant-table-wrapper
* 覆盖的表格手机模式样式,如果想修改在手机上表格最低宽度,可以在这里改动
*/
.ant-table-wrapper {
.ant-table-content {
overflow-y: auto;
}
.ant-table-body {
min-width: 800px;
}
}
.sidemenu {
.ant-header-fixedHeader {
&.ant-header-side-opened, &.ant-header-side-closed {
width: 100%
}
}
}
.topmenu {
/* 必须为 topmenu 才能启用流式布局 */
&.content-width-Fluid {
.header-index-wide {
margin-left: 0;
}
}
}
.header, .top-nav-header-index {
.user-wrapper .action {
padding: 0 12px;
}
}
}
&.ant-layout-has-sider {
flex-direction: row;
}
.trigger {
font-size: 22px;
line-height: 42px;
padding: 0 18px;
cursor: pointer;
transition: color 300ms, background 300ms;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
.topmenu {
.ant-header-fixedHeader {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
transition: width .2s;
&.ant-header-side-opened {
width: 100%;
}
&.ant-header-side-closed {
width: 100%;
}
}
/* 必须为 topmenu 才能启用流式布局 */
&.content-width-Fluid {
.header-index-wide {
max-width: unset;
margin-left: 24px;
}
.page-header-index-wide {
max-width: unset;
}
}
}
.sidemenu {
.ant-header-fixedHeader {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
transition: width .2s;
&.ant-header-side-opened {
width: calc(100% - 200px)
}
&.ant-header-side-closed {
width: calc(100% - 80px)
}
}
}
.header {
height: 64px;
padding: 0 12px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
position: relative;
}
.header, .top-nav-header-index {
.user-wrapper {
float: right;
height: 100%;
.action {
cursor: pointer;
padding: 0 14px;
display: inline-block;
transition: all .3s;
height: 70%;
line-height: 46px;
&.action-full {
height: 100%;
}
&:hover {
background: rgba(255, 255, 255, 0.3);
}
.avatar {
margin: 20px 10px 20px 0;
color: #1890ff;
background: hsla(0, 0%, 100%, .85);
vertical-align: middle;
}
.icon {
font-size: 16px;
padding: 4px;
}
}
}
&.dark {
.user-wrapper {
.action {
color: black;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
}
}
}
}
&.mobile {
.top-nav-header-index {
.header-index-wide {
.header-index-left {
.trigger {
color: rgba(255, 255, 255, 0.85);
padding: 0 12px;
}
.logo.top-nav-header {
text-align: center;
width: 56px;
line-height: 58px;
}
}
}
.user-wrapper .action .avatar {
margin: 20px 0;
}
&.light {
.header-index-wide {
.header-index-left {
.trigger {
color: rgba(0, 0, 0, 0.65);
}
}
}
//
}
}
}
&.tablet {
// overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
.top-nav-header-index {
.header-index-wide {
.header-index-left {
.logo > a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.top-nav-header-index {
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
position: relative;
transition: background .3s, width .2s;
.header-index-wide {
width: 100%;
margin: auto;
padding: 0 20px 0 0;
display: flex;
height: 59px;
.ant-menu.ant-menu-horizontal {
border: none;
height: 64px;
line-height: 64px;
}
.header-index-left {
flex: 1 1;
display: flex;
.logo.top-nav-header {
width: 165px;
height: 64px;
position: relative;
line-height: 64px;
transition: all .3s;
overflow: hidden;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: #fff;
display: inline-block;
vertical-align: top;
font-size: 16px;
margin: 0 0 0 12px;
font-weight: 400;
}
}
}
.header-index-right {
float: right;
height: 59px;
overflow: hidden;
.action:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
}
&.light {
background-color: #fff;
.header-index-wide {
.header-index-left {
.logo {
h1 {
color: #002140;
}
}
}
}
}
&.dark {
.user-wrapper {
.action {
color: white;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
.header-index-wide .header-index-left .trigger:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
// 内容区
.layout-content {
margin: 24px 24px 0px;
height: 64px;
padding: 0 12px 0 0;
}
}
.topmenu {
.page-header-index-wide {
margin: 0 auto;
width: 100%;
}
}
// drawer-sider 自定义
.ant-drawer.drawer-sider {
.sider {
box-shadow: none;
}
&.dark {
.ant-drawer-content {
background-color: rgb(0, 21, 41);
}
}
&.light {
box-shadow: none;
.ant-drawer-content {
background-color: #fff;
}
}
.ant-drawer-body {
padding: 0
}
}
// 菜单样式
.sider {
box-shadow: 2px 116px 6px 0 rgba(0, 21, 41, .35);
position: relative;
z-index: 10;
&.ant-fixed-sidemenu {
position: fixed;
height: 100%;
}
.logo {
height: 64px;
position: relative;
line-height: 64px;
padding-left: 24px;
-webkit-transition: all .3s;
transition: all .3s;
background: #002140;
overflow: hidden;
img, h1 {
display: inline-block;
vertical-align: middle;
}
img {
height: 32px;
}
h1 {
color: #fff;
font-size: 18px;
margin: 0 0 0 8px;
font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-weight: 600;
}
}
&.light {
background-color: #fff;
box-shadow: 2px 116px 8px 0 rgba(29, 35, 41, 0.05);
.logo {
background: #fff;
box-shadow: 1px 1px 0 0 #e8e8e8;
h1 {
color: unset;
}
}
.ant-menu-light {
border-right-color: transparent;
}
}
}
// 外置的样式控制
.user-dropdown-menu-wrapper.ant-dropdown-menu {
padding: 4px 0;
.ant-dropdown-menu-item {
width: 160px;
}
.ant-dropdown-menu-item > .anticon:first-child,
.ant-dropdown-menu-item > a > .anticon:first-child,
.ant-dropdown-menu-submenu-title > .anticon:first-child
.ant-dropdown-menu-submenu-title > a > .anticon:first-child {
min-width: 12px;
margin-right: 8px;
}
}
// 数据列表 样式
.table-alert {
margin-bottom: 16px;
}
.table-page-search-wrapper {
.ant-form-inline {
.ant-form-item {
display: flex;
margin-bottom: 24px;
margin-right: 0;
.ant-form-item-control-wrapper {
flex: 1 1;
display: inline-block;
vertical-align: middle;
}
> .ant-form-item-label {
line-height: 32px;
padding-right: 8px;
width: auto;
}
.ant-form-item-control {
height: 32px;
line-height: 32px;
}
}
}
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
}
.content {
.table-operator {
margin-bottom: 18px;
button {
margin-right: 8px;
}
}
}
</style>

View File

@ -0,0 +1,238 @@
<template>
<div class="page-header">
<div class="page-header-index-wide">
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
<router-link v-if="item.name != name" :to="{ path: item.path }">
{{ item.meta.title }}
</router-link>
<span v-else>{{ item.meta.title }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
<div class="detail">
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
<div class="row">
<img v-if="logo" :src="logo" class="logo"/>
<h1 v-if="title" class="title">{{ title }}</h1>
<div class="action">
<slot name="action"></slot>
</div>
</div>
<div class="row">
<div v-if="avatar" class="avatar">
<a-avatar :src="avatar"/>
</div>
<div v-if="this.$slots.content" class="headerContent">
<slot name="content"></slot>
</div>
<div v-if="this.$slots.extra" class="extra">
<slot name="extra"></slot>
</div>
</div>
<div>
<slot name="pageMenu"></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Breadcrumb from '@/components/tools/Breadcrumb'
export default {
name: "PageHeader",
components: {
"s-breadcrumb": Breadcrumb
},
props: {
title: {
type: String,
default: '',
required: false
},
breadcrumb: {
type: Array,
default: null,
required: false
},
logo: {
type: String,
default: '',
required: false
},
avatar: {
type: String,
default: '',
required: false
}
},
data() {
return {
name: '',
breadList: [],
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
this.breadList = []
// this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: '首页'}})
this.name = this.$route.name
this.$route.matched.forEach((item) => {
// item.name !== 'index' && this.breadList.push(item)
this.breadList.push(item)
})
}
},
watch: {
$route() {
this.getBreadcrumb()
}
}
}
</script>
<style lang="scss" scoped>
.page-header {
background: #fff;
padding: 16px 32px 0;
border-bottom: 1px solid #e8e8e8;
.breadcrumb {
margin-bottom: 16px;
}
.detail {
display: flex;
/*margin-bottom: 16px;*/
.avatar {
flex: 0 1 72px;
margin: 0 24px 8px 0;
& > span {
border-radius: 72px;
display: block;
width: 72px;
height: 72px;
}
}
.main {
width: 100%;
flex: 0 1 auto;
.row {
display: flex;
width: 100%;
.avatar {
margin-bottom: 16px;
}
}
.title {
font-size: 20px;
font-weight: 500;
font-size: 20px;
line-height: 28px;
font-weight: 500;
color: rgba(0,0,0,.85);
margin-bottom: 16px;
flex: auto;
}
.logo {
width: 28px;
height: 28px;
border-radius: 4px;
margin-right: 16px;
}
.content, .headerContent {
flex: auto;
color: rgba(0,0,0,.45);
line-height: 22px;
.link {
margin-top: 16px;
line-height: 24px;
a {
font-size: 14px;
margin-right: 32px;
}
}
}
.extra {
flex: 0 1 auto;
margin-left: 88px;
min-width: 242px;
text-align: right;
}
.action {
margin-left: 56px;
min-width: 266px;
flex: 0 1 auto;
text-align: right;
&:empty {
display: none;
}
}
}
}
}
.mobile .page-header {
.main {
.row {
flex-wrap: wrap;
.avatar {
flex: 0 1 25%;
margin: 0 2% 8px 0;
}
.content, .headerContent {
flex: 0 1 70%;
.link {
margin-top: 16px;
line-height: 24px;
a {
font-size: 14px;
margin-right: 10px;
}
}
}
.extra {
flex: 1 1 auto;
margin-left: 0;
min-width: 0;
text-align: right;
}
.action {
margin-left: unset;
min-width: 266px;
flex: 0 1 auto;
text-align: left;
margin-bottom: 12px;
&:empty {
display: none;
}
}
}
}
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div :style="!$route.meta.pageHeader ? 'margin: -10px -24px 0;' : null">
<!-- pageHeader , route meta hideHeader:true on hide -->
<page-header v-if="!$route.meta.pageHeader" :title="title" :logo="logo" :avatar="avatar">
<slot slot="action" name="action"></slot>
<slot slot="content" name="headerContent"></slot>
<div slot="content" v-if="!this.$slots.headerContent && desc">
<p style="font-size: 14px;color: rgba(0,0,0,.65)">{{ desc }}</p>
<div class="link">
<template v-for="(link, index) in linkList">
<a :key="index" :href="link.href">
<a-icon :type="link.icon"/>
<span>{{ link.title }}</span>
</a>
</template>
</div>
</div>
<slot slot="extra" name="extra"></slot>
<div slot="pageMenu">
<div class="page-menu-search" v-if="search">
<a-input-search style="width: 80%; max-width: 522px;" placeholder="请输入..." size="large" enterButton="搜索" />
</div>
<div class="page-menu-tabs" v-if="tabs && tabs.items">
<!-- @change="callback" :activeKey="activeKey" -->
<a-tabs :tabBarStyle="{margin: 0}" @change="tabs.callback" :activeKey="tabs.active()">
<a-tab-pane v-for="item in tabs.items" :tab="item.title" :key="item.key"></a-tab-pane>
</a-tabs>
</div>
</div>
</page-header>
<div class="content">
<div :class="['page-header-index-wide']">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
import PageHeader from './PageHeader'
export default {
name: "LayoutContent",
components: {
PageHeader
},
// ['desc', 'logo', 'title', 'avatar', 'linkList', 'extraImage']
props: {
desc: {
type: String,
default: null
},
logo: {
type: String,
default: null
},
title: {
type: String,
default: null
},
avatar: {
type: String,
default: null
},
linkList: {
type: Array,
default: null
},
extraImage: {
type: String,
default: null
},
search: {
type: Boolean,
default: false
},
tabs: {
type: Object,
default: () => {}
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.content {
margin: 24px 24px 0;
.link {
margin-top: 16px;
&:not(:empty) {
margin-bottom: 16px;
}
a {
margin-right: 32px;
height: 24px;
line-height: 24px;
display: inline-block;
i {
font-size: 24px;
margin-right: 8px;
vertical-align: middle;
}
span {
height: 24px;
line-height: 24px;
display: inline-block;
vertical-align: middle;
}
}
}
}
.page-menu-search {
text-align: center;
margin-bottom: 16px;
}
.page-menu-tabs {
margin-top: 48px;
}
.page-header[data-v-6740ec88] {
margin: 0px 24px 0;
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<a-popover trigger="click" placement="bottomRight" :overlayStyle="{ width: '300px' }">
<template slot="content">
<a-spin :spinning="loadding">
<a-tabs>
<a-tab-pane v-for="(tab, k) in tabs" :tab="tab.title" :key="k">
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<span @click="fetchNotice" class="header-notice">
<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",
props: {
tabs: {
type: Array,
default: null,
required: true
}
},
data () {
return {
loadding: false
}
},
methods: {
fetchNotice () {
if (this.loadding) {
this.loadding = false
return
}
this.loadding = true
setTimeout(() => {
this.loadding = false
}, 2000)
}
}
}
</script>
<style lang="scss" scoped>
.header-notice{
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
}
</style>

View File

@ -0,0 +1,329 @@
<template>
<div class="setting-drawer">
<a-drawer
width="300"
placement="right"
:closable="false"
@close="onClose"
:visible="visible"
:style="{}"
>
<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">
暗色菜单风格
</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>
<div slot="title" :style="{ textDecoration: !fixedHeader ? 'line-through' : 'unset' }">下滑时隐藏 Header</div>
</a-list-item-meta>
</a-list-item>
<a-list-item >
<a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :checked="dataFixSiderbar" @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="multipage" @change="onMultipageWeak" />
<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-alert type="warning">
<span slot="message">
配置栏只在开发环境用于预览,生产环境不会展现,请手动修改配置文件
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/defaultSettings.js" target="_blank">src/defaultSettings.js</a>
</span>
</a-alert>
</div>
</div>
<div class="setting-drawer-index-handle" @click="toggle">
<a-icon type="setting" v-if="!visible"/>
<a-icon type="close" v-else/>
</div>
</a-drawer>
</div>
</template>
<script>
import DetailList from '@/components/tools/DetailList'
import SettingItem from '@/components/setting/SettingItem'
import config from '@/defaultSettings'
import { updateTheme, updateColorWeak, colorList } from '@/components/tools/setting'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
components: {
DetailList,
SettingItem
},
mixins: [mixin, mixinDevice],
data() {
return {
visible: true,
colorList,
dataFixSiderbar: false
}
},
watch: {
},
mounted () {
const vm = this
setTimeout(() => {
vm.visible = false
}, 16)
// 当主题色不是默认色时,才进行主题编译
if (this.primaryColor !== config.primaryColor) {
updateTheme(this.primaryColor)
}
if (this.colorWeak !== config.colorWeak) {
updateColorWeak(this.colorWeak)
}
if (this.multipage !== config.multipage) {
this.$store.dispatch('ToggleMultipage', this.multipage)
}
},
methods: {
showDrawer() {
this.visible = true
},
onClose() {
this.visible = false
},
toggle() {
this.visible = !this.visible
},
onColorWeak (checked) {
this.$store.dispatch('ToggleWeak', checked)
updateColorWeak(checked)
},
onMultipageWeak (checked) {
this.$store.dispatch('ToggleMultipage', checked)
},
handleMenuTheme (theme) {
this.$store.dispatch('ToggleTheme', theme)
},
handleLayout (mode) {
this.$store.dispatch('ToggleLayoutMode', mode)
// 因为顶部菜单不能固定左侧菜单栏,所以强制关闭
this.handleFixSiderbar(false)
// 触发窗口resize事件
triggerWindowResizeEvent()
},
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') {
fixed = false
}
this.dataFixSiderbar = fixed
this.$store.dispatch('ToggleFixSiderbar', fixed)
}
},
}
</script>
<style lang="scss" scoped>
.setting-drawer-index-content {
.setting-drawer-index-blockChecbox {
display: flex;
.setting-drawer-index-item {
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
img {
width: 48px;
}
.setting-drawer-index-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: #1890ff;
font-size: 14px;
font-weight: 700;
}
}
}
.setting-drawer-theme-color-colorBlock {
width: 20px;
height: 20px;
border-radius: 2px;
float: left;
cursor: pointer;
margin-right: 8px;
padding-left: 0px;
padding-right: 0px;
text-align: center;
color: #fff;
font-weight: 700;
i {
font-size: 14px;
}
}
}
.setting-drawer-index-handle {
position: absolute;
top: 240px;
background: #1890ff;
width: 48px;
height: 48px;
right: 300px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
pointer-events: auto;
z-index: 1001;
text-align: center;
font-size: 16px;
border-radius: 4px 0 0 4px;
i {
color: rgb(255, 255, 255);
font-size: 20px;
}
}
</style>

View File

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

View File

@ -0,0 +1,292 @@
Table 重封装组件说明
====
封装说明
----
> 基础的使用方式与 API 与 [官方版(Table)](https://vuecomponent.github.io/ant-design-vue/components/table-cn/) 本一致,在其基础上,封装了加载数据的方法。
>
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 Table 组件传递绑定 `:data="Promise"` 对象即可
例子1
----
(基础使用)
```vue
<template>
<s-table
ref="table"
:rowKey="(record) => record.data.id"
size="default"
:columns="columns"
:data="loadData"
>
</s-table>
</template>
<script>
import STable from '@/components/table/'
export default {
components: {
STable
},
data() {
return {
columns: [
{
title: '规则编号',
dataIndex: 'no'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '服务调用次数',
dataIndex: 'callNo',
sorter: true,
needTotal: true,
customRender: (text) => text + ' 次'
},
{
title: '状态',
dataIndex: 'status',
needTotal: true
},
{
title: '更新时间',
dataIndex: 'updatedAt',
sorter: true
}
],
// 查询条件参数
queryParam: {},
// 加载数据方法 必须为 Promise 对象
loadData: parameter => {
return this.$http.get('/service', {
params: Object.assign(parameter, this.queryParam)
}).then(res => {
return res.result
})
},
}
}
}
</script>
```
例子2
----
(简单的表格,最后一列是各种操作)
```vue
<template>
<s-table
ref="table"
size="default"
:columns="columns"
:data="loadData"
>
<span slot="action" slot-scope="text, record">
<a>编辑</a>
<a-divider type="vertical"/>
<a-dropdown>
<a class="ant-dropdown-link">
更多 <a-icon type="down"/>
</a>
<a-menu slot="overlay">
<a-menu-item>
<a href="javascript:;">1st menu item</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:;">2nd menu item</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:;">3rd menu item</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</span>
</s-table>
</template>
<script>
import STable from '@/components/table/'
export default {
components: {
STable
},
data() {
return {
columns: [
{
title: '规则编号',
dataIndex: 'no'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '服务调用次数',
dataIndex: 'callNo',
},
{
title: '状态',
dataIndex: 'status',
},
{
title: '更新时间',
dataIndex: 'updatedAt',
},
{
table: '操作',
dataIndex: 'action',
scopedSlots: {customRender: 'action'},
}
],
// 查询条件参数
queryParam: {},
// 加载数据方法 必须为 Promise 对象
loadData: parameter => {
return this.$http.get('/service', {
params: Object.assign(parameter, this.queryParam)
}).then(res => {
return res.result
})
},
}
},
methods: {
edit(row) {
// axios 发送数据到后端 修改数据成功后
// 调用 refresh() 重新加载列表数据
// 这里 setTimeout 模拟发起请求的网络延迟..
setTimeout(() => {
this.$refs.table.refresh()
}, 1500)
}
}
}
</script>
```
内置方法
----
通过 `this.$refs.table` 调用
`this.$refs.table.refresh()` 刷新列表 (用户新增/修改数据后,重载列表数据)
> 注意:要调用 `refresh()` 需要给表格组件设定 `ref` 值
注意事项
----
> 你可能需要为了与后端提供的接口返回结果一致而去修改以下代码:
(需要注意的是,这里的修改是全局性的,意味着整个项目所有使用该 table 组件都需要遵守这个返回结果定义的字段。)
修改 `@/components/table/index.js` 第 106 行起
```javascript
result.then(r => {
this.localPagination = Object.assign({}, this.localPagination, {
current: r.pageNo, // 返回结果中的当前分页数
total: r.totalCount, // 返回结果中的总记录数
showSizeChanger: this.showSizeChanger,
pageSize: (pagination && pagination.pageSize) ||
this.localPagination.pageSize
});
!r.totalCount && ['auto', false].includes(this.showPagination) && (this.localPagination = false)
this.localDataSource = r.data; // 返回结果中的数组数据
this.localLoading = false
});
```
返回 JSON 例子:
```json
{
"message": "",
"result": {
"data": [{
id: 1,
cover: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
title: 'Alipay',
description: '那是一种内在的东西, 他们到达不了,也无法触及的',
status: 1,
updatedAt: '2018-07-26 00:00:00'
},
{
id: 2,
cover: 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png',
title: 'Angular',
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
status: 1,
updatedAt: '2018-07-26 00:00:00'
},
{
id: 3,
cover: 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png',
title: 'Ant Design',
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
status: 1,
updatedAt: '2018-07-26 00:00:00'
},
{
id: 4,
cover: 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png',
title: 'Ant Design Pro',
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
status: 1,
updatedAt: '2018-07-26 00:00:00'
},
{
id: 5,
cover: 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png',
title: 'Bootstrap',
description: '凛冬将至',
status: 1,
updatedAt: '2018-07-26 00:00:00'
},
{
id: 6,
cover: 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png',
title: 'Vue',
description: '生命就像一盒巧克力,结果往往出人意料',
status: 1,
updatedAt: '2018-07-26 00:00:00'
}
],
"pageSize": 10,
"pageNo": 0,
"totalPage": 6,
"totalCount": 57
},
"status": 200,
"timestamp": 1534955098193
}
```
更新时间
----
该文档最后更新于: 2018-10-31 PM 08:15

View File

@ -0,0 +1,252 @@
<template>
<div class="standard-table">
<div class="alert">
<a-alert type="info" :show-icon="true">
<div slot="message">
已选择&nbsp;<a style="font-weight: 600">{{ selectedRows.length }}</a>&nbsp;&nbsp;
<template v-for="(item, index) in needTotalList" v-if="item.needTotal">
{{ item.title }} 总计&nbsp;
<a :key="index" style="font-weight: 600">
{{ item.customRender ? item.customRender(item.total) : item.total }}
</a>&nbsp;&nbsp;
</template>
<a style="margin-left: 24px" @click="onClearSelected">清空</a>
</div>
</a-alert>
</div>
<a-table
:size="size"
:bordered="bordered"
:loading="loading"
:columns="columns"
:dataSource="current"
:rowKey="rowKey"
:pagination="pagination"
:rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: updateSelect }"
>
</a-table>
</div>
</template>
<script>
export default {
name: "StandardTable",
// props: ['bordered', 'loading', 'columns', 'data', 'rowKey', 'pagination', 'selectedRows'],
props: {
/**
* 数据加载函数,返回值必须是 Promise
* 默认情况下必须传递 data 参数;
* 如果使用本地数据渲染表格,业务代码中将获取本地数据包装为 Promise 即可。
*
* currentData 用于向外暴露表格当前渲染的数据,
* 业务开发中也可以直接修改 currentData从而重新渲染表格仅推荐用于客户端排序、数据过滤等场景
*/
data: {
type: Function,
required: true
},
dataSource: {
type: Array,
default () {
return []
}
},
columns: {
type: Array,
required: true
},
/* pagination: {
type: Object,
default () {
return {}
}
},*/
pageSize: {
type: Number,
default: 10
},
pageNum: {
type: Number,
default: 1
},
pageSizeOptions: {
type: Array,
default () {
return ['10', '20', '30', '40', '50']
}
},
responseParamsName: {
type: Object,
default () {
return {}
}
},
bordered: {
type: Boolean,
default: false
},
/**
* 表格大小风格default, middle, small
*/
size: {
type: String,
default: 'default'
},
rowKey: {
type: String,
default: ''
},
selectedRows: {
type: Array,
default: null
}
},
data () {
return {
needTotalList: [],
selectedRowKeys: [],
loading: true,
total: 0,
pageNumber: this.pageNum,
currentPageSize: this.pageSize,
defaultCurrent: 1,
sortParams: {},
current: [],
pagination: {},
paramsName: {},
}
},
created () {
//数据请求参数配置
this.paramsName = Object.assign(
{},
{
pageNumber: "pageNo",
pageSize: "pageSize",
total: "totalCount",
results: "data",
sortColumns: "sortColumns"
},
this.responseParamsName
);
this.needTotalList = this.initTotalList(this.columns)
// load data
this.loadData( { pageNum: this.pageNumber } )
},
methods: {
updateSelect (selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
let list = this.needTotalList
this.needTotalList = list.map(item => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
return sum + val[item.dataIndex]
}, 0)
}
})
this.$emit('change', selectedRowKeys, selectedRows)
},
initTotalList (columns) {
const totalList = []
columns.forEach(column => {
if (column.needTotal) {
totalList.push({ ...column, total: 0 })
}
})
return totalList
},
loadData (params) {
let that = this
that.loading = true
params = Object.assign({}, params)
const remoteParams = Object.assign({}, that.sortParams)
remoteParams[that.paramsName.pageNumber] = params.pageNum || that.pageNumber
remoteParams[that.paramsName.pageSize] = params.pageSize || that.currentPageSize
if (params.pageNum) {
that.pageNumber = params.pageNum
}
if (params.pageSize) {
that.currentPageSize = params.pageSize
}
let dataPromise = that.data(remoteParams)
dataPromise.then( response => {
if (!response) {
that.loading = false
return
}
let results = response[that.paramsName.results]
results = (results instanceof Array && results) || []
that.current = results
that.$emit("update:currentData", that.current.slice())
that.$emit("dataloaded", that.current.slice())
that.total = response[that.paramsName.total] * 1
that.pagination = that.pager()
that.loading = false
}, () => {
// error callback
that.loading = false
})
},
// eslint-disable-next-line
onPagerChange (page, pageSize) {
this.pageNumber = page
this.loadData({ pageNum: page })
},
onPagerSizeChange (current, size) {
this.currentPageSize = size
/*
if (current === this.pageNumber) this.loadData()
console.log('page-size-change', current, size)
*/
},
onClearSelected () {
this.selectedRowKeys = []
this.updateSelect([], [])
},
pager () {
return {
total: this.total,
showTotal: total => `共有 ${total} 条`,
showSizeChanger: true,
pageSizeOptions: this.pageSizeOptions,
pageSize: this.pageSize,
defaultCurrent: this.defaultCurrent,
onChange: this.onPagerChange,
onShowSizeChange: this.onPagerSizeChange
}
}
},
watch: {
'selectedRows': function (selectedRows) {
this.needTotalList = this.needTotalList.map(item => {
return {
...item,
total: selectedRows.reduce( (sum, val) => {
return sum + val[item.dataIndex]
}, 0)
}
})
}
}
}
</script>
<style scoped>
.alert {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,265 @@
import T from "ant-design-vue/es/table/Table";
import get from "lodash.get"
export default {
data() {
return {
needTotalList: [],
selectedRows: [],
selectedRowKeys: [],
localLoading: false,
localDataSource: [],
localPagination: Object.assign({}, T.props.pagination)
};
},
props: Object.assign({}, T.props, {
rowKey: {
type: [String, Function],
default: 'id'
},
data: {
type: Function,
required: true
},
pageNum: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
showSizeChanger: {
type: Boolean,
default: true
},
showAlertInfo: {
type: Boolean,
default: false
},
showPagination: {
default: 'auto'
}
}),
watch: {
'localPagination.current'(val) {
this.$router.push({
name: this.$route.name,
params: Object.assign({}, this.$route.params, {
pageNo: val
}),
});
},
pageNum(val) {
Object.assign(this.localPagination, {
current: val
});
},
pageSize(val) {
console.log('pageSize:', val)
Object.assign(this.localPagination, {
pageSize: val
});
},
showSizeChanger(val) {
console.log('showSizeChanger', val)
Object.assign(this.localPagination, {
showSizeChanger: val
});
}
},
created() {
this.localPagination = ['auto', true].includes(this.showPagination) && Object.assign({}, this.localPagination, {
current: this.pageNum,
pageSize: this.pageSize,
showSizeChanger: this.showSizeChanger
});
this.needTotalList = this.initTotalList(this.columns)
this.loadData();
},
methods: {
refresh() {
this.loadData();
},
loadData(pagination, filters, sorter) {
this.localLoading = true
var result = this.data(
Object.assign({
pageNo: (pagination && pagination.current) ||
this.localPagination.current,
pageSize: (pagination && pagination.pageSize) ||
this.localPagination.pageSize
},
(sorter && sorter.field && {
sortField: sorter.field
}) || {},
(sorter && sorter.order && {
sortOrder: sorter.order
}) || {}, {
...filters
}
)
);
if (result instanceof Promise) {
result.then(r => {
this.localPagination = Object.assign({}, this.localPagination, {
current: r.pageNo, // 返回结果中的当前分页数
total: r.totalCount, // 返回结果中的总记录数
showSizeChanger: this.showSizeChanger,
pageSize: (pagination && pagination.pageSize) ||
this.localPagination.pageSize
});
!r.totalCount && ['auto', false].includes(this.showPagination) && (this.localPagination = false)
this.localDataSource = r.data; // 返回结果中的数组数据
this.localLoading = false
});
}
},
initTotalList(columns) {
const totalList = []
columns && columns instanceof Array && columns.forEach(column => {
if (column.needTotal) {
totalList.push({ ...column,
total: 0
})
}
})
return totalList
},
updateSelect(selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
this.selectedRows = selectedRows
let list = this.needTotalList
this.needTotalList = list.map(item => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
let total = sum + get(val, item.dataIndex)
return isNaN(total) ? 0 : total
}, 0)
}
})
// this.$emit('change', selectedRowKeys, selectedRows)
},
updateEdit() {
this.selectedRows = []
},
onClearSelected() {
this.selectedRowKeys = []
this.updateSelect([], [])
},
renderMsg(h) {
const _vm = this
let d = []
// 构建 已选择
d.push(
h('span', {
style: {
marginRight: '12px'
}
}, ['已选择 ', h('a', {
style: {
fontWeight: 600
}
}, this.selectedRows.length)])
);
// 构建 列统计
this.needTotalList.map(item => {
d.push(h('span', {
style: {
marginRight: '12px'
}
},
[
`${ item.title }总计 `,
h('a', {
style: {
fontWeight: 600
}
}, `${ !item.customRender ? item.total : item.customRender(item.total) }`)
]))
});
// 构建 清空选择
d.push(h('a', {
style: {
marginLeft: '24px'
},
on: {
click: _vm.onClearSelected
}
}, '清空'))
return d
},
renderAlert(h) {
return h('span', {
slot: 'message'
}, this.renderMsg(h))
},
},
render(h) {
const _vm = this
let props = {},
localKeys = Object.keys(this.$data);
Object.keys(T.props).forEach(k => {
let localKey = `local${k.substring(0,1).toUpperCase()}${k.substring(1)}`;
if (localKeys.includes(localKey)) {
return props[k] = _vm[localKey];
}
return props[k] = _vm[k];
})
// 显示信息提示
if (this.showAlertInfo) {
props.rowSelection = {
selectedRowKeys: this.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
_vm.updateSelect(selectedRowKeys, selectedRows)
_vm.$emit('onSelect', { selectedRowKeys: selectedRowKeys, selectedRows: selectedRows })
}
};
return h('div', {}, [
h("a-alert", {
style: {
marginBottom: '16px'
},
props: {
type: 'info',
showIcon: true
}
}, [_vm.renderAlert(h)]),
h("a-table", {
tag: "component",
attrs: props,
on: {
change: _vm.loadData
},
scopedSlots: this.$scopedSlots
}, this.$slots.default)
]);
}
return h("a-table", {
tag: "component",
attrs: props,
on: {
change: _vm.loadData
},
scopedSlots: this.$scopedSlots
}, this.$slots.default);
}
};

View File

@ -0,0 +1,48 @@
<template>
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item v-for="(item, index) in breadList" :key="index">
<router-link v-if="item.name != name" :to="{ path: item.path }">
{{ item.meta.title }}
</router-link>
<span v-else>{{ item.meta.title }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
</template>
<script>
export default {
data() {
return {
name: '',
breadList: [],
}
},
created () {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
console.log('this.$route.matched', this.$route.matched)
this.breadList = []
this.breadList.push({ name: 'dashboard', path: '/dashboard/', meta: { title: '首页' } })
this.name = this.$route.name
this.$route.matched.forEach((item) => {
// item.meta.name === 'dashboard' ? item.path = '/dashboard' : this.$route.path === item.path
this.breadList.push(item)
})
}
},
watch: {
$route() {
this.getBreadcrumb()
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,154 @@
<template>
<a-modal
:title="title"
:width="450"
:visible="visible"
:closable="false"
:maskClosable="closable">
<template slot="footer">
<a-button v-if="closable" @click="close">关闭</a-button>
<a-button type="primary" @click="departOk">确认</a-button>
</template>
<a-form>
<a-form-item
:labelCol="{span:4}"
:wrapperCol="{span:20}"
style="margin-bottom:10px"
:validate-status="validate_status">
<a-tooltip placement="topLeft" >
<template slot="title">
<span>您隶属于多部门请选择当前所在部门</span>
</template>
<a-avatar style="backgroundColor:#87d068" icon="gold" />
</a-tooltip>
<a-select v-model="departSelected" :class="{'valid-error':validate_status=='error'}" placeholder="请选择登录部门" style="margin-left:10px;width: 80%">
<a-icon slot="suffixIcon" type="gold" />
<a-select-option
v-for="d in departList"
:key="d.id"
:value="d.orgCode">
{{ d.departName }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import { getAction,putAction } from '@/api/manage'
export default {
name: 'DepartSelect',
props:{
title:{
type:String,
default:"部门选择",
required:false
},
closable:{
type:Boolean,
default:false,
required:false
},
username:{
type:String,
default:"",
required:false
}
},
watch:{
username(val){
if(val){
this.loadDepartList()
}
}
},
data(){
return {
visible:false,
departList:[],
departSelected:"",
validate_status:"",
currDepartName:"",
}
},
created(){
//this.loadDepartList()
},
methods:{
loadDepartList(){
return new Promise(resolve => {
let url = "/sys/user/getCurrentUserDeparts"
this.currDepartName=''
getAction(url).then(res=>{
if(res.success){
let departs = res.result.list
let orgCode = res.result.orgCode
if(departs && departs.length>0){
for(let i of departs){
if(i.orgCode == orgCode){
this.currDepartName = i.departName
break
}
}
}
this.departSelected = orgCode
this.departList = departs
if(this.currDepartName){
this.title ="部门切换当前部门 : "+this.currDepartName+""
}
}
resolve()
})
})
},
close(){
this.departClear()
},
departOk(){
if(!this.departSelected){
this.validate_status='error'
return false
}
let obj = {
orgCode:this.departSelected,
username:this.username
}
putAction("/sys/selectDepart",obj).then(res=>{
if(res.success){
this.departClear()
}
})
},
show(){
//如果组件传值username此处就不用loadDepartList了
this.loadDepartList().then(()=>{
this.visible=true
if(!this.departList || this.departList.length<=0){
this.$message.warning("您尚未设置部门信息!")
this.departClear()
}
})
},
departClear(){
this.departList=[]
this.departSelected=""
this.visible=false
this.validate_status=''
this.currDepartName=""
},
}
}
</script>
<style scoped>
.valid-error .ant-select-selection__placeholder{
color: #f5222d;
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div :class="['detail-list', size, layout === 'vertical' ? 'vertical': 'horizontal']">
<div v-if="title" class="title">{{ title }}</div>
<a-row>
<slot></slot>
</a-row>
</div>
</template>
<script>
import { Col } from 'ant-design-vue/es/grid/'
const Item = {
name: 'DetailListItem',
props: {
term: {
type: String,
default: '',
required: false
},
},
inject: {
col: {
type: Number
}
},
render () {
return (
<Col {...{props: responsive[this.col]}}>
<div class="term">{this.$props.term}</div>
<div class="content">{this.$slots.default}</div>
</Col>
)
}
}
const responsive = {
1: { xs: 24 },
2: { xs: 24, sm: 12 },
3: { xs: 24, sm: 12, md: 8 },
4: { xs: 24, sm: 12, md: 6 }
}
export default {
name: "DetailList",
Item: Item,
components: {
Col
},
props: {
title: {
type: String,
default: '',
required: false
},
col: {
type: Number,
required: false,
default: 3
},
size: {
type: String,
required: false,
default: 'large'
},
layout: {
type: String,
required: false,
default: 'horizontal'
}
},
provide () {
return {
col: this.col > 4 ? 4 : this.col
}
}
}
</script>
<style lang="scss">
.detail-list {
.title {
color: rgba(0,0,0,.85);
font-size: 14px;
font-weight: 500;
margin-bottom: 16px;
}
.term {
color: rgba(0,0,0,.85);
display: table-cell;
line-height: 20px;
margin-right: 8px;
padding-bottom: 16px;
white-space: nowrap;
&:after {
content: ":";
margin: 0 8px 0 2px;
position: relative;
top: -.5px;
}
}
.content {
color: rgba(0,0,0,.65);
display: table-cell;
line-height: 22px;
padding-bottom: 16px;
width: 100%;
}
&.small {
.title {
font-size: 14px;
color: rgba(0, 0, 0, .65);
font-weight: normal;
margin-bottom: 12px;
}
.term, .content {
padding-bottom: 8px;
}
}
&.large {
.term, .content {
padding-bottom: 16px;
}
.title {
font-size: 16px;
}
}
&.vertical {
.term {
padding-bottom: 8px;
}
.term, .content {
display: block;
}
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="toolbar">
<div style="float: left">
<slot name="extra"></slot>
</div>
<div style="float: right">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "FooterToolBar"
}
</script>
<style lang="scss" scoped>
.toolbar {
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;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div class="head-info" :class="center && 'center'">
<span>{{ title }}</span>
<p>{{ content }}</p>
<em v-if="bordered"/>
</div>
</template>
<script>
export default {
name: "HeadInfo",
props: {
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
bordered: {
type: Boolean,
default: false
},
center: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="scss" scoped>
.head-info {
position: relative;
text-align: left;
padding: 0 32px 0 0;
min-width: 125px;
&.center {
text-align: center;
padding: 0 32px;
}
span {
color: rgba(0, 0, 0, .45);
display: inline-block;
font-size: 14px;
line-height: 22px;
margin-bottom: 4px;
}
p {
color: rgba(0, 0, 0, .85);
font-size: 24px;
line-height: 32px;
margin: 0;
}
em {
background-color: #e8e8e8;
position: absolute;
height: 56px;
width: 1px;
top: 0;
right: 0;
}
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<a-popover
trigger="click"
placement="bottomRight"
:autoAdjustOverflow="true"
:arrowPointAtCenter="true"
overlayClassName="header-notice-wrapper"
@visibleChange="handleHoverChange"
:overlayStyle="{ width: '300px', top: '50px' }">
<template slot="content">
<a-spin :spinning="loadding">
<a-tabs>
<a-tab-pane :tab="msg1Title" 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="你推荐的 IT大牛 已通过第三轮面试" 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-list>
<a-list-item :key="index" v-for="(record, index) in announcement1">
<div style="margin-left: 5%;width: 80%">
<p><a @click="showAnnouncement(record)">标题{{ record.titile }}</a></p>
<p style="color: rgba(0,0,0,.45);margin-bottom: 0px">{{ record.createTime }} 发布</p>
</div>
<div style="text-align: right">
<a-tag @click="showAnnouncement(record)" v-if="record.priority === 'L'" color="blue">一般消息</a-tag>
<a-tag @click="showAnnouncement(record)" v-if="record.priority === 'M'" color="orange">重要消息</a-tag>
<a-tag @click="showAnnouncement(record)" v-if="record.priority === 'H'" color="red">紧急消息</a-tag>
</div>
</a-list-item>
<div style="margin-top: 5px;text-align: center">
<a-button @click="toMyAnnouncement()" type="dashed" block>查看更多</a-button>
</div>
</a-list>
</a-tab-pane>
<a-tab-pane :tab="msg2Title" key="2">
<a-list>
<a-list-item :key="index" v-for="(record, index) in announcement2">
<div style="margin-left: 5%;width: 80%">
<p><a @click="showAnnouncement(record)">标题{{ record.titile }}</a></p>
<p style="color: rgba(0,0,0,.45);margin-bottom: 0px">{{ record.createTime }} 发布</p>
</div>
<div style="text-align: right">
<a-tag @click="showAnnouncement(record)" v-if="record.priority === 'L'" color="blue">一般消息</a-tag>
<a-tag @click="showAnnouncement(record)" v-if="record.priority === 'M'" color="orange">重要消息</a-tag>
<a-tag @click="showAnnouncement(record)" v-if="record.priority === 'H'" color="red">紧急消息</a-tag>
</div>
</a-list-item>
<div style="margin-top: 5px;text-align: center">
<a-button @click="toMyAnnouncement()" type="dashed" block>查看更多</a-button>
</div>
</a-list>
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<span @click="fetchNotice" class="header-notice">
<a-badge :count="msgTotal">
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
</a-badge>
</span>
<show-announcement ref="ShowAnnouncement" @ok="modalFormOk"></show-announcement>
</a-popover>
</template>
<script>
import { getAction,putAction } from '@/api/manage'
import ShowAnnouncement from './ShowAnnouncement'
export default {
name: "HeaderNotice",
components: {
ShowAnnouncement,
},
data () {
return {
loadding: false,
url:{
listCementByUser:"/sys/annountCement/listByUser",
editCementSend:"/system/sysAnnouncementSend/editByAnntIdAndUserId",
},
hovered: false,
announcement1:[],
announcement2:[],
msg1Count:"3",
msg2Count:"0",
msg1Title:"通知(3)",
msg2Title:"",
}
},
computed:{
msgTotal () {
return parseInt(this.msg1Count)+parseInt(this.msg2Count);
}
},
created() {
this.loadData();
this.timer();
},
methods: {
timer() {
return setInterval(()=>{
this.loadData()
},60000)
},
loadData (){
try {
// 获取系统消息
getAction(this.url.listCementByUser).then((res) => {
if (res.success) {
this.announcement1 = res.result.anntMsgList;
this.msg1Count = res.result.anntMsgTotal;
this.msg1Title = "通知(" + res.result.anntMsgTotal + ")";
this.announcement2 = res.result.sysMsgList;
this.msg2Count = res.result.sysMsgTotal;
this.msg2Title = "系统消息(" + res.result.sysMsgTotal + ")";
}
});
} catch (err) {
}
},
fetchNotice () {
if (this.loadding) {
this.loadding = false
return
}
this.loadding = true
setTimeout(() => {
this.loadding = false
}, 200)
},
showAnnouncement(record){
putAction(this.url.editCementSend,{anntId:record.id}).then((res)=>{
if(res.success){
this.loadData();
}
});
this.hovered = false;
this.$refs.ShowAnnouncement.detail(record);
},
toMyAnnouncement(){
this.$router.push({
path: '/isps/userAnnouncement',
name: 'isps-userAnnouncement'
});
},
modalFormOk(){
},
handleHoverChange (visible) {
this.hovered = visible;
},
}
}
</script>
<style lang="css">
.header-notice-wrapper {
top: 50px !important;
}
</style>
<style lang="scss" scoped>
.header-notice{
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div class="logo">
<router-link :to="{name:'dashboard'}">
<img src="~@/assets/logo.svg" alt="logo">
<h1 v-if="showTitle">{{ title }}</h1>
</router-link>
</div>
</template>
<script>
export default {
name: 'Logo',
props: {
title: {
type: String,
default: 'Jeecg-Boot Pro',
required: false
},
showTitle: {
type: Boolean,
default: true,
required: false
}
}
}
</script>
<style lang="scss" scoped>
/*缩小首页布 局顶部的高度*/
$height: 59px;
.sider {
box-shadow: none !important;
.logo {
height: $height !important;
line-height: $height !important;
box-shadow: none !important;
transition: background 300ms;
a {
color: white;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}
&.light .logo {
background-color: #1890ff;
}
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<a-modal
width="60%"
:visible="visible"
:bodyStyle ="bodyStyle"
@cancel="handleCancel"
destroyOnClose
:footer="null">
<a-card style="width: 100%;height: 100%" class="daily-article" :loading="loading">
<a-card-meta
:title="record.titile"
:description="'发布人'+record.sender + ' 发布时间 ' + record.sendTime"/>
<a-divider />
<span v-html="record.msgContent" class="article-content"></span>
</a-card>
</a-modal>
</template>
<script>
export default {
name: "SysAnnouncementModal",
components: {
},
data () {
return {
title:"通知消息",
record: {},
labelCol: {
xs: { span: 24 },
sm: { span: 5 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
visible: false,
loading: false,
bodyStyle:{
padding: "0",
height:(window.innerHeight*0.8)+"px",
"overflow-y":"auto"
},
}
},
created () {
},
methods: {
detail (record) {
this.visible = true;
this.record = record;
},
handleCancel () {
this.visible = false;
},
}
}
</script>
<style scoped lang="less">
.daily-article {
.article-button {
font-size: 1.2rem !important;
}
.ant-card-body {
padding: 18px !important;
}
.ant-card-head {
padding: 0 1rem;
}
.ant-card-meta {
margin-bottom: 1rem;
}
.article-content {
p {
word-wrap: break-word;
word-break: break-all;
text-overflow: initial;
white-space: normal;
font-size: .9rem !important;
margin-bottom: .8rem;
}
}
}
</style>

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