mirror of
https://gitee.com/lab1024/smart-admin.git
synced 2025-11-21 09:56:54 +08:00
v1.0.4
This commit is contained in:
67
smart-admin-web/src/components/active-plate/active-plate.vue
Normal file
67
smart-admin-web/src/components/active-plate/active-plate.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="active-plate-main">
|
||||
<ul class="active-list">
|
||||
<li class="item" v-for="item in infoList" :key="item.title">
|
||||
<p class="num" :style="{color:item.color}"><CountTo :end="item.count">{{item.count}}</CountTo></p>
|
||||
<p class="desc">{{item.title}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CountTo from '_c/count-to';
|
||||
export default {
|
||||
name: 'activePlate',
|
||||
components: {
|
||||
CountTo
|
||||
},
|
||||
props: {
|
||||
// 需要展示的数据集合
|
||||
infoList: {
|
||||
type: Array,
|
||||
require: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.active-plate-main {
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
.active-list {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding-top:15px;
|
||||
.item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
.num {
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.desc {
|
||||
font-size: 16px;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
top:18px;
|
||||
right: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 56px;
|
||||
background: #e7eef0;
|
||||
}
|
||||
&:nth-last-of-type(1) {
|
||||
&::after {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
smart-admin-web/src/components/charts/bar.vue
Normal file
73
smart-admin-web/src/components/charts/bar.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div ref="dom" class="charts chart-bar"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import echarts from 'echarts';
|
||||
import tdTheme from './theme.json';
|
||||
import { on, off } from '@/lib/util';
|
||||
echarts.registerTheme('tdTheme', tdTheme);
|
||||
export default {
|
||||
// 柱状图
|
||||
name: 'chartBar',
|
||||
props: {
|
||||
// map类型 keys 为图表x轴 values 为图表y轴数据
|
||||
value: {
|
||||
type: Object,
|
||||
require: true
|
||||
},
|
||||
// 标题
|
||||
text: {
|
||||
type: String,
|
||||
require: false,
|
||||
default: ''
|
||||
},
|
||||
// 子标题
|
||||
subtext: {
|
||||
type: String,
|
||||
require: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dom: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
resize () {
|
||||
this.dom.resize();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
let xAxisData = Object.keys(this.value);
|
||||
let seriesData = Object.values(this.value);
|
||||
let option = {
|
||||
title: {
|
||||
text: this.text,
|
||||
subtext: this.subtext,
|
||||
x: 'center'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: seriesData,
|
||||
type: 'bar'
|
||||
}]
|
||||
};
|
||||
this.dom = echarts.init(this.$refs.dom, 'tdTheme');
|
||||
this.dom.setOption(option);
|
||||
on(window, 'resize', this.resize);
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
off(window, 'resize', this.resize);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
3
smart-admin-web/src/components/charts/index.js
Normal file
3
smart-admin-web/src/components/charts/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ChartPie from './pie.vue';
|
||||
import ChartBar from './bar.vue';
|
||||
export { ChartPie, ChartBar };
|
||||
85
smart-admin-web/src/components/charts/pie.vue
Normal file
85
smart-admin-web/src/components/charts/pie.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div ref="dom" class="charts chart-pie"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import echarts from 'echarts';
|
||||
import tdTheme from './theme.json';
|
||||
import { on, off } from '@/lib/util';
|
||||
echarts.registerTheme('tdTheme', tdTheme);
|
||||
export default {
|
||||
// 饼图
|
||||
name: 'chartPie',
|
||||
props: {
|
||||
// 数据集合
|
||||
value: {
|
||||
type: Array,
|
||||
require: true
|
||||
},
|
||||
// 标题
|
||||
text: {
|
||||
type: String,
|
||||
require: true,
|
||||
default: ''
|
||||
},
|
||||
// 子标题
|
||||
subtext: {
|
||||
type: String,
|
||||
require: true,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dom: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
resize () {
|
||||
this.dom.resize();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
let legend = this.value.map(_ => _.name);
|
||||
let option = {
|
||||
title: {
|
||||
text: this.text,
|
||||
subtext: this.subtext,
|
||||
x: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: legend
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
center: ['50%', '60%'],
|
||||
data: this.value,
|
||||
itemStyle: {
|
||||
emphasis: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
this.dom = echarts.init(this.$refs.dom, 'tdTheme');
|
||||
this.dom.setOption(option);
|
||||
on(window, 'resize', this.resize);
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
off(window, 'resize', this.resize);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
490
smart-admin-web/src/components/charts/theme.json
Normal file
490
smart-admin-web/src/components/charts/theme.json
Normal file
@@ -0,0 +1,490 @@
|
||||
{
|
||||
"color": [
|
||||
"#2d8cf0",
|
||||
"#19be6b",
|
||||
"#ff9900",
|
||||
"#E46CBB",
|
||||
"#9A66E4",
|
||||
"#ed3f14"
|
||||
],
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"textStyle": {},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#516b91"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#93b7e3"
|
||||
}
|
||||
},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "2"
|
||||
}
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"radar": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "2"
|
||||
}
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pie": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scatter": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"boxplot": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parallel": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sankey": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"funnel": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"candlestick": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"color": "#edafda",
|
||||
"color0": "transparent",
|
||||
"borderColor": "#d680bc",
|
||||
"borderColor0": "#8fd3e8",
|
||||
"borderWidth": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": 1,
|
||||
"color": "#aaa"
|
||||
}
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true,
|
||||
"color": [
|
||||
"#2d8cf0",
|
||||
"#19be6b",
|
||||
"#f5ae4a",
|
||||
"#9189d5",
|
||||
"#56cae2",
|
||||
"#cbb0e3"
|
||||
],
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#eee"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"areaColor": "#f3f3f3",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"areaColor": "rgba(165,231,240,1)",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 1
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#000"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "rgb(81,107,145)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"geo": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"areaColor": "#f3f3f3",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"areaColor": "rgba(165,231,240,1)",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 1
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#000"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "rgb(81,107,145)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbox": {
|
||||
"iconStyle": {
|
||||
"normal": {
|
||||
"borderColor": "#999"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderColor": "#666"
|
||||
}
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#ccc",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#ccc",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#8fd3e8",
|
||||
"width": 1
|
||||
},
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"color": "#8fd3e8",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
},
|
||||
"controlStyle": {
|
||||
"normal": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "#8fd3e8",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "#8fd3e8",
|
||||
"borderWidth": 0.5
|
||||
}
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "rgba(138,124,168,0.37)"
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": [
|
||||
"#516b91",
|
||||
"#59c4e6",
|
||||
"#a5e7f0"
|
||||
]
|
||||
},
|
||||
"dataZoom": {
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"dataBackgroundColor": "rgba(255,255,255,0.3)",
|
||||
"fillerColor": "rgba(167,183,204,0.4)",
|
||||
"handleColor": "#a7b7cc",
|
||||
"handleSize": "100%",
|
||||
"textStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#eee"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#eee"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
smart-admin-web/src/components/common-icon/common-icon.vue
Normal file
52
smart-admin-web/src/components/common-icon/common-icon.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Icons :is="iconType" :type="iconName" :color="iconColor" :size="iconSize"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icons from '_c/icons';
|
||||
export default {
|
||||
name: 'CommonIcon',
|
||||
components: { Icons },
|
||||
props: {
|
||||
// 图标名 eg: icon-name
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
// 图标颜色
|
||||
color: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
// 图标尺寸
|
||||
size: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconType () {
|
||||
return this.type.indexOf('_') === 0 ? 'Icons' : 'Icon';
|
||||
},
|
||||
iconName () {
|
||||
return this.iconType === 'Icons' ? this.getCustomIconName(this.type) : this.type;
|
||||
},
|
||||
iconSize () {
|
||||
return this.size || (this.iconType === 'Icons' ? 12 : undefined);
|
||||
},
|
||||
iconColor () {
|
||||
return this.color || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 返回图标名
|
||||
getCustomIconName (iconName) {
|
||||
return iconName.slice(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
2
smart-admin-web/src/components/common-icon/index.js
Normal file
2
smart-admin-web/src/components/common-icon/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import CommonIcon from './common-icon.vue';
|
||||
export default CommonIcon;
|
||||
198
smart-admin-web/src/components/count-to/count-to.vue
Normal file
198
smart-admin-web/src/components/count-to/count-to.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="count-to-wrapper">
|
||||
<slot name="left"/>
|
||||
<p class="content-outer">
|
||||
<span :class="['count-to-count-text', countClass]" :id="counterId">{{ init }}</span>
|
||||
<i :class="['count-to-unit-text', unitClass]">{{ unitText }}</i>
|
||||
</p>
|
||||
<slot name="right"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CountUp from 'countup';
|
||||
import './index.less';
|
||||
export default {
|
||||
name: 'countTo',
|
||||
props: {
|
||||
/**
|
||||
* @description 默认值
|
||||
*/
|
||||
init: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* @description 起始值,即动画开始前显示的数值
|
||||
*/
|
||||
startVal: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* @description 结束值,即动画结束后显示的数值
|
||||
*/
|
||||
end: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* @description 保留几位小数
|
||||
*/
|
||||
decimals: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* @description 分隔整数和小数的符号,默认是小数点
|
||||
*/
|
||||
decimal: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '.'
|
||||
},
|
||||
/**
|
||||
* @description 动画持续的时间,单位是秒
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 2
|
||||
},
|
||||
/**
|
||||
* @description 动画延迟开始的时间,单位是秒
|
||||
*/
|
||||
delay: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* @description 是否禁用easing动画效果
|
||||
*/
|
||||
uneasing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 是否使用分组,分组后每三位会用一个符号分隔
|
||||
*/
|
||||
usegroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 用于分组(usegroup)的符号
|
||||
*/
|
||||
separator: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ','
|
||||
},
|
||||
/**
|
||||
* @description 是否简化显示,设为true后会使用unit单位来做相关省略
|
||||
*/
|
||||
simplify: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 自定义单位,如[3, 'K+'], [6, 'M+']即大于3位数小于6位数的用k+来做省略
|
||||
* 1000即显示为1K+
|
||||
*/
|
||||
unit: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default () {
|
||||
return [[3, 'K+'], [6, 'M+'], [9, 'B+']];
|
||||
}
|
||||
},
|
||||
// 数值样式
|
||||
countClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
// 单位样式
|
||||
unitClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// CountUp 实例
|
||||
counter: null,
|
||||
// 单位描述
|
||||
unitText: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 唯一标识
|
||||
counterId () {
|
||||
return `count_to_${this._uid}`;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
end (newVal) {
|
||||
let endVal = this.getValue(newVal);
|
||||
this.counter.update(endVal);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
let endVal = this.getValue(this.end);
|
||||
this.counter = new CountUp(this.counterId, this.startVal, endVal, this.decimals, this.duration, {
|
||||
useEasing: !this.uneasing,
|
||||
useGrouping: this.useGroup,
|
||||
separator: this.separator,
|
||||
decimal: this.decimal
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (!this.counter.error) this.counter.start();
|
||||
}, this.delay);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getHandleVal (val, len) {
|
||||
return {
|
||||
endVal: parseInt(val / Math.pow(10, this.unit[len - 1][0])),
|
||||
unitText: this.unit[len - 1][1]
|
||||
};
|
||||
},
|
||||
transformValue (val) {
|
||||
let len = this.unit.length;
|
||||
let res = {
|
||||
endVal: 0,
|
||||
unitText: ''
|
||||
};
|
||||
if (val < Math.pow(10, this.unit[0][0])) res.endVal = val;
|
||||
else {
|
||||
for (let i = 1; i < len; i++) {
|
||||
if (val >= Math.pow(10, this.unit[i - 1][0]) && val < Math.pow(10, this.unit[i][0])) res = this.getHandleVal(val, i);
|
||||
}
|
||||
}
|
||||
if (val > Math.pow(10, this.unit[len - 1][0])) res = this.getHandleVal(val, len);
|
||||
return res;
|
||||
},
|
||||
getValue (val) {
|
||||
let res = 0;
|
||||
if (this.simplify) {
|
||||
let { endVal, unitText } = this.transformValue(val);
|
||||
this.unitText = unitText;
|
||||
res = endVal;
|
||||
} else {
|
||||
res = val;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
2
smart-admin-web/src/components/count-to/index.js
Normal file
2
smart-admin-web/src/components/count-to/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import countTo from './count-to.vue';
|
||||
export default countTo;
|
||||
10
smart-admin-web/src/components/count-to/index.less
Normal file
10
smart-admin-web/src/components/count-to/index.less
Normal file
@@ -0,0 +1,10 @@
|
||||
@prefix: ~"count-to";
|
||||
|
||||
.@{prefix}-wrapper{
|
||||
.content-outer{
|
||||
display: inline-block;
|
||||
.@{prefix}-unit-text{
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
smart-admin-web/src/components/editor/editor.vue
Normal file
77
smart-admin-web/src/components/editor/editor.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="editor-wrapper">
|
||||
<div :id="editorId"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Editor from 'wangeditor';
|
||||
import 'wangeditor/release/wangEditor.min.css';
|
||||
import { oneOf } from '@/lib/util';
|
||||
export default {
|
||||
name: 'Editor',
|
||||
props: {
|
||||
// 缓存Html内容
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 绑定的值的类型, enum: ['html', 'text']
|
||||
*/
|
||||
valueType: {
|
||||
type: String,
|
||||
default: 'html',
|
||||
validator: (val) => {
|
||||
return oneOf(val, ['html', 'text']);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 设置change事件触发时间间隔
|
||||
*/
|
||||
changeInterval: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
/**
|
||||
* @description 是否开启本地存储
|
||||
*/
|
||||
cache: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 唯一id
|
||||
editorId () {
|
||||
return `editor${this._uid}`;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.editor = new Editor(`#${this.editorId}`);
|
||||
this.editor.customConfig.onchange = (html) => {
|
||||
let text = this.editor.txt.text();
|
||||
if (this.cache) localStorage.editorCache = html;
|
||||
this.$emit('input', this.valueType === 'html' ? html : text);
|
||||
this.$emit('on-change', html, text);
|
||||
};
|
||||
this.editor.customConfig.onchangeTimeout = this.changeInterval;
|
||||
// create这个方法一定要在所有配置项之后调用
|
||||
this.editor.create();
|
||||
// 如果本地有存储加载本地存储内容
|
||||
let html = this.value || localStorage.editorCache;
|
||||
if (html) this.editor.txt.html(html);
|
||||
},
|
||||
methods: {
|
||||
setHtml (val) {
|
||||
this.editor.txt.html(val);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.editor-wrapper * {
|
||||
z-index: 100 !important;
|
||||
}
|
||||
</style>
|
||||
2
smart-admin-web/src/components/editor/index.js
Normal file
2
smart-admin-web/src/components/editor/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Editor from './editor.vue';
|
||||
export default Editor;
|
||||
38
smart-admin-web/src/components/icons/icons.vue
Normal file
38
smart-admin-web/src/components/icons/icons.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<i :class="`iconfont icon-${type}`" :style="styles"></i>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Icons',
|
||||
props: {
|
||||
// 图标名
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
// 图标颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#5c6b77'
|
||||
},
|
||||
// 图标尺寸
|
||||
size: {
|
||||
type: Number,
|
||||
default: 16
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles () {
|
||||
return {
|
||||
fontSize: `${this.size}px`,
|
||||
color: this.color
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
2
smart-admin-web/src/components/icons/index.js
Normal file
2
smart-admin-web/src/components/icons/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Icons from './icons.vue';
|
||||
export default Icons;
|
||||
@@ -0,0 +1,2 @@
|
||||
import ABackTop from './index.vue';
|
||||
export default ABackTop;
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div :class="classes" :style="styles" @click="back">
|
||||
<slot>
|
||||
<div :class="innerClasses">
|
||||
<i class="ivu-icon ivu-icon-ios-arrow-up"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { on, off } from '@/lib/util';
|
||||
const prefixCls = 'ivu-back-top';
|
||||
|
||||
export default {
|
||||
name: 'ABackTop',
|
||||
props: {
|
||||
// 允许出现返回头部高度
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
// 返回头部按钮 距离底部距离
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
// 返回头部按钮 距离窗口右侧距离
|
||||
right: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
// 滚动速度
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
// 可视区
|
||||
container: {
|
||||
type: null,
|
||||
default: window
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// 是否显示返回顶部按钮
|
||||
backTop: false
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
// window.addEventListener('scroll', this.handleScroll, false)
|
||||
// window.addEventListener('resize', this.handleScroll, false)
|
||||
on(this.containerEle, 'scroll', this.handleScroll);
|
||||
on(this.containerEle, 'resize', this.handleScroll);
|
||||
},
|
||||
beforeDestroy () {
|
||||
// window.removeEventListener('scroll', this.handleScroll, false)
|
||||
// window.removeEventListener('resize', this.handleScroll, false)
|
||||
off(this.containerEle, 'scroll', this.handleScroll);
|
||||
off(this.containerEle, 'resize', this.handleScroll);
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-show`]: this.backTop
|
||||
}
|
||||
];
|
||||
},
|
||||
styles () {
|
||||
return {
|
||||
bottom: `${this.bottom}px`,
|
||||
right: `${this.right}px`
|
||||
};
|
||||
},
|
||||
innerClasses () {
|
||||
return `${prefixCls}-inner`;
|
||||
},
|
||||
containerEle () {
|
||||
return this.container === window ? window : document.querySelector(this.container);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleScroll () {
|
||||
this.backTop = this.containerEle.scrollTop >= this.height;
|
||||
},
|
||||
back () {
|
||||
let target = typeof this.container === 'string' ? this.containerEle : (document.documentElement || document.body);
|
||||
const sTop = target.scrollTop;
|
||||
this.scrollTop(this.containerEle, sTop, 0, this.duration);
|
||||
this.$emit('on-click');
|
||||
},
|
||||
// scrollTop animation
|
||||
scrollTop (el, from = 0, to, duration = 500, endCallback) {
|
||||
if (!window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame = (
|
||||
window.webkitRequestAnimationFrame ||
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.msRequestAnimationFrame ||
|
||||
function (callback) {
|
||||
return window.setTimeout(callback, 1000 / 60);
|
||||
}
|
||||
);
|
||||
}
|
||||
const difference = Math.abs(from - to);
|
||||
const step = Math.ceil(difference / duration * 50);
|
||||
|
||||
const scroll = (start, end, step) => {
|
||||
if (start === end) {
|
||||
endCallback && endCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
let d = (start + step > end) ? end : start + step;
|
||||
if (start > end) {
|
||||
d = (start - step < end) ? end : start - step;
|
||||
}
|
||||
|
||||
if (el === window) {
|
||||
window.scrollTo(d, d);
|
||||
} else {
|
||||
el.scrollTop = d;
|
||||
}
|
||||
window.requestAnimationFrame(() => scroll(d, end, step));
|
||||
};
|
||||
scroll(from, to, step);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div v-if="showFullScreenBtn" class="full-screen-btn-con">
|
||||
<Tooltip :content="value ? '退出全屏' : '全屏'" placement="bottom">
|
||||
<Icon @click.native="handleChange" :type="value ? 'md-contract' : 'md-expand'" :size="16"></Icon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Fullscreen',
|
||||
props: {
|
||||
// 当前全屏状态
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showFullScreenBtn () {
|
||||
return window.navigator.userAgent.indexOf('MSIE') < 0;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
let isFullscreen = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
|
||||
isFullscreen = !!isFullscreen;
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.$emit('input', !this.value);
|
||||
this.$emit('on-change', !this.value);
|
||||
});
|
||||
document.addEventListener('mozfullscreenchange', () => {
|
||||
this.$emit('input', !this.value);
|
||||
this.$emit('on-change', !this.value);
|
||||
});
|
||||
document.addEventListener('webkitfullscreenchange', () => {
|
||||
this.$emit('input', !this.value);
|
||||
this.$emit('on-change', !this.value);
|
||||
});
|
||||
document.addEventListener('msfullscreenchange', () => {
|
||||
this.$emit('input', !this.value);
|
||||
this.$emit('on-change', !this.value);
|
||||
});
|
||||
this.$emit('input', isFullscreen);
|
||||
},
|
||||
methods: {
|
||||
handleFullscreen () {
|
||||
let main = document.body;
|
||||
if (this.value) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitCancelFullScreen) {
|
||||
document.webkitCancelFullScreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (main.requestFullscreen) {
|
||||
main.requestFullscreen();
|
||||
} else if (main.mozRequestFullScreen) {
|
||||
main.mozRequestFullScreen();
|
||||
} else if (main.webkitRequestFullScreen) {
|
||||
main.webkitRequestFullScreen();
|
||||
} else if (main.msRequestFullscreen) {
|
||||
main.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
},
|
||||
handleChange () {
|
||||
this.handleFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.full-screen-btn-con .ivu-tooltip-rel {
|
||||
height: 52px;
|
||||
line-height: 52px;
|
||||
padding:0 12px;
|
||||
&:hover {
|
||||
background: #f7f7f8;
|
||||
cursor: pointer;
|
||||
}
|
||||
cursor: pointer;
|
||||
i {
|
||||
cursor: pointer;
|
||||
color:#909ca4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Fullscreen from './fullscreen.vue';
|
||||
export default Fullscreen;
|
||||
@@ -0,0 +1,4 @@
|
||||
.custom-bread-crumb{
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="custom-bread-crumb">
|
||||
<Breadcrumb :style="{fontSize: `${fontSize}px`}">
|
||||
<BreadcrumbItem v-for="item in list" :to="item.to" :key="`bread-crumb-${item.name}`">
|
||||
{{ showTitle(item) }}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { showTitle } from '@/lib/menu-func';
|
||||
import './custom-bread-crumb.less';
|
||||
export default {
|
||||
name: 'CustomBreadCrumb',
|
||||
props: {
|
||||
// 面包屑集合
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 面包屑显示字体大小
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showTitle (item) {
|
||||
return showTitle(item, this);
|
||||
},
|
||||
isCustomIcon (iconName) {
|
||||
return iconName.indexOf('_') === 0;
|
||||
},
|
||||
getCustomIconName (iconName) {
|
||||
return iconName.slice(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import customBreadCrumb from './custom-bread-crumb.vue';
|
||||
export default customBreadCrumb;
|
||||
@@ -0,0 +1,14 @@
|
||||
.header-bar{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.custom-content-con{
|
||||
float: right;
|
||||
height: auto;
|
||||
padding-right: 20px;
|
||||
// line-height: 64px;
|
||||
& > *{
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="header-bar">
|
||||
<SiderTrigger :collapsed="collapsed" icon="icon iconfont icondaohangzhedie" @on-change="handleCollpasedChange"/>
|
||||
<CustomBreadCrumb show-icon style="margin-left: 30px;" :list="breadCrumbList"/>
|
||||
<div class="custom-content-con">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SiderTrigger from './sider-trigger';
|
||||
import CustomBreadCrumb from './custom-bread-crumb';
|
||||
import './header-bar.less';
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
components: {
|
||||
SiderTrigger,
|
||||
CustomBreadCrumb
|
||||
},
|
||||
props: {
|
||||
// 折叠状态
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
require: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 面包屑集合
|
||||
breadCrumbList () {
|
||||
return this.$store.state.app.breadCrumbList;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCollpasedChange (state) {
|
||||
this.$emit('on-coll-change', state);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import HeaderBar from './header-bar';
|
||||
export default HeaderBar;
|
||||
@@ -0,0 +1,2 @@
|
||||
import siderTrigger from './sider-trigger.vue';
|
||||
export default siderTrigger;
|
||||
@@ -0,0 +1,21 @@
|
||||
.trans{
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
@size: 40px;
|
||||
.sider-trigger-a{
|
||||
padding: 6px;
|
||||
width: @size;
|
||||
height: @size;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
color: #5c6b77;
|
||||
margin-top: 10px;
|
||||
i{
|
||||
.trans;
|
||||
vertical-align: top;
|
||||
}
|
||||
&.collapsed i{
|
||||
transform: rotateZ(180deg);
|
||||
.trans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<a @click="handleChange" type="text" :class="['sider-trigger-a', collapsed ? 'collapsed' : '']">
|
||||
<Icon :type="icon" :size="size"/>
|
||||
</a>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'SiderTrigger',
|
||||
props: {
|
||||
// 折叠状态
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
require: false
|
||||
},
|
||||
// 图标名称
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'navicon-round'
|
||||
},
|
||||
// 图标尺寸
|
||||
size: {
|
||||
type: Number,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange () {
|
||||
this.$emit('on-change', !this.collapsed);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import "./sider-trigger.less";
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Language from './language.vue';
|
||||
export default Language;
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dropdown trigger="click" @on-click="selectLang">
|
||||
<a href="javascript:void(0)">
|
||||
{{ title }}
|
||||
<Icon :size="18" type="md-arrow-dropdown"/>
|
||||
</a>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem v-for="(value, key) in localList" :name="key" :key="`lang-${key}`">{{ value }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Language',
|
||||
props: {
|
||||
lang: {
|
||||
type: String,
|
||||
require: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
langList: {
|
||||
'zh-CN': '语言',
|
||||
'zh-TW': '語言',
|
||||
'en-US': 'Lang'
|
||||
},
|
||||
localList: {
|
||||
'zh-CN': '中文简体',
|
||||
'zh-TW': '中文繁体',
|
||||
'en-US': 'English'
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
lang (lang) {
|
||||
this.$i18n.locale = lang;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title () {
|
||||
return this.langList[this.lang];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectLang (name) {
|
||||
this.$emit('on-lang-change', name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
371
smart-admin-web/src/components/main/components/notice/notice.vue
Normal file
371
smart-admin-web/src/components/main/components/notice/notice.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="notice">
|
||||
<Tooltip content="消息" placement="bottom">
|
||||
<Badge :count="noticeNumber" class-name="demo-badge-alone">
|
||||
<Icon @click="openNotice" class="demo-badge" size="18" type="icon iconfont iconnews"></Icon>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<div :class="modalOpen?'show':''" class="notice-main">
|
||||
<div :class="modalOpen?'show':''" @click="modalOpen=false" class="notice-bg"></div>
|
||||
<div :class="modalOpen?'show':''" class="notice-box">
|
||||
<div class="header">
|
||||
<div class="item">消息通知({{noticeNumber}})</div>
|
||||
<Icon @click="modalClose" class="close" size="24" type="ios-close" />
|
||||
</div>
|
||||
|
||||
<div class="notice-list" ref="noticeList">
|
||||
<div :key="item.id" @click.stop="getDetail(item)" class="item" v-for="item in noticeList">
|
||||
<img alt src="@/assets/images/message.png" />
|
||||
<div class="info">
|
||||
<p class="title">{{item.title}}</p>
|
||||
<p class="time">{{item.updateTime}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteLoading @infinite="scroll" ref="infiniteLoading">
|
||||
<span slot="no-more">暂时没有新消息啦!</span>
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="detailModalOpen?'show':''" class="notice-detail">
|
||||
<div class="title">
|
||||
<div class="item">{{noticeDetail.title}}</div>
|
||||
<Icon @click="detailModalOpen=false" class="close" type="md-close" />
|
||||
</div>
|
||||
<div class="detail">{{noticeDetail.content}}</div>
|
||||
<p class="time">{{noticeDetail.updateTime}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { noticeApi } from '@/api/notice';
|
||||
import { socketBaseUrl } from '@/lib/http';
|
||||
import InfiniteLoading from 'vue-infinite-loading';
|
||||
export default {
|
||||
name: 'Notice',
|
||||
components: {
|
||||
InfiniteLoading
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
// Websocket心跳间隔
|
||||
heartTimer: null,
|
||||
// 是否展示消息列表
|
||||
modalOpen: false,
|
||||
socketBaseUrl,
|
||||
// websocket 实例
|
||||
socket: null,
|
||||
// 是否展示消息详情
|
||||
detailModalOpen: false,
|
||||
searchData: {
|
||||
pageSize: 10,
|
||||
pageNum: 1
|
||||
},
|
||||
noticeDetail: {
|
||||
title: '',
|
||||
content: '',
|
||||
updateTime: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initWebSocket();
|
||||
},
|
||||
computed: {
|
||||
// 消息集合
|
||||
noticeList() {
|
||||
return this.$store.state.notice.noticeList;
|
||||
},
|
||||
// 消息数量
|
||||
noticeNumber() {
|
||||
return this.$store.state.notice.noticeNumber;
|
||||
},
|
||||
userInfo() {
|
||||
return this.$store.state.user.userLoginInfo;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initWebSocket() {
|
||||
this.socket = new WebSocket(
|
||||
`${this.socketBaseUrl}webSocket/${this.userInfo.id}`
|
||||
);
|
||||
this.socket.onopen = this.websocketOnopen;
|
||||
this.socket.onerror = this.websocketOnerror;
|
||||
this.socket.onmessage = this.websocketOnmessage;
|
||||
this.socket.onclose = this.websocketClose;
|
||||
},
|
||||
// socket连接成功
|
||||
websocketOnopen() {
|
||||
this.heartTimer = setInterval(() => {
|
||||
this.heartbeat();
|
||||
}, 5000);
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
clearInterval(this.heartTimer);
|
||||
this.$store.commit('restNotice');
|
||||
});
|
||||
},
|
||||
// socket连接出错
|
||||
websocketOnerror() {
|
||||
console.log('socket出错啦');
|
||||
},
|
||||
// socket收到消息
|
||||
websocketOnmessage(e) {
|
||||
this.$store.commit('updateNoticeNum', Number(e.data));
|
||||
},
|
||||
// socket关闭
|
||||
websocketClose() {
|
||||
console.log('socket掉线啦');
|
||||
},
|
||||
// 心跳包
|
||||
heartbeat() {
|
||||
let data = {
|
||||
jsonStr: '',
|
||||
messageType: 3
|
||||
};
|
||||
let subStr = {
|
||||
employeeId: this.userInfo.id
|
||||
};
|
||||
data.jsonStr = JSON.stringify(subStr);
|
||||
this.socket.send(JSON.stringify(data));
|
||||
},
|
||||
async getDetail(item) {
|
||||
try {
|
||||
let result = await noticeApi.getNoticeDetail(item.id);
|
||||
this.noticeDetail = result.data;
|
||||
} catch (error) {
|
||||
//TODO zhuoda sentry
|
||||
console.error(e);
|
||||
}
|
||||
if (this.detailModalOpen) {
|
||||
this.detailModalOpen = false;
|
||||
setTimeout(() => {
|
||||
this.detailModalOpen = true;
|
||||
}, 100);
|
||||
} else {
|
||||
this.detailModalOpen = true;
|
||||
}
|
||||
|
||||
try {
|
||||
let result = await noticeApi.addNoticeRecord(item.id);
|
||||
} catch (error) {
|
||||
//TODO zhuoda sentry
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
scroll($state) {
|
||||
this.getNoticeList($state);
|
||||
},
|
||||
async getNoticeList($state) {
|
||||
const result = await noticeApi.getNoticeUnreadList({
|
||||
...this.searchData
|
||||
});
|
||||
// 请求通知成功,全局更改消息数量
|
||||
this.$store.commit('updateNoticeNum', result.data.total);
|
||||
$state && $state.loaded();
|
||||
if (!result.data.list.length) {
|
||||
$state && $state.complete();
|
||||
} else {
|
||||
this.searchData.pageNum++;
|
||||
}
|
||||
this.$store.commit('updateNotice', result.data.list);
|
||||
},
|
||||
openNotice() {
|
||||
this.searchData.pageNum = 1;
|
||||
this.$store.commit('restNotice');
|
||||
this.getNoticeList();
|
||||
this.modalOpen = true;
|
||||
},
|
||||
modalClose() {
|
||||
this.getNoticeList();
|
||||
this.detailModalOpen = false;
|
||||
this.modalOpen = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang='less'>
|
||||
.notice {
|
||||
&:hover {
|
||||
background: #f7f7f8;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.demo-badge {
|
||||
margin: 0 10px;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
color: #909ca4;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.demo-badge-alone {
|
||||
background: #f47f92 !important;
|
||||
top: 5px !important;
|
||||
right: 15px !important;
|
||||
}
|
||||
.notice-main {
|
||||
position: fixed;
|
||||
z-index: -100;
|
||||
opacity: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
transition: all 0.5s;
|
||||
top: 0;
|
||||
left: 0;
|
||||
&.show {
|
||||
z-index: 993;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.notice-bg {
|
||||
position: fixed;
|
||||
z-index: -100;
|
||||
opacity: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
transition: all 0.5s;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
&.show {
|
||||
z-index: 994;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.notice-box {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: -650px;
|
||||
background: #fff;
|
||||
z-index: 997;
|
||||
width: 336px;
|
||||
height: 650px;
|
||||
transition: all 0.5s;
|
||||
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.1) !important;
|
||||
border: none !important;
|
||||
overflow: auto;
|
||||
&.show {
|
||||
bottom: 0;
|
||||
}
|
||||
.header {
|
||||
position: relative;
|
||||
height: 50px;
|
||||
border-bottom: 1px solid #eee;
|
||||
.close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
top: 15px;
|
||||
}
|
||||
.item {
|
||||
margin: 0 20px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
// border-bottom: 1px #e8e8e8 solid;
|
||||
}
|
||||
}
|
||||
.notice-list {
|
||||
height: calc(~'100% - 50px') !important;
|
||||
padding-top: 10px;
|
||||
overflow: auto;
|
||||
.no-data {
|
||||
text-align: center;
|
||||
}
|
||||
.item {
|
||||
position: relative;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
padding: 0 20px;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px #e8e8e8 solid;
|
||||
&:hover {
|
||||
background: #f4f4f4;
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: #5cb85c;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.info {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
align-content: flex-start;
|
||||
justify-content: center;
|
||||
.title {
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.time {
|
||||
line-height: 1.5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.notice-detail {
|
||||
position: fixed;
|
||||
z-index: 995;
|
||||
border-right: 1px #e8e8e8 solid;
|
||||
bottom: 0;
|
||||
right: -336px;
|
||||
width: 400px;
|
||||
height: 650px;
|
||||
background: #fff;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
&.show {
|
||||
right: 336px;
|
||||
opacity: 1;
|
||||
}
|
||||
.title {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #eee;
|
||||
.close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
top: 12px;
|
||||
}
|
||||
.item {
|
||||
margin: 0 20px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #009688;
|
||||
// border-bottom: 1px #e8e8e8 solid;
|
||||
}
|
||||
}
|
||||
.detail {
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.time {
|
||||
padding: 0 20px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<Dropdown ref="dropdown"
|
||||
@on-click="handleClick"
|
||||
:class="hideTitle ? '' : 'collased-menu-dropdown'"
|
||||
:transfer="hideTitle"
|
||||
:placement="placement"
|
||||
transfer-class-name="menu-dropdown">
|
||||
<a class="drop-menu-a"
|
||||
type="text"
|
||||
@mouseover="handleMousemove($event, children)"
|
||||
:style="{textAlign: !hideTitle ? 'left' : ''}">
|
||||
<CommonIcon :size="rootIconSize"
|
||||
:color="textColor"
|
||||
:type="parentItem.icon" />
|
||||
<span class="menu-title"
|
||||
v-if="!hideTitle">{{ showTitle(parentItem) }}</span>
|
||||
<Icon style="float: right;"
|
||||
v-if="!hideTitle"
|
||||
type="ios-arrow-forward"
|
||||
:size="16" />
|
||||
</a>
|
||||
<DropdownMenu ref="dropdown"
|
||||
slot="list">
|
||||
<template v-for="child in children">
|
||||
<CollapsedMenu v-if="showChildren(child)"
|
||||
:icon-size="iconSize"
|
||||
:parent-item="child"
|
||||
:key="`drop-${child.name}`"/>
|
||||
<DropdownItem v-else
|
||||
:key="`drop-${child.name}`"
|
||||
:name="child.name">
|
||||
<CommonIcon :size="iconSize"
|
||||
:type="child.icon" />
|
||||
<span class="menu-title">{{ showTitle(child) }}</span>
|
||||
</DropdownItem>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from './mixin';
|
||||
import itemMixin from './item-mixin';
|
||||
import { findNodeUpperByClasses } from '@/lib/menu-func';
|
||||
|
||||
export default {
|
||||
name: 'CollapsedMenu',
|
||||
mixins: [ mixin, itemMixin ],
|
||||
props: {
|
||||
// 是否隐藏标题
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 图标尺寸
|
||||
rootIconSize: {
|
||||
type: Number,
|
||||
default: 16
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// 下拉菜单出现的位置,
|
||||
// 可选值为top,top-start,top-end,bottom,bottom-start,bottom-end,left,left-start,left-end,
|
||||
// right,right-start,right-end, 2.12.0 版本开始支持自动识别
|
||||
placement: 'right-end'
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
let dropdown = findNodeUpperByClasses(this.$refs.dropdown.$el, ['ivu-select-dropdown', 'ivu-dropdown-transfer']);
|
||||
if (dropdown) dropdown.style.overflow = 'visible';
|
||||
},
|
||||
methods: {
|
||||
handleClick (name) {
|
||||
this.$emit('on-click', name);
|
||||
},
|
||||
handleMousemove (event, children) {
|
||||
const { pageY } = event;
|
||||
const height = children.length * 38;
|
||||
const isOverflow = pageY + height < window.innerHeight;
|
||||
this.placement = isOverflow ? 'right-start' : 'right-end';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import SideMenu from './side-menu.vue';
|
||||
export default SideMenu;
|
||||
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
props: {
|
||||
// 父文件
|
||||
parentItem: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
},
|
||||
// 主题
|
||||
theme: {
|
||||
type: String,
|
||||
require: false
|
||||
},
|
||||
// 图标尺寸
|
||||
iconSize: {
|
||||
type: Number,
|
||||
require: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
parentName () {
|
||||
return this.parentItem.name;
|
||||
},
|
||||
children () {
|
||||
return this.parentItem.children;
|
||||
},
|
||||
textColor () {
|
||||
return this.theme === 'dark' ? '#fff' : '#495060';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import CommonIcon from '_c/common-icon';
|
||||
import { showTitle } from '@/lib/menu-func';
|
||||
export default {
|
||||
components: {
|
||||
CommonIcon
|
||||
},
|
||||
methods: {
|
||||
showTitle (item) {
|
||||
return showTitle(item, this);
|
||||
},
|
||||
showChildren (item) {
|
||||
return item.children && (item.children.length > 1 || (item.meta && item.meta.showAlways));
|
||||
},
|
||||
getNameOrHref (item, children0) {
|
||||
return item.href ? `isTurnByHref_${item.href}` : (children0 ? item.children[0].name : item.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<Submenu :name="`${parentName}`">
|
||||
<template slot="title">
|
||||
<CommonIcon :type="parentItem.icon || ''" />
|
||||
<span>{{ showTitle(parentItem) }}</span>
|
||||
</template>
|
||||
<template v-for="item in children">
|
||||
<template v-if="item.children && item.children.length > 0">
|
||||
<side-menu-item :key="`menu-${item.name}`" :parent-item="item" v-if="showChildren(item)"></side-menu-item>
|
||||
<MenuItem :key="`menu-${item.children[0].name}`" :name="getNameOrHref(item, true)" v-else>
|
||||
<CommonIcon
|
||||
:key="`menu-${item.children[0].name}-common-icon`"
|
||||
:type="item.children[0].icon || ''"
|
||||
/>
|
||||
<span :key="`menu-${item.children[0].name}-span`">{{ showTitle(item.children[0]) }}</span>
|
||||
</MenuItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<side-menu-item :key="`menu-${item.name}`" :parent-item="item" v-if="showChildren(item)"></side-menu-item>
|
||||
<MenuItem :key="`menu-${item.name}`" :name="getNameOrHref(item)" v-else>
|
||||
<CommonIcon :key="`menu-${item.name}-common-icon`" :type="item.icon || ''" />
|
||||
<span :key="`menu-${item.name}-span`">{{ showTitle(item) }}</span>
|
||||
</MenuItem>
|
||||
</template>
|
||||
</template>
|
||||
</Submenu>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from './mixin';
|
||||
import itemMixin from './item-mixin';
|
||||
export default {
|
||||
name: 'SideMenuItem',
|
||||
mixins: [mixin, itemMixin]
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
.side-menu-wrapper{
|
||||
user-select: none;
|
||||
.menu-collapsed{
|
||||
padding-top: 10px;
|
||||
|
||||
.ivu-dropdown{
|
||||
.ivu-dropdown-rel a{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.ivu-tooltip{
|
||||
width: 100%;
|
||||
.ivu-tooltip-rel{
|
||||
width: 100%;
|
||||
}
|
||||
.ivu-tooltip-popper .ivu-tooltip-content{
|
||||
.ivu-tooltip-arrow{
|
||||
border-right-color: #fff;
|
||||
}
|
||||
.ivu-tooltip-inner{
|
||||
background: #fff;
|
||||
color: #495060;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
a.drop-menu-a{
|
||||
display: inline-block;
|
||||
padding: 6px 15px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #495060;
|
||||
}
|
||||
}
|
||||
.menu-title{
|
||||
padding-left: 6px;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="side-menu-wrapper">
|
||||
<slot></slot>
|
||||
<Menu
|
||||
:accordion="accordion"
|
||||
:active-name="activeName"
|
||||
:open-names="openedNames"
|
||||
:theme="theme"
|
||||
@on-select="handleSelect"
|
||||
ref="menu"
|
||||
v-show="!collapsed"
|
||||
width="auto"
|
||||
>
|
||||
<template v-for="item in menuList">
|
||||
<template v-if="item.children && item.children.length === 1">
|
||||
<side-menu-item :key="`menu-${item.name}`" :parent-item="item" v-if="showChildren(item)"></side-menu-item>
|
||||
<menu-item
|
||||
:key="`menu-${item.children[0].name}`"
|
||||
:name="getNameOrHref(item, true)"
|
||||
v-else
|
||||
>
|
||||
<common-icon :type="item.children[0].icon || ''" />
|
||||
<span>{{ showTitle(item.children[0]) }}</span>
|
||||
</menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<side-menu-item :key="`menu-${item.name}`" :parent-item="item" v-if="showChildren(item)"></side-menu-item>
|
||||
<menu-item :key="`menu-${item.name}`" :name="getNameOrHref(item)" v-else>
|
||||
<common-icon :type="item.icon || ''" />
|
||||
<span>{{ showTitle(item) }}</span>
|
||||
</menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</Menu>
|
||||
<div :list="menuList" class="menu-collapsed" v-show="collapsed">
|
||||
<template v-for="item in menuList">
|
||||
<CollapsedMenu
|
||||
:icon-size="iconSize"
|
||||
:key="`drop-menu-${item.name}`"
|
||||
:parent-item="item"
|
||||
:root-icon-size="rootIconSize"
|
||||
:theme="theme"
|
||||
@on-click="handleSelect"
|
||||
hide-title
|
||||
v-if="item.children && item.children.length > 1"
|
||||
></CollapsedMenu>
|
||||
<Tooltip
|
||||
:content="showTitle(item.children && item.children[0] ? item.children[0] : item)"
|
||||
:key="`drop-menu-${item.name}`"
|
||||
placement="right"
|
||||
transfer
|
||||
v-else
|
||||
>
|
||||
<a
|
||||
:style="{textAlign: 'center'}"
|
||||
@click="handleSelect(getNameOrHref(item, true))"
|
||||
class="drop-menu-a"
|
||||
>
|
||||
<CommonIcon
|
||||
:color="textColor"
|
||||
:size="rootIconSize"
|
||||
:type="item.icon || (item.children && item.children[0].icon)"
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SideMenuItem from './side-menu-item.vue';
|
||||
import CollapsedMenu from './collapsed-menu.vue';
|
||||
import { getUnion } from '@/lib/util';
|
||||
import mixin from './mixin';
|
||||
|
||||
export default {
|
||||
name: 'SideMenu',
|
||||
mixins: [mixin],
|
||||
components: {
|
||||
SideMenuItem,
|
||||
CollapsedMenu
|
||||
},
|
||||
props: {
|
||||
// 菜单集合
|
||||
menuList: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 显示状态 如果是折叠状态就不显示
|
||||
collapsed: {
|
||||
type: Boolean
|
||||
},
|
||||
// 主题
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'dark'
|
||||
},
|
||||
// 父图标尺寸
|
||||
rootIconSize: {
|
||||
type: Number,
|
||||
default: 16
|
||||
},
|
||||
// 图标尺寸
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 16
|
||||
},
|
||||
// 是否开启手风琴模式,开启后每次至多展开一个子菜单
|
||||
accordion: {
|
||||
type: Boolean,
|
||||
require: false
|
||||
},
|
||||
// 激活菜单的 name 值
|
||||
activeName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 展开的 Submenu 的 name 集合
|
||||
openNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openedNames: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return this.theme === 'dark' ? '#fff' : '#495060';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeName(name) {
|
||||
if (this.accordion) {
|
||||
this.openedNames = this.getOpenedNamesByActiveName(name);
|
||||
} else {
|
||||
this.openedNames = getUnion(
|
||||
this.openedNames,
|
||||
this.getOpenedNamesByActiveName(name)
|
||||
);
|
||||
}
|
||||
},
|
||||
openNames(newNames) {
|
||||
this.openedNames = newNames;
|
||||
},
|
||||
openedNames() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu.updateOpened();
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openedNames = getUnion(
|
||||
this.openedNames,
|
||||
this.getOpenedNamesByActiveName(name)
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
updateActiveName(name){
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu.updateOpened();
|
||||
this.$refs.menu.updateActiveName(name);
|
||||
});
|
||||
},
|
||||
handleSelect(name) {
|
||||
this.$emit('on-select', name);
|
||||
},
|
||||
// 从激活菜单的名称中获取打开的菜单
|
||||
getOpenedNamesByActiveName(name) {
|
||||
return this.$route.matched
|
||||
.map(item => item.name)
|
||||
.filter(item => item !== name);
|
||||
},
|
||||
updateOpenName(name) {
|
||||
if (name === this.$config.homeName) this.openedNames = [];
|
||||
else this.openedNames = this.getOpenedNamesByActiveName(name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './side-menu.less';
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import TagsNav from './tags-nav.vue';
|
||||
export default TagsNav;
|
||||
@@ -0,0 +1,136 @@
|
||||
.no-select{
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.size{
|
||||
height: 100%;
|
||||
}
|
||||
.tags-nav{
|
||||
position: relative;
|
||||
margin:0 auto;
|
||||
// border-top: 1px solid #F0F0F0;
|
||||
// border-bottom: 1px solid #F0F0F0;
|
||||
background: #f4f6f8;
|
||||
padding:0 20px;
|
||||
|
||||
.no-select;
|
||||
.size;
|
||||
.close-con{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 5px;
|
||||
height: 30px;
|
||||
width: 42px;
|
||||
background: #f3f5f7;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
margin-top:5px;
|
||||
|
||||
border-top: 1px solid #F5F7FC;
|
||||
border-bottom: 1px solid #F5F7FC;
|
||||
i{
|
||||
color:#666666;
|
||||
}
|
||||
button{
|
||||
margin-top:3px;
|
||||
|
||||
}
|
||||
}
|
||||
.btn-con{
|
||||
margin-top:10px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
// padding:0 10px;
|
||||
background: #f3f5f7;
|
||||
z-index: 10;
|
||||
|
||||
button{
|
||||
position: relative;
|
||||
top:-1px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
color:#666666;
|
||||
|
||||
}
|
||||
&.left-btn{
|
||||
left: 0px;
|
||||
border: 1px solid #F5F7FC;
|
||||
}
|
||||
&.right-btn{
|
||||
right: 42px;
|
||||
border: 1px solid #F5F7FC;
|
||||
}
|
||||
}
|
||||
.scroll-outer{
|
||||
position: absolute;
|
||||
left: 52px;
|
||||
right: 61px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding:10px 0;
|
||||
.scroll-body{
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
box-shadow: border-box;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
transition: left .3s ease;
|
||||
.ivu-tag{
|
||||
height:34px;
|
||||
margin:0;
|
||||
}
|
||||
.ivu-tag{
|
||||
// min-width:10px;
|
||||
height:30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
padding:0 10px;
|
||||
background: #fff!important;
|
||||
border:none;
|
||||
text-align: center;
|
||||
color:#1C2B36;
|
||||
font-size: 14px;
|
||||
margin:0 5px;
|
||||
span{
|
||||
color:#1C2B36!important;
|
||||
padding:0 5px;
|
||||
}
|
||||
i{
|
||||
color:#1C2B36!important;
|
||||
font-size: 4px;
|
||||
}
|
||||
}
|
||||
.ivu-tag-primary{
|
||||
transition: background .2s ease;
|
||||
background: #2D8CF0!important;
|
||||
span,i{
|
||||
color:#fff!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
background: #fff;
|
||||
z-index: 1000;
|
||||
list-style-type: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .1);
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 5px 15px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="tags-nav">
|
||||
<div class="close-con">
|
||||
<Dropdown transfer @on-click="handleTagsOption">
|
||||
<Button size="small" type="text">
|
||||
<Icon :size="14" type="icon iconfont iconguanbianniu" />
|
||||
</Button>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem name="close-all">关闭所有</DropdownItem>
|
||||
<DropdownItem name="close-others">关闭其他</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<ul
|
||||
v-show="visible"
|
||||
:style="{left: contextMenuLeft + 'px', top: contextMenuTop + 'px'}"
|
||||
class="contextmenu"
|
||||
>
|
||||
<li v-for="(item, key) of menuList" @click="handleTagsOption(key)" :key="key">{{item}}</li>
|
||||
</ul>
|
||||
<div class="btn-con left-btn">
|
||||
<Button type="text" @click="handleScroll(240)">
|
||||
<Icon :size="14" type="icon iconfont iconxiangzuojiantou" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="btn-con right-btn">
|
||||
<Button type="text" @click="handleScroll(-240)">
|
||||
<Icon :size="14" type="icon iconfont iconxiangyoujiantou" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
class="scroll-outer"
|
||||
ref="scrollOuter"
|
||||
@DOMMouseScroll="handlescroll"
|
||||
@mousewheel="handlescroll"
|
||||
>
|
||||
<div ref="scrollBody" class="scroll-body" :style="{left: tagBodyLeft + 'px'}">
|
||||
<transition-group name="taglist-moving-animation">
|
||||
<Tag
|
||||
v-for="(item, index) in list"
|
||||
ref="tagsPageOpened"
|
||||
:key="`tag-nav-${index}`"
|
||||
:name="item.name"
|
||||
:data-route-item="item"
|
||||
@on-close="handleClose(item)"
|
||||
@click.native="handleClick(item)"
|
||||
:closable="item.name !== $config.homeName"
|
||||
:color="isCurrentTag(item) ? 'primary' : 'default'"
|
||||
@contextmenu.prevent.native="contextMenu(item, $event)"
|
||||
>{{ showTitleInside(item) }}</Tag>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showTitle, routeEqual } from '@/lib/menu-func';
|
||||
import beforeClose from '@/router/before-close';
|
||||
export default {
|
||||
name: 'TagsNav',
|
||||
props: {
|
||||
// 当前激活路由
|
||||
value: {
|
||||
type: Object,
|
||||
require: false
|
||||
},
|
||||
// 数据集合
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tagBodyLeft: 0,
|
||||
rightOffset: 40,
|
||||
outerPadding: 4,
|
||||
contextMenuLeft: 0,
|
||||
contextMenuTop: 0,
|
||||
visible: false,
|
||||
menuList: {
|
||||
others: '关闭其他',
|
||||
all: '关闭所有'
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 当前激活路由
|
||||
currentRouteObj () {
|
||||
const { name, params, query } = this.value;
|
||||
return { name, params, query };
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route (to) {
|
||||
this.getTagElementByRoute(to);
|
||||
},
|
||||
visible (value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu);
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
setTimeout(() => {
|
||||
this.getTagElementByRoute(this.$route);
|
||||
}, 200);
|
||||
},
|
||||
methods: {
|
||||
handlescroll (e) {
|
||||
let type = e.type;
|
||||
let delta = 0;
|
||||
if (type === 'DOMMouseScroll' || type === 'mousewheel') {
|
||||
delta = e.wheelDelta ? e.wheelDelta : -(e.detail || 0) * 40;
|
||||
}
|
||||
this.handleScroll(delta);
|
||||
},
|
||||
// 左右切换
|
||||
handleScroll (offset) {
|
||||
const outerWidth = this.$refs.scrollOuter.offsetWidth;
|
||||
const bodyWidth = this.$refs.scrollBody.offsetWidth;
|
||||
if (offset > 0) {
|
||||
this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset);
|
||||
} else {
|
||||
if (outerWidth < bodyWidth) {
|
||||
if (this.tagBodyLeft < -(bodyWidth - outerWidth)) {
|
||||
this.tagBodyLeft = this.tagBodyLeft;
|
||||
} else {
|
||||
this.tagBodyLeft = Math.max(
|
||||
this.tagBodyLeft + offset,
|
||||
outerWidth - bodyWidth - 35
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.tagBodyLeft = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleTagsOption (type) {
|
||||
if (type.includes('all')) {
|
||||
// 关闭所有,除了home
|
||||
let res = this.list.filter(item => item.name === this.$config.homeName);
|
||||
this.$emit('on-close', res, 'all');
|
||||
} else if (type.includes('others')) {
|
||||
// 关闭除当前页和home页的其他页
|
||||
let res = this.list.filter(
|
||||
item =>
|
||||
routeEqual(this.currentRouteObj, item) ||
|
||||
item.name === this.$config.homeName
|
||||
);
|
||||
this.$emit('on-close', res, 'others', this.currentRouteObj);
|
||||
setTimeout(() => {
|
||||
this.getTagElementByRoute(this.currentRouteObj);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
handleClose (current) {
|
||||
if (
|
||||
current.meta &&
|
||||
current.meta.beforeCloseName &&
|
||||
current.meta.beforeCloseName in beforeClose
|
||||
) {
|
||||
new Promise(beforeClose[current.meta.beforeCloseName]).then(close => {
|
||||
if (close) {
|
||||
this.close(current);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.close(current);
|
||||
}
|
||||
},
|
||||
close (route) {
|
||||
let res = this.list.filter(item => !routeEqual(route, item));
|
||||
this.$emit('on-close', res, undefined, route);
|
||||
},
|
||||
// 点击标签页
|
||||
handleClick (item) {
|
||||
// 标记从tag跳转 不清除缓存和刷新页面
|
||||
if (item.params) {
|
||||
item.params.isRefresh = false;
|
||||
} else {
|
||||
item.params = { isRefresh: false };
|
||||
}
|
||||
this.$emit('input', item);
|
||||
},
|
||||
showTitleInside (item) {
|
||||
return showTitle(item, this);
|
||||
},
|
||||
isCurrentTag (item) {
|
||||
return routeEqual(this.currentRouteObj, item);
|
||||
},
|
||||
moveToView(tag) {
|
||||
const outerWidth = this.$refs.scrollOuter.offsetWidth - 50;
|
||||
const bodyWidth = this.$refs.scrollBody.offsetWidth;
|
||||
let padding = 35;
|
||||
if (bodyWidth < outerWidth) {
|
||||
this.tagBodyLeft = 0;
|
||||
} else if (tag.offsetLeft < -this.tagBodyLeft) {
|
||||
// 标签在可视区域左侧
|
||||
this.tagBodyLeft = -tag.offsetLeft + this.outerPadding + padding;
|
||||
} else if (
|
||||
tag.offsetLeft > -this.tagBodyLeft &&
|
||||
tag.offsetLeft + tag.offsetWidth < -this.tagBodyLeft + outerWidth
|
||||
) {
|
||||
// 标签在可视区域
|
||||
this.tagBodyLeft = Math.min(
|
||||
0,
|
||||
outerWidth -
|
||||
tag.offsetWidth -
|
||||
tag.offsetLeft -
|
||||
this.outerPadding -
|
||||
padding
|
||||
);
|
||||
} else {
|
||||
// 标签在可视区域右侧
|
||||
this.tagBodyLeft = -(
|
||||
tag.offsetLeft -
|
||||
(outerWidth - this.outerPadding - tag.offsetWidth - padding)
|
||||
);
|
||||
}
|
||||
},
|
||||
getTagElementByRoute (route) {
|
||||
this.$nextTick(() => {
|
||||
this.refsTag = this.$refs.tagsPageOpened;
|
||||
this.refsTag.forEach((item, index) => {
|
||||
if (routeEqual(route, item.$attrs['data-route-item'])) {
|
||||
let tag = this.refsTag[index].$el;
|
||||
this.moveToView(tag);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
contextMenu (item, e) {
|
||||
if (item.name === this.$config.homeName) {
|
||||
return;
|
||||
}
|
||||
this.visible = true;
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left;
|
||||
this.contextMenuLeft = e.clientX - offsetLeft + 10;
|
||||
this.contextMenuTop = e.clientY - 64;
|
||||
},
|
||||
closeMenu () {
|
||||
this.visible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import "./tags-nav.less";
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import User from './user.vue';
|
||||
export default User;
|
||||
@@ -0,0 +1,25 @@
|
||||
.user-avatar-dropdown{
|
||||
padding-left:20px;
|
||||
color:#909ca4;
|
||||
.dropdown-arrows{
|
||||
margin:0 10px;
|
||||
}
|
||||
.head{
|
||||
position: relative;
|
||||
top:-2px;
|
||||
}
|
||||
}
|
||||
.user{
|
||||
|
||||
&-avatar-dropdown{
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
// height: 64px;
|
||||
vertical-align: middle;
|
||||
// line-height: 64px;
|
||||
.ivu-badge-dot{
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
131
smart-admin-web/src/components/main/components/user/user.vue
Normal file
131
smart-admin-web/src/components/main/components/user/user.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="user-avatar-dropdown">
|
||||
<Dropdown @on-click="handleClick">
|
||||
<label style="font-size: 14px;margin-left:6px;">{{loginInfo.actualName}}</label>
|
||||
<Icon :size="6" class="dropdown-arrows" type="icon iconfont iconxiangxiala"></Icon>
|
||||
<Avatar :src="loginInfo.header||defaultIcon" class="head" icon="ios-person" size="small" />
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem name="updatePassword">修改密码</DropdownItem>
|
||||
<DropdownItem name="logout">退出登录</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Modal
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
@on-cancel="editModal=false"
|
||||
@on-ok="editSure"
|
||||
title="修改密码"
|
||||
v-model="editModal"
|
||||
>
|
||||
<Form :label-width="100" :model="formValidate" :rules="ruleValidate" ref="formValidate">
|
||||
<FormItem label="密码:" prop="oldPwd">
|
||||
<Input placeholder="请输入原密码" type="password" v-model="formValidate.oldPwd" />
|
||||
</FormItem>
|
||||
<FormItem label="新密码:" prop="pwd">
|
||||
<Input placeholder="请输入新密码" type="password" v-model="formValidate.pwd" />
|
||||
</FormItem>
|
||||
<FormItem label="确认密码:" prop="pwdAgain">
|
||||
<Input placeholder="请再次输入新密码" type="password" v-model="formValidate.pwdAgain" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import './user.less';
|
||||
import { mapActions } from 'vuex';
|
||||
import { employeeApi } from '@/api/employee';
|
||||
import cookie from '@/lib/cookie';
|
||||
import { loginApi } from '@/api/login';
|
||||
export default {
|
||||
name: 'User',
|
||||
props: {
|
||||
// 未读消息数
|
||||
messageUnreadCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
// 当前登录人信息
|
||||
let loginInfo = this.$store.state.user.userLoginInfo;
|
||||
return {
|
||||
loginInfo: loginInfo,
|
||||
defaultIcon: require('@/assets/images/default_icon.png'),
|
||||
editModal: false,
|
||||
formValidate: {},
|
||||
ruleValidate: {
|
||||
oldPwd: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
pwd: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入新密码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
pwdAgain: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入确认密码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
this.$Spin.show();
|
||||
let token = cookie.getToken();
|
||||
localStorage.clear();
|
||||
cookie.clearToken();
|
||||
|
||||
loginApi.logout(token);
|
||||
location.reload();
|
||||
},
|
||||
updatePassword() {
|
||||
this.editModal = true;
|
||||
},
|
||||
handleClick(name) {
|
||||
switch (name) {
|
||||
case 'logout':
|
||||
this.logout();
|
||||
break;
|
||||
case 'updatePassword':
|
||||
this.updatePassword();
|
||||
break;
|
||||
}
|
||||
},
|
||||
async savePassword() {
|
||||
this.$Spin.show();
|
||||
let result = await employeeApi.updatePwd(this.formValidate);
|
||||
this.$Message.success('修改密码成功');
|
||||
this.$refs['formValidate'].resetFields();
|
||||
this.editModal = false;
|
||||
this.logout();
|
||||
},
|
||||
editSure() {
|
||||
this.$refs['formValidate'].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.formValidate.passwordAgain !== this.formValidate.loginPwd) {
|
||||
return this.$Message.error('两次输入密码不一致,请重新输入!!');
|
||||
}
|
||||
this.savePassword();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.arrow-icon {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
2
smart-admin-web/src/components/main/index.js
Normal file
2
smart-admin-web/src/components/main/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Main from './main.vue';
|
||||
export default Main;
|
||||
129
smart-admin-web/src/components/main/main.less
Normal file
129
smart-admin-web/src/components/main/main.less
Normal file
@@ -0,0 +1,129 @@
|
||||
//滚动条样式
|
||||
.scrollbar(@width: 2px){
|
||||
&::-webkit-scrollbar { width: @width; height: 8px; }
|
||||
&::-webkit-scrollbar-thumb { border-radius: 5px; -webkit-box-shadow: inset 0 0 5px rgb(185, 185, 185); background: #c7c5c8; }
|
||||
&::-webkit-scrollbar-track { border-radius: 0; background: #ddd; }
|
||||
}
|
||||
.no-scrollbar{
|
||||
&::-webkit-scrollbar {display:none}
|
||||
}
|
||||
.main{
|
||||
.scrollbar,
|
||||
.ivu-table-overflowX,
|
||||
.content-wrapper,
|
||||
.w-e-text{
|
||||
.scrollbar;
|
||||
}
|
||||
iframe body{
|
||||
.scrollbar;
|
||||
}
|
||||
.logo-con{
|
||||
padding: 19px 0 15px 20px;
|
||||
&.collapsed{
|
||||
padding: 19px 0 15px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.menu-dropdown{
|
||||
margin-left: 200px;
|
||||
}
|
||||
.search-bar{
|
||||
padding:0 20px;
|
||||
margin-bottom: 20px;
|
||||
&.collapsed{
|
||||
padding:0 12px;
|
||||
i{
|
||||
width: 40px;
|
||||
|
||||
}
|
||||
}
|
||||
input{
|
||||
width: 100%;
|
||||
height:36px;
|
||||
background: #152a3a;
|
||||
color:#44505c;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
|
||||
&::placeholder{
|
||||
color:#44505c;
|
||||
}
|
||||
&:focus{
|
||||
border:none;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
}
|
||||
border:none;
|
||||
}
|
||||
i{
|
||||
line-height: 36px;
|
||||
color:#44505c;
|
||||
}
|
||||
}
|
||||
.header-con{
|
||||
background: #fff;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
}
|
||||
.main-layout-con{
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main-content-con{
|
||||
height: ~"calc(100% - 60px)";
|
||||
overflow: hidden;
|
||||
}
|
||||
.tag-nav-wrapper{
|
||||
padding: 0;
|
||||
height:50px;
|
||||
// background:#F0F0F0;
|
||||
}
|
||||
.content-wrapper{
|
||||
padding: 0 18px 18px;
|
||||
height: ~"calc(100% - 80px)";
|
||||
overflow: auto;
|
||||
}
|
||||
.left-sider{
|
||||
.ivu-layout-sider-children{
|
||||
overflow-y: scroll;
|
||||
margin-right: -18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ivu-menu-light.ivu-menu-vertical .ivu-menu-item-active:not(.ivu-menu-submenu):after{
|
||||
right: inherit; left: 0;
|
||||
}
|
||||
.ivu-menu-item > i{
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
.collased-menu-dropdown{
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
padding: 7px 0 6px 16px;
|
||||
clear: both;
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease-in-out;
|
||||
&:hover{
|
||||
background: rgba(100, 100, 100, 0.1);
|
||||
}
|
||||
& * {
|
||||
color: #515a6e;
|
||||
}
|
||||
.ivu-menu-item > i{
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-select-dropdown.ivu-dropdown-transfer{
|
||||
max-height: 400px;
|
||||
}
|
||||
443
smart-admin-web/src/components/main/main.vue
Normal file
443
smart-admin-web/src/components/main/main.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<Layout class="main" style="height: 100%">
|
||||
<Sider
|
||||
:collapsed-width="60"
|
||||
:style="{overflow: 'hidden'}"
|
||||
:width="256"
|
||||
class="left-sider"
|
||||
collapsible
|
||||
hide-trigger
|
||||
v-model="collapsed"
|
||||
>
|
||||
<SideMenu
|
||||
:active-name="$route.name"
|
||||
:collapsed="collapsed"
|
||||
:menu-list="menuList"
|
||||
@on-select="turnToPage"
|
||||
accordion
|
||||
ref="sideMenu"
|
||||
>
|
||||
<!-- 需要放在菜单上面的内容,如Logo,写在side-menu标签内部,如下 -->
|
||||
<div :class="{'collapsed':collapsed}" class="logo-con">
|
||||
<img :src="maxLogo" key="max-logo" v-show="!collapsed" />
|
||||
<img :src="minLogo" key="min-logo" v-show="collapsed" />
|
||||
</div>
|
||||
<div :class="{'collapsed':collapsed}" class="search-bar">
|
||||
<div class="search-box">
|
||||
<Input placeholder="搜索" v-model="searchKeyWord">
|
||||
<Icon @click="collapsed=false" slot="prefix" type="ios-search" />
|
||||
</Input>
|
||||
<div class="searchMenu" v-if="searchKeyWord">
|
||||
<ul>
|
||||
<li
|
||||
:key="index"
|
||||
@click="toRoute(item.name)"
|
||||
v-for="(item, index) in searchListResult"
|
||||
>{{item.title}}</li>
|
||||
<li
|
||||
@click="searchKeyWord = ''"
|
||||
class="noData"
|
||||
v-if="searchListResult.length == 0"
|
||||
>未检索到数据</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SideMenu>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header class="header-con">
|
||||
<HeaderBar :collapsed="collapsed" @on-coll-change="handleCollapsedChange">
|
||||
<User :message-unread-count="unreadCount" />
|
||||
<language
|
||||
:lang="local"
|
||||
@on-lang-change="setLocal"
|
||||
style="margin-right: 10px;"
|
||||
v-if="$config.useI18n"
|
||||
/>
|
||||
<Notice />
|
||||
<Fullscreen style="margin-right: 10px;" v-model="isFullscreen" />
|
||||
</HeaderBar>
|
||||
</Header>
|
||||
<Content class="main-content-con">
|
||||
<Layout class="main-layout-con">
|
||||
<div class="tag-nav-wrapper">
|
||||
<TagsNav
|
||||
:list="tagNavList"
|
||||
:value="$route"
|
||||
@input="handleClick"
|
||||
@on-close="handleCloseTag"
|
||||
/>
|
||||
</div>
|
||||
<Content class="content-wrapper">
|
||||
<transition mode="out-in" name="fade-transform">
|
||||
<keep-alive :include="keepAliveIncludes">
|
||||
<router-view :key="key" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
<ABackTop :bottom="80" :height="100" :right="50" container=".content-wrapper"></ABackTop>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</template>
|
||||
<script>
|
||||
import ABackTop from './components/a-back-top';
|
||||
import SideMenu from './components/side-menu';
|
||||
import HeaderBar from './components/header-bar';
|
||||
import TagsNav from './components/tags-nav';
|
||||
import Notice from './components/notice/notice';
|
||||
import User from './components/user';
|
||||
import Fullscreen from './components/fullscreen';
|
||||
import Language from './components/language';
|
||||
import { mapMutations, mapActions, mapGetters } from 'vuex';
|
||||
import { getNewTagList, routeEqual, getShowMenu } from '@/lib/menu-func';
|
||||
import { routers } from '@/router/routers';
|
||||
import minLogo from '@/assets/images/logo-min.png';
|
||||
import maxLogo from '@/assets/images/logo.png';
|
||||
import { loginApi } from '@/api/login';
|
||||
import './main.less';
|
||||
export default {
|
||||
name: 'Main',
|
||||
components: {
|
||||
SideMenu,
|
||||
HeaderBar,
|
||||
Notice,
|
||||
Language,
|
||||
TagsNav,
|
||||
Fullscreen,
|
||||
User,
|
||||
ABackTop
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 是否折叠
|
||||
collapsed: false,
|
||||
minLogo,
|
||||
maxLogo,
|
||||
// 是否全屏
|
||||
isFullscreen: false,
|
||||
// 缓存的路由:
|
||||
includes: [],
|
||||
searchKeyWord: '',
|
||||
searchList: [],
|
||||
searchListResult: [],
|
||||
menuList: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['errorCount', 'userMenuPrivilege']),
|
||||
tagNavList() {
|
||||
return this.$store.state.app.tagNavList;
|
||||
},
|
||||
tagRouter() {
|
||||
return this.$store.state.app.tagRouter;
|
||||
},
|
||||
keepAliveIncludes() {
|
||||
return this.$store.state.app.keepAliveIncludes;
|
||||
},
|
||||
cacheList() {
|
||||
const list = [
|
||||
'ParentView',
|
||||
...(this.tagNavList.length
|
||||
? this.tagNavList
|
||||
.filter(item => !(item.meta && item.meta.noKeepAlive))
|
||||
.map(item => item.name)
|
||||
: [])
|
||||
];
|
||||
return list;
|
||||
},
|
||||
local() {
|
||||
return this.$store.state.app.local;
|
||||
},
|
||||
hasReadErrorPage() {
|
||||
return this.$store.state.app.hasReadErrorPage;
|
||||
},
|
||||
unreadCount() {
|
||||
return this.$store.state.user.unreadCount;
|
||||
},
|
||||
key() {
|
||||
return this.$route.name;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchKeyWord(val) {
|
||||
if (val) {
|
||||
this.searchListResult = this.searchList.filter(
|
||||
item => item.title.indexOf(val) >= 0
|
||||
);
|
||||
}
|
||||
},
|
||||
$route(newRoute) {
|
||||
const { name, query, params, meta } = newRoute;
|
||||
this.addTag({
|
||||
route: {
|
||||
name,
|
||||
query,
|
||||
params,
|
||||
meta
|
||||
},
|
||||
type: 'push'
|
||||
});
|
||||
this.setBreadCrumb(newRoute);
|
||||
// this.pushKeepAliveIncludes(newRoute);
|
||||
this.setTagNavList(getNewTagList(this.tagNavList, newRoute));
|
||||
this.$refs.sideMenu.updateOpenName(newRoute.name);
|
||||
// 如果param参数 存在 noKeepAlive切位true
|
||||
let isParamNoKeepAlive = params && params.noKeepAlive === true;
|
||||
// 如果query参数 存在 noKeepAlive切位true
|
||||
let isQueryNoKeepAlive = query && query.noKeepAlive === true;
|
||||
// 如果router meta 存在 noKeepAlive
|
||||
let isMetaNoKeepAlive = meta && meta.noKeepAlive === true;
|
||||
//如果存在noKeepAlive且已经缓存了,需要去掉
|
||||
if (isParamNoKeepAlive || isMetaNoKeepAlive || isQueryNoKeepAlive) {
|
||||
// 去掉keep-alive
|
||||
this.deleteKeepAliveIncludes(name);
|
||||
return;
|
||||
}
|
||||
//默认缓存住所有
|
||||
this.pushKeepAliveIncludes(name);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
/**
|
||||
* @description 初始化设置面包屑导航和标签导航
|
||||
*/
|
||||
this.setTagNavList();
|
||||
this.setHomeRoute(routers);
|
||||
this.setCollapsed();
|
||||
this.setBreadCrumb(this.$route);
|
||||
//初始化左侧菜单
|
||||
this.initSideMenu();
|
||||
const { name, params, query, meta } = this.$route;
|
||||
this.addTag({
|
||||
route: {
|
||||
name,
|
||||
params,
|
||||
query,
|
||||
meta
|
||||
}
|
||||
});
|
||||
|
||||
// 设置初始语言
|
||||
this.setLocal(this.$i18n.locale);
|
||||
// 如果当前打开页面不在标签栏中,跳到homeName页
|
||||
if (!this.tagNavList.find(item => item.name === this.$route.name)) {
|
||||
this.$router.push({
|
||||
name: this.$config.homeName
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'setBreadCrumb',
|
||||
'setTagNavList',
|
||||
'pushKeepAliveIncludes',
|
||||
'clearKeepAliveIncludes',
|
||||
'deleteKeepAliveIncludes',
|
||||
'deleteOtherKeepAliveIncludes',
|
||||
'addTag',
|
||||
'setLocal',
|
||||
'setHomeRoute',
|
||||
'closeTag'
|
||||
]),
|
||||
...mapActions(['handleLogin']),
|
||||
|
||||
initSideMenu() {
|
||||
//如果是登录跳转过来
|
||||
if (this.$store.state.user.isUpdatePrivilege) {
|
||||
this.$Spin.show();
|
||||
this.buildMenuTree();
|
||||
this.$refs.sideMenu.updateActiveName(this.$route.name);
|
||||
this.$Spin.hide();
|
||||
} else {
|
||||
//如果页面刷新,需要重新获取权限
|
||||
(async () => {
|
||||
this.$Spin.show();
|
||||
let sessionResult = await loginApi.getSession();
|
||||
//设置权限
|
||||
this.$store.commit(
|
||||
'setUserPrivilege',
|
||||
sessionResult.data.privilegeList
|
||||
);
|
||||
this.buildMenuTree();
|
||||
//刷新以后手动更新左侧菜单打开和选中
|
||||
this.$refs.sideMenu.updateActiveName(this.$route.name);
|
||||
this.$Spin.hide();
|
||||
})();
|
||||
}
|
||||
},
|
||||
buildMenuTree() {
|
||||
let privilegeTree = [];
|
||||
for (const router of routers) {
|
||||
//过滤非菜单
|
||||
if (!router.meta.hideInMenu) {
|
||||
//判断是否有权限
|
||||
if (
|
||||
this.$store.state.user.privilegeMenuKeyList.indexOf(router.name) !==
|
||||
-1
|
||||
) {
|
||||
let menu = {
|
||||
name: router.name,
|
||||
meta: router.meta,
|
||||
icon: _.isUndefined(router.meta.icon) ? '' : router.meta.icon,
|
||||
children: []
|
||||
};
|
||||
privilegeTree.push(menu);
|
||||
//存在孩子节点,开始递归
|
||||
if (router.children && router.children.length > 0) {
|
||||
this.recursion(router.children, menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.menuList = privilegeTree;
|
||||
},
|
||||
|
||||
recursion(children, parentMenu) {
|
||||
for (const router of children) {
|
||||
//过滤非菜单
|
||||
if (!router.meta.hideInMenu) {
|
||||
let menu = {
|
||||
name: router.name,
|
||||
meta: router.meta,
|
||||
icon: _.isUndefined(router.meta.icon) ? '' : router.meta.icon,
|
||||
children: []
|
||||
};
|
||||
this.searchList.push({
|
||||
name: router.name,
|
||||
title: router.meta.title
|
||||
});
|
||||
parentMenu.children.push(menu);
|
||||
//存在孩子节点,开始递归
|
||||
if (router.children && router.children.length > 0) {
|
||||
this.recursion(router.children, menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 自适应左侧导航宽度
|
||||
setCollapsed() {
|
||||
let setWidth = () => {
|
||||
let width = document.body.offsetWidth;
|
||||
if (width < 1340) {
|
||||
this.collapsed = true;
|
||||
} else {
|
||||
this.collapsed = false;
|
||||
}
|
||||
};
|
||||
setWidth();
|
||||
window.onresize = () => setWidth();
|
||||
},
|
||||
turnToPage(route) {
|
||||
let { name, params, query } = {};
|
||||
if (typeof route === 'string') name = route;
|
||||
else {
|
||||
name = route.name;
|
||||
params = route.params;
|
||||
query = route.query;
|
||||
}
|
||||
if (name.indexOf('isTurnByHref_') > -1) {
|
||||
window.open(name.split('_')[1]);
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
name,
|
||||
params,
|
||||
query
|
||||
});
|
||||
},
|
||||
handleCollapsedChange(state) {
|
||||
this.collapsed = state;
|
||||
},
|
||||
handleCloseTag(res, type, route) {
|
||||
let keepRouter = route ? route.name : null;
|
||||
if (type !== 'others') {
|
||||
if (type === 'all') {
|
||||
this.turnToPage(this.$config.homeName);
|
||||
this.clearKeepAliveIncludes(this.$config.homeName);
|
||||
} else {
|
||||
//如果是关闭单个tag,则从keepAliveIncludes将关闭的那个移除掉
|
||||
this.deleteKeepAliveIncludes(route.name);
|
||||
if (routeEqual(this.$route, route)) {
|
||||
this.closeTag(route);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果是关闭其他(others),则只会保留本页面和home页面
|
||||
this.deleteOtherKeepAliveIncludes(this.$route.name);
|
||||
}
|
||||
this.setTagNavList(res);
|
||||
},
|
||||
handleClick(item) {
|
||||
this.turnToPage(item);
|
||||
},
|
||||
// 跳转路由
|
||||
toRoute(name) {
|
||||
this.$router.push({ name });
|
||||
this.searchKeyWord = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
/*fade*/
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.28s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*fade-transform*/
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.fade-transform-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
.searchMenu {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 3px;
|
||||
padding-left: 20px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
left: 0;
|
||||
margin-top: 1px;
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
li {
|
||||
line-height: 28px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #3399ff;
|
||||
}
|
||||
&.noData {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
smart-admin-web/src/components/smart-admin-ad/ad.vue
Normal file
34
smart-admin-web/src/components/smart-admin-ad/ad.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div style="text-align:center">
|
||||
<img src="http://smartadmin.1024lab.net/image/smart-admin-log-400@400.png"/>
|
||||
<p style="text-align:left;font-size:16px;margin-top:-40px">
|
||||
|
||||
SmartAdmin由河南·洛阳 <a href="https://www.1024lab.net/" target="_blank">1024创新实验室</a>团队研发的一套互联网企业级的通用型中后台解决方案!使用SpringBoot和Vue,前后端分离。
|
||||
</p>
|
||||
<br/>
|
||||
<p style="text-align:left;font-size:16px">
|
||||
我们开源一套漂亮的代码和一套整洁的代码规范,让大家在这浮躁的代码世界里感受到一股把代码写好的清流!同时又让开发者节省大量的时间,减少加班,快乐工作,热爱生活。
|
||||
</p>
|
||||
<br/>
|
||||
<p style="text-align:left;font-size:20px">
|
||||
<font color="#DC143C"><strong>你的star 是我们最强的动力,感谢支持 !(^_^)∠※ </strong></font>
|
||||
<a target="_blank" href="https://github.com/1024-lab/smart-admin">
|
||||
<img style="width: 82px;height: 20px;margin-right: 10px" alt="" src="https://img.shields.io/github/stars/1024-lab/smart-admin.svg?logo=github&style=social">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Ad',
|
||||
props: {
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
2
smart-admin-web/src/components/smart-admin-ad/index.js
Normal file
2
smart-admin-web/src/components/smart-admin-ad/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Ad from './ad.vue';
|
||||
export default Ad;
|
||||
102
smart-admin-web/src/components/tables/edit.vue
Normal file
102
smart-admin-web/src/components/tables/edit.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="tables-edit-outer">
|
||||
<div v-if="!isEditting" class="tables-edit-con">
|
||||
<span class="value-con">{{ value }}</span>
|
||||
<Button
|
||||
v-if="editable"
|
||||
@click="startEdit"
|
||||
class="tables-edit-btn"
|
||||
style="padding: 2px 4px;"
|
||||
type="text"
|
||||
>
|
||||
<Icon type="md-create"></Icon>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="tables-editting-con">
|
||||
<Input :value="value" @input="handleInput" class="tables-edit-input"/>
|
||||
<Button @click="saveEdit" style="padding: 6px 4px;" type="text">
|
||||
<Icon type="md-checkmark"></Icon>
|
||||
</Button>
|
||||
<Button @click="canceltEdit" style="padding: 6px 4px;" type="text">
|
||||
<Icon type="md-close"></Icon>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TablesEdit',
|
||||
props: {
|
||||
// 当前编辑的单元格值
|
||||
value: {
|
||||
type: [String, Number],
|
||||
require: true
|
||||
},
|
||||
// 当前编辑的单元格id
|
||||
edittingCellId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
// 表格数据
|
||||
params: {
|
||||
type: Object,
|
||||
require: true
|
||||
},
|
||||
// 表格里全局设置的是否可编辑
|
||||
editable: {
|
||||
type: Boolean,
|
||||
require: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 判断是否处于编辑状态
|
||||
isEditting () {
|
||||
return this.edittingCellId === `editting-${this.params.index}-${this.params.column.key}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInput (val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
startEdit () {
|
||||
this.$emit('on-start-edit', this.params);
|
||||
},
|
||||
saveEdit () {
|
||||
this.$emit('on-save-edit', this.params);
|
||||
},
|
||||
canceltEdit () {
|
||||
this.$emit('on-cancel-edit', this.params);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.tables-edit-outer {
|
||||
height: 100%;
|
||||
.tables-edit-con {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
.value-con {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tables-edit-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.tables-edit-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tables-editting-con {
|
||||
.tables-edit-input {
|
||||
width: ~"calc(100% - 60px)";
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
smart-admin-web/src/components/tables/handle-btns.js
Normal file
70
smart-admin-web/src/components/tables/handle-btns.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// 验证
|
||||
const validate = {
|
||||
operation: (params) => {
|
||||
if (params.operation === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
audit: (params) => {
|
||||
if (params.audit === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const btns = {
|
||||
// 删除:需要判定
|
||||
delete: (h, params, vm) => {
|
||||
let disabledFlag = false;
|
||||
return h('Tooltip', {
|
||||
props: {
|
||||
content: '删除',
|
||||
placement: 'top',
|
||||
transfer: true
|
||||
}
|
||||
}, [h('Button', {
|
||||
props: {
|
||||
type: 'error',
|
||||
size: 'small',
|
||||
icon: 'md-trash',
|
||||
disabled: disabledFlag
|
||||
},
|
||||
style: {
|
||||
marginRight: '5px'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
vm.$emit('on-delete', params);
|
||||
}
|
||||
}
|
||||
})]);
|
||||
},
|
||||
edit: (h, params, vm) => {
|
||||
let disabledFlag = false;
|
||||
return h('Tooltip', {
|
||||
props: {
|
||||
content: '编辑',
|
||||
placement: 'top',
|
||||
transfer: true
|
||||
}
|
||||
}, [h('Button', {
|
||||
props: {
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
icon: 'md-trash',
|
||||
disabled: disabledFlag
|
||||
},
|
||||
style: {
|
||||
marginRight: '5px'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
vm.$emit('on-edit', params);
|
||||
}
|
||||
}
|
||||
})]);
|
||||
}
|
||||
};
|
||||
|
||||
export default btns;
|
||||
2
smart-admin-web/src/components/tables/index.js
Normal file
2
smart-admin-web/src/components/tables/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Tables from './tables.vue';
|
||||
export default Tables;
|
||||
17
smart-admin-web/src/components/tables/index.less
Normal file
17
smart-admin-web/src/components/tables/index.less
Normal file
@@ -0,0 +1,17 @@
|
||||
.search-con{
|
||||
padding: 10px 0;
|
||||
.search{
|
||||
&-col{
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
&-input{
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
&-btn{
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
342
smart-admin-web/src/components/tables/tables.vue
Normal file
342
smart-admin-web/src/components/tables/tables.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div>
|
||||
<Table
|
||||
ref="tablesMain"
|
||||
:data="insideTableData"
|
||||
:columns="insideColumns"
|
||||
:stripe="stripe"
|
||||
:border="border"
|
||||
:show-header="showHeader"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:loading="loading"
|
||||
:disabled-hover="disabledHover"
|
||||
:highlight-row="highlightRow"
|
||||
:row-class-name="rowClassName"
|
||||
:size="size"
|
||||
:no-data-text="noDataText"
|
||||
:no-filtered-data-text="noFilteredDataText"
|
||||
@on-current-change="onCurrentChange"
|
||||
@on-select="onSelect"
|
||||
@on-select-cancel="onSelectCancel"
|
||||
@on-select-all="onSelectAll"
|
||||
@on-selection-change="onSelectionChange"
|
||||
@on-sort-change="onSortChange"
|
||||
@on-filter-change="onFilterChange"
|
||||
@on-row-click="onRowClick"
|
||||
@on-row-dblclick="onRowDblclick"
|
||||
@on-expand="onExpand"
|
||||
>
|
||||
<slot name="header" slot="header"></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
<slot name="loading" slot="loading"></slot>
|
||||
</Table>
|
||||
<Page v-if="pageShow"
|
||||
:total="total"
|
||||
:current="pageNumber"
|
||||
:page-size="pageSize"
|
||||
:show-elevator="showElevator"
|
||||
:show-sizer="showSizer"
|
||||
:show-total="showTotal"
|
||||
:page-size-opts='pageOpts'
|
||||
style="margin:24px 0;text-align:right;"
|
||||
@on-change="pageChange" @on-page-size-change="pageSizeChange"></Page>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TablesEdit from './edit.vue';
|
||||
import handleBtns from './handle-btns';
|
||||
import './index.less';
|
||||
export default {
|
||||
name: 'Tables',
|
||||
props: {
|
||||
// table 表格数据 集合类型
|
||||
value: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 是否显示页码
|
||||
pageShow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 表头
|
||||
columns: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 每页数量
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
// 显示电梯,可以快速切换到某一页
|
||||
showElevator: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// table页码
|
||||
pageNumber: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 显示分页,用来改变page-size
|
||||
showSizer: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 显示总数
|
||||
showTotal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 总数
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 表格尺寸,可选值为 large、small、default 或者不填
|
||||
size: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
// 表格宽度,单位 px
|
||||
width: {
|
||||
type: [Number, String]
|
||||
},
|
||||
// 表格高度,单位 px,设置后,如果表格内容大于此值,会固定表头
|
||||
height: {
|
||||
type: [Number, String]
|
||||
},
|
||||
// 是否显示间隔斑马纹
|
||||
stripe: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示纵向边框
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示表头
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否支持高亮选中的行,即单选
|
||||
highlightRow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 行的 className 的回调方法,传入参数:row:当前行数据 ,index:当前行的索引
|
||||
rowClassName: {
|
||||
type: Function,
|
||||
default () {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
// 数据为空时显示的提示内容
|
||||
noDataText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 筛选数据为空时显示的提示内容
|
||||
noFilteredDataText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 禁用鼠标悬停时的高亮
|
||||
disabledHover: {
|
||||
type: Boolean
|
||||
},
|
||||
// 表格是否加载中
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 全局设置是否可编辑
|
||||
*/
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 是否可搜索
|
||||
*/
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* @description 搜索控件所在位置,'top' / 'bottom'
|
||||
*/
|
||||
searchPlace: {
|
||||
type: String,
|
||||
default: 'top'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Events
|
||||
* @on-start-edit 返回值 {Object} :同iview中render函数中的params对象 { row, index, column }
|
||||
* @on-cancel-edit 返回值 {Object} 同上
|
||||
* @on-save-edit 返回值 {Object} :除上面三个参数外,还有一个value: 修改后的数据
|
||||
*/
|
||||
data () {
|
||||
return {
|
||||
// 分页时每页数量选择
|
||||
pageOpts: [10, 20, 30, 50, 100],
|
||||
// 表格 每列的配置 如名称,宽度
|
||||
insideColumns: [],
|
||||
// 表格数据
|
||||
insideTableData: [],
|
||||
// 正在编辑的单元格id
|
||||
edittingCellId: '',
|
||||
// 编辑内容
|
||||
edittingText: '',
|
||||
// 搜索内容
|
||||
searchValue: '',
|
||||
// 搜索关键词
|
||||
searchKey: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
columns (columns) {
|
||||
this.handleColumns(columns);
|
||||
this.setDefaultSearchKey();
|
||||
},
|
||||
value (val) {
|
||||
this.handleTableData();
|
||||
if (this.searchable) this.handleSearch();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.handleColumns(this.columns);
|
||||
this.setDefaultSearchKey();
|
||||
this.handleTableData();
|
||||
},
|
||||
methods: {
|
||||
// 页码改变
|
||||
pageChange (e) {
|
||||
this.$emit('on-change', e);
|
||||
},
|
||||
// 每页条数改变
|
||||
pageSizeChange (e) {
|
||||
this.$emit('on-page-size-change', e);
|
||||
},
|
||||
suportEdit (item, index) {
|
||||
item.render = (h, params) => {
|
||||
return h(TablesEdit, {
|
||||
props: {
|
||||
params: params,
|
||||
value: this.insideTableData[params.index][params.column.key],
|
||||
edittingCellId: this.edittingCellId,
|
||||
editable: this.editable
|
||||
},
|
||||
on: {
|
||||
'input': val => {
|
||||
this.edittingText = val;
|
||||
},
|
||||
'on-start-edit': (params) => {
|
||||
this.edittingCellId = `editting-${params.index}-${params.column.key}`;
|
||||
this.$emit('on-start-edit', params);
|
||||
},
|
||||
'on-cancel-edit': (params) => {
|
||||
this.edittingCellId = '';
|
||||
this.$emit('on-cancel-edit', params);
|
||||
},
|
||||
'on-save-edit': (params) => {
|
||||
this.value[params.row.initRowIndex][params.column.key] = this.edittingText;
|
||||
this.$emit('input', this.value);
|
||||
this.$emit('on-save-edit', Object.assign(params, { value: this.edittingText }));
|
||||
this.edittingCellId = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
return item;
|
||||
},
|
||||
surportHandle (item) {
|
||||
let options = item.options || [];
|
||||
let insideBtns = [];
|
||||
options.forEach(item => {
|
||||
if (handleBtns[item]) insideBtns.push(handleBtns[item]);
|
||||
});
|
||||
let btns = item.button ? [].concat(insideBtns, item.button) : insideBtns;
|
||||
item.render = (h, params) => {
|
||||
params.tableData = this.value;
|
||||
return h('div', btns.map(item => item(h, params, this)));
|
||||
};
|
||||
return item;
|
||||
},
|
||||
// 设置表格列头
|
||||
handleColumns (columns) {
|
||||
this.insideColumns = columns.map((item, index) => {
|
||||
let res = item;
|
||||
if (res.editable) { res = this.suportEdit(res, index); }
|
||||
if (res.key === 'handle') { res = this.surportHandle(res); }
|
||||
return res;
|
||||
});
|
||||
},
|
||||
// 设置默认搜索关键词
|
||||
setDefaultSearchKey () {
|
||||
this.searchKey = this.columns[0].key !== 'handle' ? this.columns[0].key : (this.columns.length > 1 ? this.columns[1].key : '');
|
||||
},
|
||||
handleClear (e) {
|
||||
if (e.target.value === '') this.insideTableData = this.value;
|
||||
},
|
||||
handleSearch () {
|
||||
this.insideTableData = this.value.filter(item => item[this.searchKey].indexOf(this.searchValue) > -1);
|
||||
},
|
||||
handleTableData () {
|
||||
this.insideTableData = this.value.map((item, index) => {
|
||||
let res = item;
|
||||
res.initRowIndex = index;
|
||||
return res;
|
||||
});
|
||||
},
|
||||
exportCsv (params) {
|
||||
this.$refs.tablesMain.exportCsv(params);
|
||||
},
|
||||
clearCurrentRow () {
|
||||
this.$refs.talbesMain.clearCurrentRow();
|
||||
},
|
||||
onCurrentChange (currentRow, oldCurrentRow) {
|
||||
this.$emit('on-current-change', currentRow, oldCurrentRow);
|
||||
},
|
||||
onSelect (selection, row) {
|
||||
this.$emit('on-select', selection, row);
|
||||
},
|
||||
onSelectCancel (selection, row) {
|
||||
this.$emit('on-select-cancel', selection, row);
|
||||
},
|
||||
onSelectAll (selection) {
|
||||
this.$emit('on-select-all', selection);
|
||||
},
|
||||
onSelectionChange (selection) {
|
||||
this.$emit('on-selection-change', selection);
|
||||
},
|
||||
onSortChange (column, key, order) {
|
||||
this.$emit('on-sort-change', column, key, order);
|
||||
},
|
||||
onFilterChange (row) {
|
||||
this.$emit('on-filter-change', row);
|
||||
},
|
||||
onRowClick (row, index) {
|
||||
this.$emit('on-row-click', row, index);
|
||||
},
|
||||
onRowDblclick (row, index) {
|
||||
this.$emit('on-row-dblclick', row, index);
|
||||
},
|
||||
onExpand (row, status) {
|
||||
this.$emit('on-expand', row, status);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user