feat: 重建 Bell GoAdmin 产品骨架 (#62)

This commit is contained in:
QiuSW
2026-08-27 18:20:40 +08:00
parent b37c350096
commit f5a3dd796b
715 changed files with 75278 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<div ref="chartRef" style="width: 100%; height: 254px;" />
</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'AppBar',
props: {
title: { type: String, default: '' },
list: { type: Array, default: () => [] }
},
data() {
return { chart: null }
},
watch: {
list: { handler() { this.updateChart() }, deep: true }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.updateChart()
},
updateChart() {
if (!this.chart) return
const data = this.list || []
this.chart.setOption({
grid: { top: 20, bottom: 30, left: '3%', right: '3%', containLabel: true },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }},
xAxis: { type: 'category', data: data.map(d => d.x), axisTick: { alignWithLabel: true }},
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: data.map(d => d.y),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#40a9ff' },
{ offset: 1, color: '#1677ff' }
]),
borderRadius: [4, 4, 0, 0]
},
barMaxWidth: 40
}]
})
}
}
}
</script>
@@ -0,0 +1,82 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import { compile } from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === '首页'
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<div class="cell">
<div class="cell-inner" :class=" border ? 'border' : '' ">
<div class="cell-item">
<div class="cell-item-label">
<span v-if="label">
{{ label }}
</span>
<span v-else><slot name="label" />
</span></div>
<div class="cell-item-value">
<span v-if="value">
{{ value }}
</span>
<span v-else>
<slot name="value" />
</span>
</div>
</div>
<div class="cell-item mt5">
<span v-if="extra">{{ extra }}</span>
<span v-else><slot name="extra" /></span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AppCell',
props: {
border: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
value: {
type: String,
default: ''
},
extra: {
type: String,
default: ''
}
}
}
</script>
<style lang="scss" scoped>
.cell-inner{
box-sizing: border-box;
width: 100%;
overflow: hidden;
color: #323233;
font-size: 14px;
line-height: 24px;
background-color: #fff;
padding: 10px 0;
.cell-item{
display: flex;
justify-content: space-between;
align-items: center;
}
.cell-item-label{
span{
color: #323233;
font-size: 14px;
line-height: 24px;
}
}
.cell-item-value{
color: #969799;
}
}
.border{
position: relative;
&::after{
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 0;
bottom: 0;
left: 0;
border-bottom: 1px solid #e6ebf5;
}
}
</style>
+150
View File
@@ -0,0 +1,150 @@
<template>
<div class="chart-card-wrap" :style="{ '--kpi-color': color }">
<el-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
<div v-if="$slots.icon" class="kpi-icon-bg">
<slot name="icon" />
</div>
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">
<slot name="title">{{ title }}</slot>
</span>
<span class="chart-card-action">
<slot name="action" />
</span>
</div>
<div class="total">
<slot name="total">
<span>{{ typeof total === 'function' && total() || total }}</span>
</slot>
</div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot />
</div>
</div>
<div class="chart-card-footer">
<div class="field">
<slot name="footer" />
</div>
</div>
</el-card>
</div>
</template>
<script>
export default {
name: 'ChartCard',
props: {
title: { type: String, default: '' },
total: { type: [Function, Number, String], required: false, default: null },
loading: { type: Boolean, default: false },
color: { type: String, default: '#1677ff' }
}
}
</script>
<style lang="scss" scoped>
.chart-card-wrap {
position: relative;
border-radius: 8px;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background-color: var(--kpi-color, #1677ff);
z-index: 1;
border-radius: 4px 0 0 4px;
}
:deep(.el-card) {
border-left: none !important;
border-radius: 0 8px 8px 0;
}
}
.kpi-icon-bg {
position: absolute;
top: 12px;
right: 16px;
font-size: 56px;
opacity: 0.1;
color: var(--kpi-color, #1677ff);
line-height: 1;
pointer-events: none;
z-index: 0;
}
.chart-card-header {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, .45);
font-size: 14px;
line-height: 22px;
}
}
.chart-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.chart-card-footer {
border-top: 1px solid #f0f0f0;
padding-top: 9px;
margin-top: 8px;
> * { position: relative; }
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
color: rgba(0, 0, 0, .65);
font-size: 14px;
}
}
.chart-card-content {
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
.content-fix {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: rgba(0, 0, 0, 0.85);
margin-top: 4px;
margin-bottom: 0;
font-size: 28px;
line-height: 38px;
height: 38px;
font-weight: 600;
}
</style>
+155
View File
@@ -0,0 +1,155 @@
<template>
<div :id="id" :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '200px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
beforeUnmount() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.getElementById(this.id))
const xAxisData = []
const data = []
const data2 = []
for (let i = 0; i < 50; i++) {
xAxisData.push(i)
data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
}
this.chart.setOption({
backgroundColor: '#08263a',
grid: {
left: '5%',
right: '5%'
},
xAxis: [{
show: false,
data: xAxisData
}, {
show: false,
data: xAxisData
}],
visualMap: {
show: false,
min: 0,
max: 50,
dimension: 0,
inRange: {
color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
}
},
yAxis: {
axisLine: {
show: false
},
axisLabel: {
textStyle: {
color: '#4a657a'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#08263f'
}
},
axisTick: {
show: false
}
},
series: [{
name: 'back',
type: 'bar',
data: data2,
z: 1,
itemStyle: {
normal: {
opacity: 0.4,
barBorderRadius: 5,
shadowBlur: 3,
shadowColor: '#111'
}
}
}, {
name: 'Simulate Shadow',
type: 'line',
data,
z: 2,
showSymbol: false,
animationDelay: 0,
animationEasing: 'linear',
animationDuration: 1200,
lineStyle: {
normal: {
color: 'transparent'
}
},
areaStyle: {
normal: {
color: '#08263a',
shadowBlur: 50,
shadowColor: '#000'
}
}
}, {
name: 'front',
type: 'bar',
data,
xAxisIndex: 1,
z: 3,
itemStyle: {
normal: {
barBorderRadius: 5
}
}
}],
animationEasing: 'elasticOut',
animationEasingUpdate: 'elasticOut',
animationDelay(idx) {
return idx * 20
},
animationDelayUpdate(idx) {
return idx * 20
}
})
}
}
}
</script>
@@ -0,0 +1,227 @@
<template>
<div :id="id" :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '200px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
beforeUnmount() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.getElementById(this.id))
this.chart.setOption({
backgroundColor: '#394056',
title: {
top: 20,
text: 'Requests',
textStyle: {
fontWeight: 'normal',
fontSize: 16,
color: '#F1F1F3'
},
left: '1%'
},
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
color: '#57617B'
}
}
},
legend: {
top: 20,
icon: 'rect',
itemWidth: 14,
itemHeight: 5,
itemGap: 13,
data: ['CMCC', 'CTCC', 'CUCC'],
right: '4%',
textStyle: {
fontSize: 12,
color: '#F1F1F3'
}
},
grid: {
top: 100,
left: '2%',
right: '2%',
bottom: '2%',
containLabel: true
},
xAxis: [{
type: 'category',
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#57617B'
}
},
data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
}],
yAxis: [{
type: 'value',
name: '(%)',
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#57617B'
}
},
axisLabel: {
margin: 10,
textStyle: {
fontSize: 14
}
},
splitLine: {
lineStyle: {
color: '#57617B'
}
}
}],
series: [{
name: 'CMCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(137, 189, 27, 0.3)'
}, {
offset: 0.8,
color: 'rgba(137, 189, 27, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(137,189,27)',
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
}
},
data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
}, {
name: 'CTCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(0, 136, 212, 0.3)'
}, {
offset: 0.8,
color: 'rgba(0, 136, 212, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(0,136,212)',
borderColor: 'rgba(0,136,212,0.2)',
borderWidth: 12
}
},
data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
}, {
name: 'CUCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(219, 50, 51, 0.3)'
}, {
offset: 0.8,
color: 'rgba(219, 50, 51, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(219,50,51)',
borderColor: 'rgba(219,50,51,0.2)',
borderWidth: 12
}
},
data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
}]
})
}
}
}
</script>
+271
View File
@@ -0,0 +1,271 @@
<template>
<div :id="id" :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '200px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
beforeUnmount() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.getElementById(this.id))
const xData = (function() {
const data = []
for (let i = 1; i < 13; i++) {
data.push(i + 'month')
}
return data
}())
this.chart.setOption({
backgroundColor: '#344b58',
title: {
text: 'statistics',
x: '20',
top: '20',
textStyle: {
color: '#fff',
fontSize: '22'
},
subtextStyle: {
color: '#90979c',
fontSize: '16'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
textStyle: {
color: '#fff'
}
}
},
grid: {
left: '5%',
right: '5%',
borderWidth: 0,
top: 150,
bottom: 95,
textStyle: {
color: '#fff'
}
},
legend: {
x: '5%',
top: '10%',
textStyle: {
color: '#90979c'
},
data: ['female', 'male', 'average']
},
calculable: true,
xAxis: [{
type: 'category',
axisLine: {
lineStyle: {
color: '#90979c'
}
},
splitLine: {
show: false
},
axisTick: {
show: false
},
splitArea: {
show: false
},
axisLabel: {
interval: 0
},
data: xData
}],
yAxis: [{
type: 'value',
splitLine: {
show: false
},
axisLine: {
lineStyle: {
color: '#90979c'
}
},
axisTick: {
show: false
},
axisLabel: {
interval: 0
},
splitArea: {
show: false
}
}],
dataZoom: [{
show: true,
height: 30,
xAxisIndex: [
0
],
bottom: 30,
start: 10,
end: 80,
handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
handleSize: '110%',
handleStyle: {
color: '#d3dee5'
},
textStyle: {
color: '#fff' },
borderColor: '#90979c'
}, {
type: 'inside',
show: true,
height: 15,
start: 1,
end: 35
}],
series: [{
name: 'female',
type: 'bar',
stack: 'total',
barMaxWidth: 35,
barGap: '10%',
itemStyle: {
normal: {
color: 'rgba(255,144,128,1)',
label: {
show: true,
textStyle: {
color: '#fff'
},
position: 'insideTop',
formatter(p) {
return p.value > 0 ? p.value : ''
}
}
}
},
data: [
709,
1917,
2455,
2610,
1719,
1433,
1544,
3285,
5208,
3372,
2484,
4078
]
},
{
name: 'male',
type: 'bar',
stack: 'total',
itemStyle: {
normal: {
color: 'rgba(0,191,183,1)',
barBorderRadius: 0,
label: {
show: true,
position: 'top',
formatter(p) {
return p.value > 0 ? p.value : ''
}
}
}
},
data: [
327,
1776,
507,
1200,
800,
482,
204,
1390,
1001,
951,
381,
220
]
}, {
name: 'average',
type: 'line',
stack: 'total',
symbolSize: 10,
symbol: 'circle',
itemStyle: {
normal: {
color: 'rgba(252,230,48,1)',
barBorderRadius: 0,
label: {
show: true,
position: 'top',
formatter(p) {
return p.value > 0 ? p.value : ''
}
}
}
},
data: [
1036,
3693,
2962,
3810,
2519,
1915,
1748,
4675,
6209,
4323,
2865,
4298
]
}
]
})
}
}
}
</script>
@@ -0,0 +1,34 @@
import { debounce } from '@/utils'
export default {
data() {
return {
$_sidebarElm: null
}
},
mounted() {
this.__resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
window.addEventListener('resize', this.__resizeHandler)
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
beforeUnmount() {
window.removeEventListener('resize', this.__resizeHandler)
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.__resizeHandler()
}
}
}
}
+78
View File
@@ -0,0 +1,78 @@
<template>
<div v-if="errorLogs.length>0">
<el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click="dialogTableVisible=true">
<el-button style="padding: 8px 10px;" size="small" type="danger">
<svg-icon icon-class="bug" />
</el-button>
</el-badge>
<el-dialog v-model="showDialog" width="80%" append-to-body>
<template #header>
<span style="padding-right: 10px;">Error Log</span>
<el-button size="mini" type="primary" @click="clearAll"><i class="ri-delete-bin-line" style="margin-right:4px;" />Clear All</el-button>
</template>
<el-table :data="errorLogs" border>
<el-table-column label="Message">
<template #default="{row}">
<div>
<span class="message-title">Msg:</span>
<el-tag type="danger">
{{ row.err.message }}
</el-tag>
</div>
<br>
<div>
<span class="message-title" style="padding-right: 10px;">Info: </span>
<el-tag type="warning">
{{ row.vm.$vnode.tag }} error in {{ row.info }}
</el-tag>
</div>
<br>
<div>
<span class="message-title" style="padding-right: 16px;">Url: </span>
<el-tag type="success">
{{ row.url }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="Stack">
<template #default="scope">
{{ scope.row.err.stack }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'ErrorLog',
data() {
return {
dialogTableVisible: false
}
},
computed: {
errorLogs() {
return this.$store.getters.errorLogs
}
},
methods: {
clearAll() {
this.dialogTableVisible = false
this.$store.dispatch('errorLog/clearErrorLog')
}
}
}
</script>
<style scoped>
.message-title {
font-size: 16px;
color: #333;
font-weight: bold;
padding-right: 8px;
}
</style>
@@ -0,0 +1,193 @@
<script>
import { deepClone } from '@/utils/generator/index'
import render from '@/components/FormGenRender/render.js'
const ruleTrigger = {
'el-input': 'blur',
'el-input-number': 'blur',
'el-select': 'change',
'el-radio-group': 'change',
'el-checkbox-group': 'change',
'el-cascader': 'change',
'el-time-picker': 'change',
'el-date-picker': 'change',
'el-rate': 'change'
}
const layouts = {
colFormItem(h, scheme) {
const config = scheme.__config__
const listeners = buildListeners.call(this, scheme)
let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
if (config.showLabel === false) labelWidth = '0'
return (
<el-col span={config.span}>
<el-form-item label-width={labelWidth} prop={scheme.__vModel__}
label={config.showLabel ? config.label : ''}>
<render conf={scheme} on={listeners} />
</el-form-item>
</el-col>
)
},
rowFormItem(h, scheme) {
let child = renderChildren.apply(this, arguments)
if (scheme.type === 'flex') {
child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}>
{child}
</el-row>
}
return (
<el-col span={scheme.span}>
<el-row gutter={scheme.gutter}>
{child}
</el-row>
</el-col>
)
}
}
function renderFrom(h) {
const { formConfCopy } = this
return (
<el-row gutter={formConfCopy.gutter}>
<el-form
size={formConfCopy.size}
label-position={formConfCopy.labelPosition}
disabled={formConfCopy.disabled}
label-width={`${formConfCopy.labelWidth}px`}
ref={formConfCopy.formRef}
// model不能直接赋值 https://github.com/vuejs/jsx/issues/49#issuecomment-472013664
props={{ model: this[formConfCopy.formModel] }}
rules={this[formConfCopy.formRules]}
>
{renderFormItem.call(this, h, formConfCopy.fields)}
{formConfCopy.formBtns && formBtns.call(this, h)}
</el-form>
</el-row>
)
}
function formBtns(h) {
return <el-col>
<el-form-item size='large'>
<el-button type='primary' onClick={this.submitForm}>提交</el-button>
<el-button onClick={this.resetForm}>重置</el-button>
</el-form-item>
</el-col>
}
function renderFormItem(h, elementList) {
return elementList.map(scheme => {
const config = scheme.__config__
const layout = layouts[config.layout]
if (layout) {
return layout.call(this, h, scheme)
}
throw new Error(`没有与${config.layout}匹配的layout`)
})
}
function renderChildren(h, scheme) {
const config = scheme.__config__
if (!Array.isArray(config.children)) return null
return renderFormItem.call(this, h, config.children)
}
function setValue(event, config, scheme) {
config.defaultValue = event
this[this.formConf.formModel][scheme.__vModel__] = event
}
function buildListeners(scheme) {
const config = scheme.__config__
const methods = this.formConf.__methods__ || {}
const listeners = {}
// 给__methods__中的方法绑定this和event
Object.keys(methods).forEach(key => {
listeners[key] = event => methods[key].call(this, event)
})
// 响应 render.js 中的 vModel $emit('input', val)
listeners.input = event => setValue.call(this, event, config, scheme)
return listeners
}
export default {
components: {
render
},
props: {
formConf: {
type: Object,
required: true
}
// fileList: {
// type: Array,
// required: false
// }
},
data() {
const data = {
formConfCopy: deepClone(this.formConf),
[this.formConf.formModel]: {},
[this.formConf.formRules]: {}
// fileList: this.fileList
}
this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel])
this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules])
return data
},
methods: {
initFormData(componentList, formData) {
componentList.forEach(cur => {
const config = cur.__config__
if (cur.__vModel__) formData[cur.__vModel__] = config.defaultValue
if (config.children) this.initFormData(config.children, formData)
})
},
buildRules(componentList, rules) {
componentList.forEach(cur => {
const config = cur.__config__
if (Array.isArray(config.regList)) {
if (config.required) {
const required = { required: config.required, message: cur.placeholder }
if (Array.isArray(config.defaultValue)) {
required.type = 'array'
required.message = `请至少选择一个${config.label}`
}
required.message === undefined && (required.message = `${config.label}不能为空`)
config.regList.push(required)
}
rules[cur.__vModel__] = config.regList.map(item => {
// eslint-disable-next-line no-eval
item.pattern && (item.pattern = eval(item.pattern))
item.trigger = ruleTrigger && ruleTrigger[config.tag]
return item
})
}
if (config.children) this.buildRules(config.children, rules)
})
},
resetForm() {
this.formConfCopy = deepClone(this.formConf)
this.$refs[this.formConf.formRef].resetFields()
},
submitForm() {
this.$refs[this.formConf.formRef].validate(valid => {
if (!valid) return false
// 触发sumit事件
this.$emit('submit', this[this.formConf.formModel])
return true
})
}
},
render(h) {
return renderFrom.call(this, h)
}
}
</script>
@@ -0,0 +1,122 @@
import { deepClone } from '@/utils/generator/index'
const componentChild = {}
/**
* 将./slots中的文件挂载到对象componentChild上
* 文件名为key,对应JSON配置中的__config__.tag
* 文件内容为value,解析JSON配置中的__slot__
*/
const slotsFiles = require.context('./slots', false, /\.js$/)
const keys = slotsFiles.keys() || []
keys.forEach(key => {
const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = slotsFiles(key).default
componentChild[tag] = value
})
function vModel(dataObject, defaultValue) {
dataObject.props.value = defaultValue
dataObject.on.input = val => {
this.$emit('input', val)
}
}
function mountSlotFiles(h, confClone, children) {
const childObjs = componentChild[confClone.__config__.tag]
if (childObjs) {
Object.keys(childObjs).forEach(key => {
const childFunc = childObjs[key]
if (confClone.__slot__ && confClone.__slot__[key]) {
children.push(childFunc(h, confClone, key))
}
})
}
}
function emitEvents(confClone) {
['on', 'nativeOn'].forEach(attr => {
const eventKeyList = Object.keys(confClone[attr] || {})
eventKeyList.forEach(key => {
const val = confClone[attr][key]
if (typeof val === 'string') {
confClone[attr][key] = event => this.$emit(val, event)
}
})
})
}
function buildDataObject(confClone, dataObject) {
Object.keys(confClone).forEach(key => {
const val = confClone[key]
if (key === '__vModel__') {
vModel.call(this, dataObject, confClone.__config__.defaultValue)
} else if (dataObject[key] !== undefined) {
if (dataObject[key] === null ||
dataObject[key] instanceof RegExp ||
['boolean', 'string', 'number', 'function', 'Array'].includes(typeof dataObject[key])) {
dataObject[key] = val
} else if (Array.isArray(dataObject[key])) {
dataObject[key] = [...dataObject[key], ...val]
} else {
dataObject[key] = { ...dataObject[key], ...val }
}
} else {
dataObject.attrs[key] = val
}
})
// 清理属性
clearAttrs(dataObject)
}
function clearAttrs(dataObject) {
delete dataObject.attrs.__config__
delete dataObject.attrs.__slot__
delete dataObject.attrs.__methods__
}
function makeDataObject() {
// 深入数据对象:
// https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
return {
class: {},
attrs: {},
props: {},
domProps: {},
nativeOn: {},
on: {},
style: {},
directives: [],
scopedSlots: {},
slot: null,
key: null,
ref: null,
refInFor: true
}
}
export default {
props: {
conf: {
type: Object,
required: true
}
},
render(h) {
const dataObject = makeDataObject()
const confClone = deepClone(this.conf)
const children = this.$slots.default || []
// 如果slots文件夹存在与当前tag同名的文件,则执行文件中的代码
mountSlotFiles.call(this, h, confClone, children)
// 将字符串类型的事件,发送为消息
emitEvents.call(this, confClone)
// 将json表单配置转化为vue render可以识别的 “数据对象(dataObject)”
buildDataObject.call(this, confClone, dataObject)
return h(this.conf.__config__.tag, dataObject, children)
}
}
@@ -0,0 +1,5 @@
export default {
default(h, conf, key) {
return conf.__slot__[key]
}
}
@@ -0,0 +1,13 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
if (conf.__config__.optionType === 'button') {
list.push(<el-checkbox-button label={item.value}>{item.label}</el-checkbox-button>)
} else {
list.push(<el-checkbox label={item.value} border={conf.border}>{item.label}</el-checkbox>)
}
})
return list
}
}
@@ -0,0 +1,8 @@
export default {
prepend(h, conf, key) {
return <template slot='prepend'>{conf.__slot__[key]}</template>
},
append(h, conf, key) {
return <template slot='append'>{conf.__slot__[key]}</template>
}
}
@@ -0,0 +1,13 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
if (conf.__config__.optionType === 'button') {
list.push(<el-radio-button label={item.value}>{item.label}</el-radio-button>)
} else {
list.push(<el-radio label={item.value} border={conf.border}>{item.label}</el-radio>)
}
})
return list
}
}
@@ -0,0 +1,9 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
})
return list
}
}
@@ -0,0 +1,17 @@
export default {
'list-type': (h, conf, key) => {
const list = []
const config = conf.__config__
if (conf['list-type'] === 'picture-card') {
list.push(<i class='el-icon-plus'></i>)
} else {
list.push(<el-button size='small' type='primary' icon='el-icon-upload'>{config.buttonText}</el-button>)
}
if (config.showTip) {
list.push(
<div slot='tip' class='el-upload__tip'>只能上传不超过 {config.fileSize}{config.sizeUnit} 的{conf.accept}文件</div>
)
}
return list
}
}
@@ -0,0 +1,54 @@
<template>
<a href="https://github.com/wenjianzhang/go-admin" target="_blank" class="github-corner" aria-label="View source on Github">
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill:#40c9c6; color:#fff;"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px;"
class="octo-arm"
/>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
/>
</svg>
</a>
</template>
<style scoped>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0)
}
20%,
60% {
transform: rotate(-25deg)
}
40%,
80% {
transform: rotate(10deg)
}
}
@media (max-width:500px) {
.github-corner:hover .octo-arm {
animation: none
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out
}
}
</style>
@@ -0,0 +1,21 @@
<template>
<div>
<svg-icon icon-class="question" @click="goto" />
</div>
</template>
<script>
export default {
name: 'GoAdminDoc',
data() {
return {
url: 'http://doc.zhangwj.com/go-admin-site'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>
@@ -0,0 +1,21 @@
<template>
<div>
<svg-icon icon-class="heart" @click="goto" />
</div>
</template>
<script>
export default {
name: 'GoAdminDonate',
data() {
return {
url: 'http://doc.zhangwj.com/go-admin-site/donate/'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>
@@ -0,0 +1,21 @@
<template>
<div>
<svg-icon icon-class="github" @click="goto" />
</div>
</template>
<script>
export default {
name: 'GoAdminGit',
data() {
return {
url: 'https://github.com/wenjianzhang/go-admin'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'AppHamburger',
emits: ['toggleClick'],
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>
@@ -0,0 +1,180 @@
<template>
<div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
>
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js'
import path from 'path'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined
}
},
computed: {
routes() {
return this.$store.getters.permission_routes
}
},
watch: {
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
}
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
this.$router.push(val.path)
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: path.resolve(basePath, router.path),
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
}
}
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
:deep(.el-input__inner) {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>
@@ -0,0 +1,71 @@
<template>
<div class="icon-body">
<el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
<template #suffix>
<i class="ri-search-line el-input__icon" />
</template>
</el-input>
<div class="icon-list">
<div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
<svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
<span>{{ item }}</span>
</div>
</div>
</div>
</template>
<script>
import icons from './requireIcons'
export default {
name: 'IconSelect',
emits: ['selected'],
data() {
return {
name: '',
iconList: icons
}
},
methods: {
filterIcons() {
if (this.name) {
this.iconList = this.iconList.filter(item => item.includes(this.name))
} else {
this.iconList = icons
}
},
selectedIcon(name) {
this.$emit('selected', name)
document.body.click()
},
reset() {
this.name = ''
this.iconList = icons
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.icon-body {
width: 100%;
padding: 10px;
.icon-list {
height: 200px;
overflow-y: scroll;
div {
height: 30px;
line-height: 30px;
margin-bottom: -5px;
cursor: pointer;
width: 33%;
float: left;
}
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
}
}
</style>
@@ -0,0 +1,10 @@
const req = require.context('../../icons/svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys()
const re = /\.\/(.*)\.svg/
const icons = requireAll(req).map(i => {
return i.match(re)[1]
})
export default icons
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
/**
* database64文件格式转换为2进制
*
* @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
* @param {[String]} mime [description]
* @return {[blob]} [description]
*/
export default function(data, mime) {
data = data.split(',')[1]
data = window.atob(data)
var ia = new Uint8Array(data.length)
for (var i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i)
}
// canvas.toDataURL 返回的默认格式就是 image/png
return new Blob([ia], {
type: mime
})
}
@@ -0,0 +1,39 @@
/**
* 点击波纹效果
*
* @param {[event]} e [description]
* @param {[Object]} arg_opts [description]
* @return {[bollean]} [description]
*/
export default function(e, arg_opts) {
var opts = Object.assign({
ele: e.target, // 波纹作用元素
type: 'hit', // hit点击位置扩散center中心点扩展
bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
}, arg_opts)
var target = opts.ele
if (target) {
var rect = target.getBoundingClientRect()
var ripple = target.querySelector('.e-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'e-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'e-ripple'
}
switch (opts.type) {
case 'center':
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
break
default:
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.bgc
ripple.className = 'e-ripple z-active'
return false
}
}
@@ -0,0 +1,232 @@
export default {
zh: {
hint: '点击,或拖动图片至此处',
loading: '正在上传……',
noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
success: '上传成功',
fail: '图片上传失败',
preview: '头像预览',
btn: {
off: '取消',
close: '关闭',
back: '上一步',
save: '保存'
},
error: {
onlyImg: '仅限图片格式',
outOfSize: '单文件大小不能超过 ',
lowestPx: '图片最低像素为(宽*高):'
}
},
'zh-tw': {
hint: '點擊,或拖動圖片至此處',
loading: '正在上傳……',
noSupported: '瀏覽器不支持該功能,請使用IE10以上或其他現代瀏覽器!',
success: '上傳成功',
fail: '圖片上傳失敗',
preview: '頭像預覽',
btn: {
off: '取消',
close: '關閉',
back: '上一步',
save: '保存'
},
error: {
onlyImg: '僅限圖片格式',
outOfSize: '單文件大小不能超過 ',
lowestPx: '圖片最低像素為(寬*高):'
}
},
en: {
hint: 'Click or drag the file here to upload',
loading: 'Uploading…',
noSupported: 'Browser is not supported, please use IE10+ or other browsers',
success: 'Upload success',
fail: 'Upload failed',
preview: 'Preview',
btn: {
off: 'Cancel',
close: 'Close',
back: 'Back',
save: 'Save'
},
error: {
onlyImg: 'Image only',
outOfSize: 'Image exceeds size limit: ',
lowestPx: 'Image\'s size is too low. Expected at least: '
}
},
ro: {
hint: 'Atinge sau trage fișierul aici',
loading: 'Se încarcă',
noSupported: 'Browser-ul tău nu suportă acest feature. Te rugăm încearcă cu alt browser.',
success: 'S-a încărcat cu succes',
fail: 'A apărut o problemă la încărcare',
preview: 'Previzualizează',
btn: {
off: 'Anulează',
close: 'Închide',
back: 'Înapoi',
save: 'Salvează'
},
error: {
onlyImg: 'Doar imagini',
outOfSize: 'Imaginea depășește limita de: ',
loewstPx: 'Imaginea este prea mică; Minim: '
}
},
ru: {
hint: 'Нажмите, или перетащите файл в это окно',
loading: 'Загружаю……',
noSupported: 'Ваш браузер не поддерживается, пожалуйста, используйте IE10 + или другие браузеры',
success: 'Загрузка выполнена успешно',
fail: 'Ошибка загрузки',
preview: 'Предпросмотр',
btn: {
off: 'Отменить',
close: 'Закрыть',
back: 'Назад',
save: 'Сохранить'
},
error: {
onlyImg: 'Только изображения',
outOfSize: 'Изображение превышает предельный размер: ',
lowestPx: 'Минимальный размер изображения: '
}
},
'pt-br': {
hint: 'Clique ou arraste o arquivo aqui para carregar',
loading: 'Carregando…',
noSupported: 'Browser não suportado, use o IE10+ ou outro browser',
success: 'Sucesso ao carregar imagem',
fail: 'Falha ao carregar imagem',
preview: 'Pré-visualizar',
btn: {
off: 'Cancelar',
close: 'Fechar',
back: 'Voltar',
save: 'Salvar'
},
error: {
onlyImg: 'Apenas imagens',
outOfSize: 'A imagem excede o limite de tamanho: ',
lowestPx: 'O tamanho da imagem é muito pequeno. Tamanho mínimo: '
}
},
fr: {
hint: 'Cliquez ou glissez le fichier ici.',
loading: 'Téléchargement…',
noSupported: 'Votre navigateur n\'est pas supporté. Utilisez IE10 + ou un autre navigateur s\'il vous plaît.',
success: 'Téléchargement réussit',
fail: 'Téléchargement echoué',
preview: 'Aperçu',
btn: {
off: 'Annuler',
close: 'Fermer',
back: 'Retour',
save: 'Enregistrer'
},
error: {
onlyImg: 'Image uniquement',
outOfSize: 'L\'image sélectionnée dépasse la taille maximum: ',
lowestPx: 'L\'image sélectionnée est trop petite. Dimensions attendues: '
}
},
nl: {
hint: 'Klik hier of sleep een afbeelding in dit vlak',
loading: 'Uploaden…',
noSupported: 'Je browser wordt helaas niet ondersteund. Gebruik IE10+ of een andere browser.',
success: 'Upload succesvol',
fail: 'Upload mislukt',
preview: 'Voorbeeld',
btn: {
off: 'Annuleren',
close: 'Sluiten',
back: 'Terug',
save: 'Opslaan'
},
error: {
onlyImg: 'Alleen afbeeldingen',
outOfSize: 'De afbeelding is groter dan: ',
lowestPx: 'De afbeelding is te klein! Minimale afmetingen: '
}
},
tr: {
hint: 'Tıkla veya yüklemek istediğini buraya sürükle',
loading: 'Yükleniyor…',
noSupported: 'Tarayıcı desteklenmiyor, lütfen IE10+ veya farklı tarayıcı kullanın',
success: 'Yükleme başarılı',
fail: 'Yüklemede hata oluştu',
preview: 'Önizle',
btn: {
off: 'İptal',
close: 'Kapat',
back: 'Geri',
save: 'Kaydet'
},
error: {
onlyImg: 'Sadece resim',
outOfSize: 'Resim yükleme limitini aşıyor: ',
lowestPx: 'Resmin boyutu çok küçük. En az olması gereken: '
}
},
'es-MX': {
hint: 'Selecciona o arrastra una imagen',
loading: 'Subiendo...',
noSupported: 'Tu navegador no es soportado, porfavor usa IE10+ u otros navegadores mas recientes',
success: 'Subido exitosamente',
fail: 'Sucedió un error',
preview: 'Vista previa',
btn: {
off: 'Cancelar',
close: 'Cerrar',
back: 'Atras',
save: 'Guardar'
},
error: {
onlyImg: 'Unicamente imagenes',
outOfSize: 'La imagen excede el tamaño maximo:',
lowestPx: 'La imagen es demasiado pequeño. Se espera por lo menos:'
}
},
de: {
hint: 'Klick hier oder zieh eine Datei hier rein zum Hochladen',
loading: 'Hochladen…',
noSupported: 'Browser wird nicht unterstützt, bitte verwende IE10+ oder andere Browser',
success: 'Upload erfolgreich',
fail: 'Upload fehlgeschlagen',
preview: 'Vorschau',
btn: {
off: 'Abbrechen',
close: 'Schließen',
back: 'Zurück',
save: 'Speichern'
},
error: {
onlyImg: 'Nur Bilder',
outOfSize: 'Das Bild ist zu groß: ',
lowestPx: 'Das Bild ist zu klein. Mindestens: '
}
},
ja: {
hint: 'クリック・ドラッグしてファイルをアップロード',
loading: 'アップロード中...',
noSupported: 'このブラウザは対応されていません。IE10+かその他の主要ブラウザをお使いください。',
success: 'アップロード成功',
fail: 'アップロード失敗',
preview: 'プレビュー',
btn: {
off: 'キャンセル',
close: '閉じる',
back: '戻る',
save: '保存'
},
error: {
onlyImg: '画像のみ',
outOfSize: '画像サイズが上限を超えています。上限: ',
lowestPx: '画像が小さすぎます。最小サイズ: '
}
}
}
@@ -0,0 +1,7 @@
export default {
'jpg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'psd': 'image/photoshop'
}
+360
View File
@@ -0,0 +1,360 @@
<template>
<div :class="computedClasses" class="material-input__component">
<div :class="{iconClass:icon}">
<i v-if="icon" :class="['ri-' + icon]" class="el-input__icon material-input__icon" />
<input
v-if="type === 'email'"
v-model="currentValue"
:name="name"
:placeholder="fillPlaceHolder"
:readonly="readonly"
:disabled="disabled"
:autocomplete="autoComplete"
:required="required"
type="email"
class="material-input"
@focus="handleMdFocus"
@blur="handleMdBlur"
@input="handleModelInput"
>
<input
v-if="type === 'url'"
v-model="currentValue"
:name="name"
:placeholder="fillPlaceHolder"
:readonly="readonly"
:disabled="disabled"
:autocomplete="autoComplete"
:required="required"
type="url"
class="material-input"
@focus="handleMdFocus"
@blur="handleMdBlur"
@input="handleModelInput"
>
<input
v-if="type === 'number'"
v-model="currentValue"
:name="name"
:placeholder="fillPlaceHolder"
:step="step"
:readonly="readonly"
:disabled="disabled"
:autocomplete="autoComplete"
:max="max"
:min="min"
:minlength="minlength"
:maxlength="maxlength"
:required="required"
type="number"
class="material-input"
@focus="handleMdFocus"
@blur="handleMdBlur"
@input="handleModelInput"
>
<input
v-if="type === 'password'"
v-model="currentValue"
:name="name"
:placeholder="fillPlaceHolder"
:readonly="readonly"
:disabled="disabled"
:autocomplete="autoComplete"
:max="max"
:min="min"
:required="required"
type="password"
class="material-input"
@focus="handleMdFocus"
@blur="handleMdBlur"
@input="handleModelInput"
>
<input
v-if="type === 'tel'"
v-model="currentValue"
:name="name"
:placeholder="fillPlaceHolder"
:readonly="readonly"
:disabled="disabled"
:autocomplete="autoComplete"
:required="required"
type="tel"
class="material-input"
@focus="handleMdFocus"
@blur="handleMdBlur"
@input="handleModelInput"
>
<input
v-if="type === 'text'"
v-model="currentValue"
:name="name"
:placeholder="fillPlaceHolder"
:readonly="readonly"
:disabled="disabled"
:autocomplete="autoComplete"
:minlength="minlength"
:maxlength="maxlength"
:required="required"
type="text"
class="material-input"
@focus="handleMdFocus"
@blur="handleMdBlur"
@input="handleModelInput"
>
<span class="material-input-bar" />
<label class="material-label">
<slot />
</label>
</div>
</div>
</template>
<script>
// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
export default {
name: 'MdInput',
props: {
/* eslint-disable */
icon: String,
name: String,
type: {
type: String,
default: 'text'
},
value: [String, Number],
placeholder: String,
readonly: Boolean,
disabled: Boolean,
min: String,
max: String,
step: String,
minlength: Number,
maxlength: Number,
required: {
type: Boolean,
default: true
},
autoComplete: {
type: String,
default: 'off'
},
validateEvent: {
type: Boolean,
default: true
}
},
data() {
return {
currentValue: this.value,
focus: false,
fillPlaceHolder: null
}
},
computed: {
computedClasses() {
return {
'material--active': this.focus,
'material--disabled': this.disabled,
'material--raised': Boolean(this.focus || this.currentValue) // has value
}
}
},
watch: {
value(newValue) {
this.currentValue = newValue
}
},
methods: {
handleModelInput(event) {
const value = event.target.value
this.$emit('input', value)
if (this.$parent.$options.componentName === 'ElFormItem') {
if (this.validateEvent) {
this.$parent.$emit('el.form.change', [value])
}
}
this.$emit('change', value)
},
handleMdFocus(event) {
this.focus = true
this.$emit('focus', event)
if (this.placeholder && this.placeholder !== '') {
this.fillPlaceHolder = this.placeholder
}
},
handleMdBlur(event) {
this.focus = false
this.$emit('blur', event)
this.fillPlaceHolder = null
if (this.$parent.$options.componentName === 'ElFormItem') {
if (this.validateEvent) {
this.$parent.$emit('el.form.blur', [this.currentValue])
}
}
}
}
}
</script>
<style lang="scss" scoped>
// Fonts:
$font-size-base: 16px;
$font-size-small: 18px;
$font-size-smallest: 12px;
$font-weight-normal: normal;
$font-weight-bold: bold;
$apixel: 1px;
// Utils
$spacer: 12px;
$transition: 0.2s ease all;
$index: 0px;
$index-has-icon: 30px;
// Theme:
$color-white: white;
$color-grey: #9E9E9E;
$color-grey-light: #E0E0E0;
$color-blue: #2196F3;
$color-red: #F44336;
$color-black: black;
// Base clases:
%base-bar-pseudo {
content: '';
height: 1px;
width: 0;
bottom: 0;
position: absolute;
transition: $transition;
}
// Mixins:
@mixin slided-top() {
top: - ($font-size-base + $spacer);
left: 0;
font-size: $font-size-base;
font-weight: $font-weight-bold;
}
// Component:
.material-input__component {
margin-top: 36px;
position: relative;
* {
box-sizing: border-box;
}
.iconClass {
.material-input__icon {
position: absolute;
left: 0;
line-height: $font-size-base;
color: $color-blue;
top: $spacer;
width: $index-has-icon;
height: $font-size-base;
font-size: $font-size-base;
font-weight: $font-weight-normal;
pointer-events: none;
}
.material-label {
left: $index-has-icon;
}
.material-input {
text-indent: $index-has-icon;
}
}
.material-input {
font-size: $font-size-base;
padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
display: block;
width: 100%;
border: none;
line-height: 1;
border-radius: 0;
&:focus {
outline: none;
border: none;
border-bottom: 1px solid transparent; // fixes the height issue
}
}
.material-label {
font-weight: $font-weight-normal;
position: absolute;
pointer-events: none;
left: $index;
top: 0;
transition: $transition;
font-size: $font-size-small;
}
.material-input-bar {
position: relative;
display: block;
width: 100%;
&:before {
@extend %base-bar-pseudo;
left: 50%;
}
&:after {
@extend %base-bar-pseudo;
right: 50%;
}
}
// Disabled state:
&.material--disabled {
.material-input {
border-bottom-style: dashed;
}
}
// Raised state:
&.material--raised {
.material-label {
@include slided-top();
}
}
// Active state:
&.material--active {
.material-input-bar {
&:before,
&:after {
width: 50%;
}
}
}
}
.material-input__component {
background: $color-white;
.material-input {
background: none;
color: $color-black;
text-indent: $index;
border-bottom: 1px solid $color-grey-light;
}
.material-label {
color: $color-grey;
}
.material-input-bar {
&:before,
&:after {
background: $color-blue;
}
}
// Active state:
&.material--active {
.material-label {
color: $color-blue;
}
}
// Errors:
&.material--has-errors {
&.material--active .material-label {
color: $color-red;
}
.material-input-bar {
&:before,
&:after {
background: transparent;
}
}
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="antv-chart-mini">
<div ref="chartRef" style="height: 50px;" />
</div>
</template>
<script>
import * as echarts from 'echarts'
import moment from 'moment'
const generateData = () => {
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('MM-DD'),
y: Math.round(Math.random() * 10)
})
}
return data
}
export default {
name: 'MiniArea',
data() {
return { chart: null, rawData: generateData() }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.chart.setOption({
grid: { top: 2, bottom: 2, left: 2, right: 2 },
xAxis: { type: 'category', data: this.rawData.map(d => d.x), show: false, boundaryGap: false },
yAxis: { show: false },
series: [{
type: 'line',
data: this.rawData.map(d => d.y),
smooth: true,
symbol: 'none',
lineStyle: { color: '#1677ff', width: 2 },
areaStyle: { color: 'rgba(22,119,255,0.15)' }
}]
})
}
}
}
</script>
<style lang="scss" scoped>
.antv-chart-mini {
position: relative;
width: 100%;
}
</style>
+58
View File
@@ -0,0 +1,58 @@
<template>
<div class="antv-chart-mini">
<div ref="chartRef" style="height: 50px;" />
</div>
</template>
<script>
import * as echarts from 'echarts'
import moment from 'moment'
const generateData = () => {
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('MM-DD'),
y: Math.round(Math.random() * 10)
})
}
return data
}
export default {
name: 'MiniBar',
data() {
return { chart: null, rawData: generateData() }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.chart.setOption({
grid: { top: 2, bottom: 2, left: 2, right: 2 },
xAxis: { type: 'category', data: this.rawData.map(d => d.x), show: false },
yAxis: { show: false },
series: [{
type: 'bar',
data: this.rawData.map(d => d.y),
itemStyle: { color: '#1677ff', borderRadius: 2 },
barMaxWidth: 12
}]
})
}
}
}
</script>
<style lang="scss" scoped>
.antv-chart-mini {
position: relative;
width: 100%;
}
</style>
@@ -0,0 +1,66 @@
<template>
<div class="chart-mini-progress">
<div class="target" :style="{ left: target + '%'}">
<span :style="{ backgroundColor: indicatorColor }" />
<span :style="{ backgroundColor: indicatorColor }" />
</div>
<div class="progress-wrapper">
<div class="progress" :style="{ background: color, width: percentage + '%', height: height }" />
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: {
target: { type: Number, default: 0 },
height: { type: String, default: '10px' },
color: { type: String, default: 'linear-gradient(90deg, #40a9ff, #1677ff)' },
indicatorColor: { type: String, default: '#1677ff' },
percentage: { type: Number, default: 0 }
}
}
</script>
<style lang="scss" scoped>
.chart-mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
&:last-child {
top: auto;
bottom: 0;
}
}
}
.progress-wrapper {
background-color: #f0f0f0;
border-radius: 4px;
position: relative;
overflow: hidden;
.progress {
transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
border-radius: 4px;
width: 0;
height: 100%;
}
}
}
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div :class="{'hidden':hidden}" class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:background="background"
:layout="layout"
:page-sizes="pageSizes"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import { scrollTo } from '@/utils/scroll-to'
export default {
name: 'PaginationComponent',
emits: ['update:page', 'update:limit', 'pagination'],
props: {
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
},
computed: {
currentPage: {
get() {
return this.page
},
set(val) {
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
handleSizeChange(val) {
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) {
scrollTo(0, 800)
}
},
handleCurrentChange(val) {
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
scrollTo(0, 800)
}
}
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
}
</style>
+142
View File
@@ -0,0 +1,142 @@
<template>
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
<div class="pan-info">
<div class="pan-info-roles-container">
<slot />
</div>
</div>
<!-- eslint-disable-next-line -->
<div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
</div>
</template>
<script>
export default {
name: 'PanThumb',
props: {
image: {
type: String,
required: true
},
zIndex: {
type: Number,
default: 1
},
width: {
type: String,
default: '150px'
},
height: {
type: String,
default: '150px'
}
}
}
</script>
<style scoped>
.pan-item {
width: 200px;
height: 200px;
border-radius: 50%;
display: inline-block;
position: relative;
cursor: default;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.pan-info-roles-container {
padding: 20px;
text-align: center;
}
.pan-thumb {
width: 100%;
height: 100%;
background-position: center center;
background-size: cover;
border-radius: 50%;
overflow: hidden;
position: absolute;
transform-origin: 95% 40%;
transition: all 0.3s ease-in-out;
}
/* .pan-thumb:after {
content: '';
width: 8px;
height: 8px;
position: absolute;
border-radius: 50%;
top: 40%;
left: 95%;
margin: -4px 0 0 -4px;
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
} */
.pan-info {
position: absolute;
width: inherit;
height: inherit;
border-radius: 50%;
overflow: hidden;
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
}
.pan-info h3 {
color: #fff;
text-transform: uppercase;
position: relative;
letter-spacing: 2px;
font-size: 18px;
margin: 0 60px;
padding: 22px 0 0 0;
height: 85px;
font-family: 'Open Sans', Arial, sans-serif;
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
}
.pan-info p {
color: #fff;
padding: 10px 5px;
font-style: italic;
margin: 0 30px;
font-size: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
.pan-info p a {
display: block;
color: #333;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: #fff;
font-style: normal;
font-weight: 700;
text-transform: uppercase;
font-size: 9px;
letter-spacing: 1px;
padding-top: 24px;
margin: 7px auto 0;
font-family: 'Open Sans', Arial, sans-serif;
opacity: 0;
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
transform: translateX(60px) rotate(90deg);
}
.pan-info p a:hover {
background: rgba(255, 255, 255, 0.5);
}
.pan-item:hover .pan-thumb {
transform: rotate(-110deg);
}
.pan-item:hover .pan-info p a {
opacity: 1;
transform: translateX(0px) rotate(0deg);
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<div class="rank">
<h4 class="title">{{ title }}</h4>
<ul class="list">
<li v-for="(item, index) in list" :key="index">
<span :class="rankBadgeClass(index)">{{ index + 1 }}</span>
<span class="rank-name">{{ item.name }}</span>
<span class="rank-total">{{ item.total }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'RankList',
props: {
title: { type: String, default: '' },
list: { type: Array, default: null }
},
methods: {
rankBadgeClass(index) {
if (index === 0) return 'rank-badge rank-gold'
if (index === 1) return 'rank-badge rank-silver'
if (index === 2) return 'rank-badge rank-bronze'
return 'rank-badge'
}
}
}
</script>
<style lang="scss" scoped>
.rank {
padding: 0 32px 32px 72px;
.title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 0;
}
.list {
margin: 16px 0 0;
padding: 0;
list-style: none;
li {
display: flex;
align-items: center;
margin-top: 14px;
}
}
.rank-badge {
flex-shrink: 0;
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
margin-right: 16px;
background: #f0f0f0;
color: #8c8c8c;
&.rank-gold {
background: linear-gradient(135deg, #ffe58f, #faad14);
color: #fff;
}
&.rank-silver {
background: linear-gradient(135deg, #d9d9d9, #8c8c8c);
color: #fff;
}
&.rank-bronze {
background: linear-gradient(135deg, #ffbb96, #d4380d);
color: #fff;
}
}
.rank-name {
flex: 1;
color: rgba(0, 0, 0, .65);
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rank-total {
flex-shrink: 0;
color: rgba(0, 0, 0, .85);
font-size: 14px;
font-weight: 500;
margin-left: 8px;
}
}
.mobile .rank {
padding: 0 32px 32px 32px;
}
</style>
+145
View File
@@ -0,0 +1,145 @@
<template>
<div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
<i :class="show?'ri-close-line':'ri-settings-line'" />
</div>
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
import { addClass, removeClass } from '@/utils'
export default {
name: 'RightPanel',
props: {
clickNotClose: {
default: false,
type: Boolean
},
buttonTop: {
default: 250,
type: Number
}
},
data() {
return {
show: false
}
},
computed: {
theme() {
return this.$store.state.settings.theme
}
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
if (value) {
addClass(document.body, 'showRightPanel')
} else {
removeClass(document.body, 'showRightPanel')
}
}
},
mounted() {
this.insertToBody()
},
beforeUnmount() {
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.rightPanel')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
},
insertToBody() {
const elx = this.$refs.rightPanel
const body = document.querySelector('body')
body.insertBefore(elx, body.firstChild)
}
}
}
</script>
<style>
.showRightPanel {
overflow: hidden;
position: relative;
width: calc(100% - 15px);
}
</style>
<style lang="scss" scoped>
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
.rightPanel {
width: 100%;
max-width: 300px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
.show {
transition: all .3s cubic-bezier(.7, .3, .1, 1);
.rightPanel-background {
z-index: 20000;
opacity: 1;
width: 100%;
height: 100%;
}
.rightPanel {
transform: translate(0);
}
}
.handle-button {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>
@@ -0,0 +1,60 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'AppScreenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeUnmount() {
this.destroy()
},
methods: {
click() {
// if (!screenfull.enabled) {
// this.$message({
// message: 'you browser can not work',
// type: 'warning'
// })
// return false
// }
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.enabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.enabled) {
screenfull.off('change', this.change)
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>
@@ -0,0 +1,100 @@
<template>
<div :class="{active:isActive}" class="share-dropdown-menu">
<div class="share-dropdown-menu-wrapper">
<span class="share-dropdown-menu-title" @click.self="clickTitle">{{ title }}</span>
<div v-for="(item,index) of items" :key="index" class="share-dropdown-menu-item">
<a v-if="item.href" :href="item.href" target="_blank">{{ item.title }}</a>
<span v-else>{{ item.title }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: function() {
return []
}
},
title: {
type: String,
default: 'vue'
}
},
data() {
return {
isActive: false
}
},
methods: {
clickTitle() {
this.isActive = !this.isActive
}
}
}
</script>
<style lang="scss">
$n: 9; //和items.length 相同
$t: .1s;
.share-dropdown-menu {
width: 250px;
position: relative;
z-index: 1;
&-title {
width: 100%;
display: block;
cursor: pointer;
background: black;
color: white;
height: 60px;
line-height: 60px;
font-size: 20px;
text-align: center;
z-index: 2;
transform: translate3d(0,0,0);
}
&-wrapper {
position: relative;
}
&-item {
text-align: center;
position: absolute;
width: 100%;
background: #e0e0e0;
line-height: 60px;
height: 60px;
cursor: pointer;
font-size: 20px;
opacity: 1;
transition: transform 0.28s ease;
&:hover {
background: black;
color: white;
}
@for $i from 1 through $n {
&:nth-of-type(#{$i}) {
z-index: -1;
transition-delay: $i*$t;
transform: translate3d(0, -60px, 0);
}
}
}
&.active {
.share-dropdown-menu-wrapper {
z-index: 1;
}
.share-dropdown-menu-item {
@for $i from 1 through $n {
&:nth-of-type(#{$i}) {
transition-delay: ($n - $i)*$t;
transform: translate3d(0, ($i - 1)*60px, 0);
}
}
}
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" />
<svg v-else :class="svgClass" aria-hidden="true">
<use :href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>
@@ -0,0 +1,174 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#1890FF', '#F5222D', '#FA541C','#FAAD14','#13C2C2', '#52C460', '#2F54EB', '#722ED1', '#00b38a', '#2878FF']"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-plus/package.json').version // element-plus version from node_modules
const ORIGINAL_THEME = '#1890FF' // default color
export default {
name: 'ThemePickerComponent',
emits: ['change'],
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
const $message = this.$message({
message: '编译主题中',
customClass: 'theme-message',
type: 'success',
duration: 0,
iconClass: 'ri-loader-4-line'
})
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-plus@${version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
$message.close()
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>
+150
View File
@@ -0,0 +1,150 @@
<template>
<el-menu
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
>
<el-menu-item
v-for="(item, index) in visibleMenus"
:key="index"
:index="item.path"
><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item>
<!-- 顶部菜单超出数量折叠 -->
<el-sub-menu v-show="hiddenMenus.length > 0" index="more">
<template #title>更多菜单</template>
<el-menu-item
v-for="(item, index) in hiddenMenus"
:key="'h' + index"
:index="item.path"
><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script>
import { constantRoutes } from '@/router'
export default {
data() {
return {
// 顶部栏初始数
visibleNumber: 5,
// 是否为首次加载
isFrist: false
}
},
computed: {
// 顶部显示菜单
topMenus() {
return this.routers.map((menu) => ({
...menu,
children: undefined
}))
},
visibleMenus() {
return this.topMenus.slice(0, this.visibleNumber)
},
hiddenMenus() {
return this.topMenus.slice(this.visibleNumber)
},
// 所有的路由信息
routers() {
return this.$store.state.permission.topbarRouters
},
// 设置子路由
childrenMenus() {
var childrenMenus = []
this.routers.map((router) => {
for (var item in router.children) {
if (router.children[item].parentPath === undefined) {
router.children[item].parentPath = router.path
}
childrenMenus.push(router.children[item])
}
})
return constantRoutes.concat(childrenMenus)
},
// 默认激活的菜单
activeMenu() {
if (!this.routers || this.routers.length === 0) return ''
const path = this.$route.path
let activePath = this.routers[0].path
if (path.lastIndexOf('/') > 0) {
const tmpPath = path.substring(1, path.length)
activePath = '/' + tmpPath.substring(0, tmpPath.indexOf('/'))
} else if (path === '/index' || path === '') {
if (!this.isFrist) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.isFrist = true
} else {
activePath = 'index'
}
}
this.activeRoutes(activePath)
return activePath
}
},
mounted() {
this.setVisibleNumber()
},
methods: {
// 根据宽度计算设置显示栏数
setVisibleNumber() {
const width = document.body.getBoundingClientRect().width - 200
const elWidth = this.$el.getBoundingClientRect().width
const menuItemNodes = this.$el.children
const menuWidth = Array.from(menuItemNodes).map(
(i) => i.getBoundingClientRect().width
)
this.visibleNumber = (
parseInt(width - elWidth) / parseInt(menuWidth)
).toFixed(0)
},
// 菜单选择事件
handleSelect(key, keyPath) {
if (key.indexOf('http://') !== -1 || key.indexOf('https://') !== -1) {
// http(s):// 路径新窗口打开
window.open(key, '_blank')
} else {
this.activeRoutes(key)
}
},
// 当前激活的路由
activeRoutes(key) {
var routes = []
if (this.childrenMenus && this.childrenMenus.length > 0) {
this.childrenMenus.map((item) => {
if (key === item.parentPath || (key === 'index' && item.path === '')) {
routes.push(item)
}
})
}
// 有匹配结果才更新,避免空数组覆盖掉初始 sidebarRouters
if (routes.length > 0) {
this.$store.commit('permission/SET_SIDEBAR_ROUTERS', routes)
}
}
}
}
</script>
<style lang="scss" scoped>
.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px;
line-height: 50px;
margin: 0;
border-bottom: 3px solid transparent;
color: #999093;
padding: 0 5px;
margin: 0 10px;
}
.el-menu--horizontal > .el-menu-item.is-active {
border-bottom: 3px solid #409eff;
color: #303133;
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="chart-trend">
<slot name="term" />
<span>{{ rate }}%</span>
<span :class="[flag]">
<i :class="flag === 'top' ? 'ri-arrow-up-s-fill' : 'ri-arrow-down-s-fill'" />
</span>
</div>
</template>
<script>
export default {
name: 'TrendIndicator',
props: {
rate: {
type: String,
default: '',
required: true
},
flag: {
type: String,
default: '',
required: true
}
}
}
</script>
<style lang="scss" scoped>
.chart-trend {
display: inline-block;
font-size: 14px;
line-height: 22px;
.trend-icon {
font-size: 12px;
}
}
.top,
.bottom {
margin-left: 4px;
position: relative;
top: 1px;
width: 15px;
display: inline-block;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.top {
i{
color: #f5222d!important;
}
}
.bottom {
top: -1px;
i{
color: #52c41a!important;
}
}
</style>
@@ -0,0 +1,135 @@
<template>
<div class="upload-container">
<el-upload
:data="dataObj"
:multiple="false"
:show-file-list="false"
:on-success="handleImageSuccess"
class="image-uploader"
drag
action="https://httpbin.org/post"
>
<i class="ri-upload-line" />
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
</el-upload>
<div class="image-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl+'?imageView2/1/w/200/h/200'">
<div class="image-preview-action">
<i class="ri-delete-bin-line" @click="rmImage" />
</div>
</div>
</div>
</div>
</template>
<script>
// import { getToken } from '@/api/qiniu'
export default {
name: 'SingleImageUpload',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
tempUrl: '',
dataObj: { token: '', key: '' }
}
},
computed: {
imageUrl() {
return this.value
}
},
methods: {
rmImage() {
this.emitInput('')
},
emitInput(val) {
this.$emit('input', val)
},
handleImageSuccess() {
this.emitInput(this.tempUrl)
},
beforeUpload() {
// eslint-disable-next-line no-unused-vars
const _self = this
return new Promise((resolve, reject) => {
// getToken().then(response => {
// const key = response.data.qiniu_key
// const token = response.data.qiniu_token
// _self._data.dataObj.token = token
// _self._data.dataObj.key = key
// this.tempUrl = response.data.qiniu_url
// resolve(true)
// }).catch(err => {
// console.log(err)
// reject(false)
// })
})
}
}
}
</script>
<style lang="scss" scoped>
@use "@/styles/mixin.scss" as *;
.upload-container {
width: 100%;
position: relative;
@include clearfix;
.image-uploader {
width: 60%;
float: left;
}
.image-preview {
width: 200px;
height: 200px;
position: relative;
border: 1px dashed #d9d9d9;
float: left;
margin-left: 50px;
.image-preview-wrapper {
position: relative;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
.image-preview-action {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, .5);
transition: opacity .3s;
cursor: pointer;
text-align: center;
line-height: 200px;
.ri-delete-bin-line {
font-size: 36px;
}
}
&:hover {
.image-preview-action {
opacity: 1;
}
}
}
}
</style>
@@ -0,0 +1,131 @@
<template>
<div class="singleImageUpload2 upload-container">
<el-upload
:data="dataObj"
:multiple="false"
:show-file-list="false"
:on-success="handleImageSuccess"
class="image-uploader"
drag
action="https://httpbin.org/post"
>
<i class="ri-upload-line" />
<div class="el-upload__text">
Drag或<em>点击上传</em>
</div>
</el-upload>
<div v-show="imageUrl.length>0" class="image-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl">
<div class="image-preview-action">
<i class="ri-delete-bin-line" @click="rmImage" />
</div>
</div>
</div>
</div>
</template>
<script>
// import { getToken } from '@/api/qiniu'
export default {
name: 'SingleImageUpload2',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
tempUrl: '',
dataObj: { token: '', key: '' }
}
},
computed: {
imageUrl() {
return this.value
}
},
methods: {
rmImage() {
this.emitInput('')
},
emitInput(val) {
this.$emit('input', val)
},
handleImageSuccess() {
this.emitInput(this.tempUrl)
},
beforeUpload() {
// eslint-disable-next-line no-unused-vars
const _self = this
return new Promise((resolve, reject) => {
// getToken().then(response => {
// const key = response.data.qiniu_key
// const token = response.data.qiniu_token
// _self._data.dataObj.token = token
// _self._data.dataObj.key = key
// this.tempUrl = response.data.qiniu_url
// resolve(true)
// }).catch(() => {
// reject(false)
// })
})
}
}
}
</script>
<style lang="scss" scoped>
.upload-container {
width: 100%;
height: 100%;
position: relative;
.image-uploader {
height: 100%;
}
.image-preview {
width: 100%;
height: 100%;
position: absolute;
left: 0px;
top: 0px;
border: 1px dashed #d9d9d9;
.image-preview-wrapper {
position: relative;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
.image-preview-action {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, .5);
transition: opacity .3s;
cursor: pointer;
text-align: center;
line-height: 200px;
.ri-delete-bin-line {
font-size: 36px;
}
}
&:hover {
.image-preview-action {
opacity: 1;
}
}
}
}
</style>
@@ -0,0 +1,158 @@
<template>
<div class="upload-container">
<el-upload
:data="dataObj"
:multiple="false"
:show-file-list="false"
:on-success="handleImageSuccess"
class="image-uploader"
drag
action="https://httpbin.org/post"
>
<i class="ri-upload-line" />
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
</el-upload>
<div class="image-preview image-app-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl">
<div class="image-preview-action">
<i class="ri-delete-bin-line" @click="rmImage" />
</div>
</div>
</div>
<div class="image-preview">
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
<img :src="imageUrl">
<div class="image-preview-action">
<i class="ri-delete-bin-line" @click="rmImage" />
</div>
</div>
</div>
</div>
</template>
<script>
// import { getToken } from '@/api/qiniu'
export default {
name: 'SingleImageUpload3',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
tempUrl: '',
dataObj: { token: '', key: '' }
}
},
computed: {
imageUrl() {
return this.value
}
},
methods: {
rmImage() {
this.emitInput('')
},
emitInput(val) {
this.$emit('input', val)
},
handleImageSuccess(file) {
this.emitInput(file.files.file)
},
beforeUpload() {
// eslint-disable-next-line no-unused-vars
const _self = this
// return new Promise((resolve, reject) => {
// getToken().then(response => {
// const key = response.data.qiniu_key
// const token = response.data.qiniu_token
// _self._data.dataObj.token = token
// _self._data.dataObj.key = key
// this.tempUrl = response.data.qiniu_url
// resolve(true)
// }).catch(err => {
// console.log(err)
// reject(false)
// })
// })
}
}
}
</script>
<style lang="scss" scoped>
@use "@/styles/mixin.scss" as *;
.upload-container {
width: 100%;
position: relative;
@include clearfix;
.image-uploader {
width: 35%;
float: left;
}
.image-preview {
width: 200px;
height: 200px;
position: relative;
border: 1px dashed #d9d9d9;
float: left;
margin-left: 50px;
.image-preview-wrapper {
position: relative;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
.image-preview-action {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
cursor: default;
text-align: center;
color: #fff;
opacity: 0;
font-size: 20px;
background-color: rgba(0, 0, 0, .5);
transition: opacity .3s;
cursor: pointer;
text-align: center;
line-height: 200px;
.ri-delete-bin-line {
font-size: 36px;
}
}
&:hover {
.image-preview-action {
opacity: 1;
}
}
}
.image-app-preview {
width: 320px;
height: 180px;
position: relative;
border: 1px dashed #d9d9d9;
float: left;
margin-left: 50px;
.app-fake-conver {
height: 44px;
position: absolute;
width: 100%; // background: rgba(0, 0, 0, .1);
text-align: center;
line-height: 64px;
color: #fff;
}
}
}
</style>
+26
View File
@@ -0,0 +1,26 @@
<template>
<div class="app-container" :style="{ background:bg }">
<div class="app-container-section">
<slot name="main" />
</div>
</div>
</template>
<script>
export default {
name: 'Layout',
props: {
bg: {
type: String,
default: '#f0f1f5'
}
}
}
</script>
<style lang="scss" scoped>
.app-container{
padding: 10px;
background: #f0f1f5;
}
</style>