Initial commit
This commit is contained in:
@@ -36,6 +36,35 @@
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-switch-wrapper">
|
||||||
|
<div class="theme-switch">
|
||||||
|
<div
|
||||||
|
class="theme-item"
|
||||||
|
:class="{ active: theme === 'dark' }"
|
||||||
|
@click="setTheme('dark')"
|
||||||
|
>
|
||||||
|
<span class="color-dot dark-dot"></span>
|
||||||
|
深色
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="theme-item"
|
||||||
|
:class="{ active: theme === 'orange' }"
|
||||||
|
@click="setTheme('orange')"
|
||||||
|
>
|
||||||
|
<span class="color-dot orange-dot"></span>
|
||||||
|
橙色
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="theme-item"
|
||||||
|
:class="{ active: theme === 'light' }"
|
||||||
|
@click="setTheme('light')"
|
||||||
|
>
|
||||||
|
<span class="color-dot light-dot"></span>
|
||||||
|
浅白色
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -53,13 +82,17 @@ export default {
|
|||||||
...mapGetters([
|
...mapGetters([
|
||||||
'sidebar',
|
'sidebar',
|
||||||
'avatar',
|
'avatar',
|
||||||
'name'
|
'name',
|
||||||
|
'theme'
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleSideBar() {
|
toggleSideBar() {
|
||||||
this.$store.dispatch('app/toggleSideBar')
|
this.$store.dispatch('app/toggleSideBar')
|
||||||
},
|
},
|
||||||
|
setTheme(theme) {
|
||||||
|
this.$store.dispatch('settings/setTheme', theme)
|
||||||
|
},
|
||||||
async logout() {
|
async logout() {
|
||||||
await this.$store.dispatch('user/logout')
|
await this.$store.dispatch('user/logout')
|
||||||
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
|
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
|
||||||
@@ -150,5 +183,56 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-switch-wrapper {
|
||||||
|
float: right;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
.theme-switch {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
.theme-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #303133;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-dot { background: #1b2735; }
|
||||||
|
.orange-dot { background: linear-gradient(135deg, #f6d365 0%, #fda085 100%); }
|
||||||
|
.light-dot { background: #f0f2f5; border: 1px solid #ccc; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
background: #2b333e;
|
background: var(--menuBg);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
& .sidebar-title {
|
& .sidebar-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #fff;
|
color: var(--menuText);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{'has-logo':showLogo}">
|
<div :class="{'has-logo':showLogo}" :style="cssVars">
|
||||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||||
<el-menu
|
<el-menu
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import Logo from './Logo'
|
import Logo from './Logo'
|
||||||
import SidebarItem from './SidebarItem'
|
import SidebarItem from './SidebarItem'
|
||||||
import variables from '@/styles/variables.scss'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SidebarItem, Logo },
|
components: { SidebarItem, Logo },
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
'permission_routes',
|
'permission_routes',
|
||||||
'sidebar'
|
'sidebar',
|
||||||
|
'theme'
|
||||||
]),
|
]),
|
||||||
activeMenu() {
|
activeMenu() {
|
||||||
const route = this.$route
|
const route = this.$route
|
||||||
@@ -43,8 +43,47 @@ export default {
|
|||||||
showLogo() {
|
showLogo() {
|
||||||
return this.$store.state.settings.sidebarLogo
|
return this.$store.state.settings.sidebarLogo
|
||||||
},
|
},
|
||||||
|
cssVars() {
|
||||||
|
switch (this.theme) {
|
||||||
|
case 'orange':
|
||||||
|
return {
|
||||||
|
'--menuBg': '#3e2723',
|
||||||
|
'--menuHover': '#4e342e',
|
||||||
|
'--subMenuBg': '#4e342e',
|
||||||
|
'--subMenuHover': '#5d4037',
|
||||||
|
'--menuText': '#ffecb3',
|
||||||
|
'--menuActiveText': '#ff9800',
|
||||||
|
'--subMenuActiveText': '#ff9800'
|
||||||
|
}
|
||||||
|
case 'light':
|
||||||
|
return {
|
||||||
|
'--menuBg': '#ffffff',
|
||||||
|
'--menuHover': '#f5f5f5',
|
||||||
|
'--subMenuBg': '#ffffff',
|
||||||
|
'--subMenuHover': '#f5f5f5',
|
||||||
|
'--menuText': '#333333',
|
||||||
|
'--menuActiveText': '#409EFF',
|
||||||
|
'--subMenuActiveText': '#303133'
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
'--menuBg': '#2b333e',
|
||||||
|
'--menuHover': '#1f262f',
|
||||||
|
'--subMenuBg': '#1f262f',
|
||||||
|
'--subMenuHover': '#001528',
|
||||||
|
'--menuText': '#bfcbd9',
|
||||||
|
'--menuActiveText': '#409EFF',
|
||||||
|
'--subMenuActiveText': '#f4f4f5'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return variables
|
const vars = this.cssVars
|
||||||
|
return {
|
||||||
|
menuBg: vars['--menuBg'],
|
||||||
|
menuText: vars['--menuText'],
|
||||||
|
menuActiveText: vars['--menuActiveText']
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isCollapse() {
|
isCollapse() {
|
||||||
return !this.sidebar.opened
|
return !this.sidebar.opened
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const getters = {
|
|||||||
avatar: state => state.user.avatar,
|
avatar: state => state.user.avatar,
|
||||||
name: state => state.user.name,
|
name: state => state.user.name,
|
||||||
perms: state => state.user.perms,
|
perms: state => state.user.perms,
|
||||||
permission_routes: state => state.permission.routes
|
permission_routes: state => state.permission.routes,
|
||||||
|
theme: state => state.settings.theme
|
||||||
}
|
}
|
||||||
export default getters
|
export default getters
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
|
|||||||
const state = {
|
const state = {
|
||||||
showSettings: showSettings,
|
showSettings: showSettings,
|
||||||
fixedHeader: fixedHeader,
|
fixedHeader: fixedHeader,
|
||||||
sidebarLogo: sidebarLogo
|
sidebarLogo: sidebarLogo,
|
||||||
|
theme: 'dark'
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -13,12 +14,18 @@ const mutations = {
|
|||||||
if (state.hasOwnProperty(key)) {
|
if (state.hasOwnProperty(key)) {
|
||||||
state[key] = value
|
state[key] = value
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
SET_THEME: (state, theme) => {
|
||||||
|
state.theme = theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
changeSetting({ commit }, data) {
|
changeSetting({ commit }, data) {
|
||||||
commit('CHANGE_SETTING', data)
|
commit('CHANGE_SETTING', data)
|
||||||
|
},
|
||||||
|
setTheme({ commit }, theme) {
|
||||||
|
commit('SET_THEME', theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
transition: width 0.28s;
|
transition: width 0.28s;
|
||||||
width: $sideBarWidth !important;
|
width: $sideBarWidth !important;
|
||||||
background-color: $menuBg;
|
background-color: var(--menuBg);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
@@ -67,21 +67,21 @@
|
|||||||
.submenu-title-noDropdown,
|
.submenu-title-noDropdown,
|
||||||
.el-submenu__title {
|
.el-submenu__title {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $menuHover !important;
|
background-color: var(--menuHover) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-active>.el-submenu__title {
|
.is-active>.el-submenu__title {
|
||||||
color: $subMenuActiveText !important;
|
color: var(--subMenuActiveText) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .nest-menu .el-submenu>.el-submenu__title,
|
& .nest-menu .el-submenu>.el-submenu__title,
|
||||||
& .el-submenu .el-menu-item {
|
& .el-submenu .el-menu-item {
|
||||||
min-width: $sideBarWidth !important;
|
min-width: $sideBarWidth !important;
|
||||||
background-color: $subMenuBg !important;
|
background-color: var(--subMenuBg) !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $subMenuHover !important;
|
background-color: var(--subMenuHover) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export default {
|
|||||||
chartData: {
|
chartData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ names: [], counts: [], title: '机构学员' })
|
default: () => ({ names: [], counts: [], title: '机构学员' })
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'dark'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -32,6 +36,17 @@ export default {
|
|||||||
chart: null
|
chart: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
theme() {
|
||||||
|
this.initChart()
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.initChart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.initChart()
|
this.initChart()
|
||||||
@@ -46,17 +61,25 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initChart() {
|
initChart() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose() // Dispose old instance to apply new theme completely if needed, or just setOption
|
||||||
|
}
|
||||||
this.chart = echarts.init(this.$el, 'macarons')
|
this.chart = echarts.init(this.$el, 'macarons')
|
||||||
|
|
||||||
const names = this.chartData.names || []
|
const names = this.chartData.names || []
|
||||||
const counts = this.chartData.counts || []
|
const counts = this.chartData.counts || []
|
||||||
const title = this.chartData.title || '机构学员'
|
const title = this.chartData.title || '机构学员'
|
||||||
|
|
||||||
|
const isDark = this.theme === 'dark';
|
||||||
|
const textColor = isDark ? '#fff' : '#333';
|
||||||
|
const axisLineColor = isDark ? '#fff' : '#333';
|
||||||
|
const splitLineColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
this.chart.setOption({
|
this.chart.setOption({
|
||||||
title: {
|
title: {
|
||||||
text: title,
|
text: title,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
},
|
},
|
||||||
left: 'center'
|
left: 'center'
|
||||||
},
|
},
|
||||||
@@ -80,11 +103,11 @@ export default {
|
|||||||
alignWithLabel: true
|
alignWithLabel: true
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff'
|
color: axisLineColor
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff'
|
color: axisLineColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
@@ -95,16 +118,16 @@ export default {
|
|||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff'
|
color: axisLineColor
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff'
|
color: axisLineColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(255, 255, 255, 0.1)'
|
color: splitLineColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export default {
|
|||||||
chartData: {
|
chartData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'dark'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -37,6 +41,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
theme() {
|
||||||
|
this.setOptions(this.chartData)
|
||||||
|
},
|
||||||
chartData: {
|
chartData: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler(val) {
|
handler(val) {
|
||||||
@@ -58,10 +65,17 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initChart() {
|
initChart() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
}
|
||||||
this.chart = echarts.init(this.$el, 'macarons')
|
this.chart = echarts.init(this.$el, 'macarons')
|
||||||
this.setOptions(this.chartData)
|
this.setOptions(this.chartData)
|
||||||
},
|
},
|
||||||
setOptions({ expectedData, actualData, xAxis } = {}) {
|
setOptions({ expectedData, actualData, xAxis } = {}) {
|
||||||
|
const isDark = this.theme === 'dark';
|
||||||
|
const textColor = isDark ? '#fff' : '#333';
|
||||||
|
const splitLineColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
this.chart.setOption({
|
this.chart.setOption({
|
||||||
xAxis: {
|
xAxis: {
|
||||||
data: xAxis && xAxis.length ? xAxis : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
data: xAxis && xAxis.length ? xAxis : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||||
@@ -70,11 +84,11 @@ export default {
|
|||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,23 +111,23 @@ export default {
|
|||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(255, 255, 255, 0.1)'
|
color: splitLineColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: ['上周', '本周'],
|
data: ['上周', '本周'],
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export default {
|
|||||||
chartData: {
|
chartData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'dark'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -33,6 +37,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
theme() {
|
||||||
|
this.setOptions(this.chartData)
|
||||||
|
},
|
||||||
chartData: {
|
chartData: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler(val) {
|
handler(val) {
|
||||||
@@ -54,6 +61,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initChart() {
|
initChart() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
}
|
||||||
this.chart = echarts.init(this.$el, 'macarons')
|
this.chart = echarts.init(this.$el, 'macarons')
|
||||||
|
|
||||||
// Check if map is already registered
|
// Check if map is already registered
|
||||||
@@ -76,6 +86,29 @@ export default {
|
|||||||
setOptions(data) {
|
setOptions(data) {
|
||||||
if (!this.chart) return
|
if (!this.chart) return
|
||||||
|
|
||||||
|
const isDark = this.theme === 'dark';
|
||||||
|
const textColor = isDark ? '#fff' : '#333';
|
||||||
|
|
||||||
|
let areaColor, borderColor, visualMapColor;
|
||||||
|
|
||||||
|
switch (this.theme) {
|
||||||
|
case 'orange':
|
||||||
|
areaColor = '#fdf6ec';
|
||||||
|
borderColor = '#faecd8';
|
||||||
|
visualMapColor = ['#fdf6ec', '#e6a23c'];
|
||||||
|
break;
|
||||||
|
case 'light':
|
||||||
|
areaColor = '#f0f9eb';
|
||||||
|
borderColor = '#fff';
|
||||||
|
visualMapColor = ['#f0f9eb', '#67c23a'];
|
||||||
|
break;
|
||||||
|
default: // dark
|
||||||
|
areaColor = 'rgba(20, 41, 87, 0.6)';
|
||||||
|
borderColor = '#4facfe';
|
||||||
|
visualMapColor = ['#e0ffff', '#006edd'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this.chart.setOption({
|
this.chart.setOption({
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
title: {
|
title: {
|
||||||
@@ -83,7 +116,7 @@ export default {
|
|||||||
left: 'center',
|
left: 'center',
|
||||||
top: 20,
|
top: 20,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: textColor,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}
|
}
|
||||||
@@ -104,11 +137,11 @@ export default {
|
|||||||
bottom: '50',
|
bottom: '50',
|
||||||
text: ['高', '低'],
|
text: ['高', '低'],
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
},
|
},
|
||||||
calculable: true,
|
calculable: true,
|
||||||
inRange: {
|
inRange: {
|
||||||
color: ['#e0ffff', '#006edd']
|
color: visualMapColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
@@ -120,7 +153,7 @@ export default {
|
|||||||
zoom: 1.2,
|
zoom: 1.2,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
color: '#fff',
|
color: textColor,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
formatter: function(params) {
|
formatter: function(params) {
|
||||||
if (params.value > 0) {
|
if (params.value > 0) {
|
||||||
@@ -132,7 +165,7 @@ export default {
|
|||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
color: '#fff'
|
color: textColor
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
areaColor: '#fbc2eb',
|
areaColor: '#fbc2eb',
|
||||||
@@ -141,8 +174,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
areaColor: 'rgba(20, 41, 87, 0.6)',
|
areaColor: areaColor,
|
||||||
borderColor: '#4facfe',
|
borderColor: borderColor,
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
data: data
|
data: data
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div :class="'theme-' + theme">
|
||||||
<el-row :gutter="40" class="panel-group">
|
<el-row :gutter="40" class="panel-group">
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel card-panel-blue" @click="handleSetLineChartData('students')">
|
<div class="card-panel card-panel-blue" @click="handleSetLineChartData('students')">
|
||||||
@@ -104,11 +104,17 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CountTo from 'vue-count-to'
|
import CountTo from 'vue-count-to'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CountTo
|
CountTo
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'theme'
|
||||||
|
])
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
panelData: {
|
panelData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -148,15 +154,59 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
|
|
||||||
|
.card-panel-icon-wrapper {
|
||||||
|
float: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
transition: all 0.38s ease-out;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-panel-icon {
|
||||||
|
float: left;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-panel-description {
|
||||||
|
float: none;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 20px;
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.card-panel-text {
|
||||||
|
line-height: 18px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-panel-num {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme: Dark (Original Styles) */
|
||||||
|
.theme-dark {
|
||||||
|
.card-panel {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -170,73 +220,103 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-icon-wrapper {
|
.card-panel-icon-wrapper {
|
||||||
float: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 12px;
|
|
||||||
transition: all 0.38s ease-out;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-icon {
|
.card-panel-icon {
|
||||||
float: left;
|
fill: #fff !important;
|
||||||
font-size: 32px;
|
color: #fff;
|
||||||
fill: #fff !important; /* Force white icon */
|
|
||||||
color: #fff; /* Some svgs use color */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-description {
|
|
||||||
float: none;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: 20px;
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.card-panel-text {
|
.card-panel-text {
|
||||||
line-height: 18px;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-num {
|
.card-panel-num {
|
||||||
font-size: 24px;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 600;
|
|
||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Gradients - 使用 background-image 确保不被覆盖 */
|
||||||
|
&.card-panel-blue { background-image: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
|
||||||
|
&.card-panel-purple { background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||||
|
&.card-panel-orange { background-image: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
||||||
|
&.card-panel-green { background-image: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); }
|
||||||
|
&.card-panel-red { background-image: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
|
||||||
|
&.card-panel-yellow { background-image: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%); }
|
||||||
|
&.card-panel-pink { background-image: linear-gradient(135deg, #ff0844 0%, #ffb199 100%); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specific Gradient Backgrounds */
|
/* Theme: Light & Orange */
|
||||||
.card-panel-blue {
|
.theme-light, .theme-orange {
|
||||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
.card-panel {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,21,41,.08); /* 更柔和的阴影 */
|
||||||
|
border: 1px solid #e6ebf5; /* 增加边框 */
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px 0 rgba(0,0,0,.1);
|
||||||
|
border-color: #dcdfe6;
|
||||||
|
.card-panel-icon-wrapper {
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.card-panel-purple {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
}
|
||||||
.card-panel-orange {
|
|
||||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); /* Lighter peach */
|
.card-panel-icon-wrapper {
|
||||||
/* Let's try stronger orange */
|
background: #f0f2f5; /* 稍微深一点的背景 */
|
||||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
transition: all 0.3s ease-out;
|
||||||
}
|
}
|
||||||
.card-panel-green {
|
|
||||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
.card-panel-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
fill: currentColor !important;
|
||||||
}
|
}
|
||||||
.card-panel-red {
|
|
||||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
.card-panel-text {
|
||||||
|
color: rgba(0, 0, 0, 0.65); /* 加深文字颜色 */
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.card-panel-yellow {
|
.card-panel-num {
|
||||||
background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%);
|
color: #333; /* 加深数字颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific Colors */
|
||||||
|
&.card-panel-blue {
|
||||||
|
.card-panel-icon { color: #36a3f7; }
|
||||||
|
.card-panel-icon-wrapper { color: #e6f7ff; background: #e6f7ff; } /* 浅蓝背景 */
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #36a3f7; }
|
||||||
|
}
|
||||||
|
&.card-panel-purple {
|
||||||
|
.card-panel-icon { color: #667eea; }
|
||||||
|
.card-panel-icon-wrapper { color: #f2f6fc; background: #f2f6fc; }
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #667eea; }
|
||||||
|
}
|
||||||
|
&.card-panel-orange {
|
||||||
|
.card-panel-icon { color: #f5576c; }
|
||||||
|
.card-panel-icon-wrapper { color: #fef0f0; background: #fef0f0; }
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #f5576c; }
|
||||||
|
}
|
||||||
|
&.card-panel-green {
|
||||||
|
.card-panel-icon { color: #38f9d7; }
|
||||||
|
.card-panel-icon-wrapper { color: #f0f9eb; background: #f0f9eb; }
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #38f9d7; }
|
||||||
|
}
|
||||||
|
&.card-panel-red {
|
||||||
|
.card-panel-icon { color: #fa709a; }
|
||||||
|
.card-panel-icon-wrapper { color: #fef0f0; background: #fef0f0; }
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #fa709a; }
|
||||||
|
}
|
||||||
|
&.card-panel-yellow {
|
||||||
|
.card-panel-icon { color: #fbc2eb; }
|
||||||
|
.card-panel-icon-wrapper { color: #fdf6ec; background: #fdf6ec; }
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #fbc2eb; }
|
||||||
|
}
|
||||||
|
&.card-panel-pink {
|
||||||
|
.card-panel-icon { color: #ff0844; }
|
||||||
|
.card-panel-icon-wrapper { color: #fef0f0; background: #fef0f0; }
|
||||||
|
&:hover .card-panel-icon-wrapper { background: #ff0844; }
|
||||||
}
|
}
|
||||||
.card-panel-pink {
|
|
||||||
background: linear-gradient(135deg, #ff0844 0%, #ffb199 100%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export default {
|
|||||||
legendData: {
|
legendData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'dark'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -36,6 +40,17 @@ export default {
|
|||||||
chart: null
|
chart: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
theme() {
|
||||||
|
this.initChart()
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.initChart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.initChart()
|
this.initChart()
|
||||||
@@ -50,14 +65,20 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initChart() {
|
initChart() {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
}
|
||||||
this.chart = echarts.init(this.$el, 'macarons')
|
this.chart = echarts.init(this.$el, 'macarons')
|
||||||
|
|
||||||
|
const isDark = this.theme === 'dark';
|
||||||
|
const textColor = isDark ? '#fff' : '#333';
|
||||||
|
|
||||||
this.chart.setOption({
|
this.chart.setOption({
|
||||||
title: {
|
title: {
|
||||||
text: '项目人数',
|
text: '项目人数',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -69,7 +90,7 @@ export default {
|
|||||||
bottom: '10',
|
bottom: '10',
|
||||||
data: this.legendData && this.legendData.map(item => item.name),
|
data: this.legendData && this.legendData.map(item => item.name),
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
@@ -85,12 +106,12 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}',
|
formatter: '{b}',
|
||||||
color: '#fff'
|
color: textColor
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#fff'
|
color: textColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container" :class="'theme-' + theme">
|
||||||
<div v-if="loading" class="chart-wrapper">加载中...</div>
|
<div v-if="loading" class="chart-wrapper">加载中...</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<panel-group
|
<panel-group
|
||||||
@@ -10,18 +10,18 @@
|
|||||||
<el-row :gutter="32">
|
<el-row :gutter="32">
|
||||||
<el-col :xs="24" :sm="24" :lg="12">
|
<el-col :xs="24" :sm="24" :lg="12">
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" />
|
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" :theme="theme" />
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="24" :lg="12">
|
<el-col :xs="24" :sm="24" :lg="12">
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<bar-chart :chart-data="barChartData" />
|
<bar-chart :chart-data="barChartData" :theme="theme" />
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="map-chart-wrapper">
|
<el-row class="map-chart-wrapper">
|
||||||
<map-chart :chart-data="mapChartData" />
|
<map-chart :chart-data="mapChartData" :theme="theme" />
|
||||||
</el-row>
|
</el-row>
|
||||||
<div v-if="error" class="chart-wrapper">{{ error }}</div>
|
<div v-if="error" class="chart-wrapper">{{ error }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
import PanelGroup from './components/PanelGroup'
|
import PanelGroup from './components/PanelGroup'
|
||||||
import LineChart from './components/LineChart'
|
|
||||||
import MapChart from './components/MapChart'
|
import MapChart from './components/MapChart'
|
||||||
import PieChart from './components/PieChart'
|
import PieChart from './components/PieChart'
|
||||||
import BarChart from './components/BarChart'
|
import BarChart from './components/BarChart'
|
||||||
@@ -40,11 +40,15 @@ export default {
|
|||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
components: {
|
components: {
|
||||||
PanelGroup,
|
PanelGroup,
|
||||||
LineChart,
|
|
||||||
MapChart,
|
MapChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
BarChart
|
BarChart
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'theme'
|
||||||
|
])
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -110,10 +114,22 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: background 0.5s ease;
|
||||||
|
|
||||||
|
&.theme-dark {
|
||||||
|
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme-orange {
|
||||||
|
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme-light {
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
/* Subtle animated background elements could be added here if needed, but gradient is good for now */
|
/* Subtle animated background elements could be added here if needed, but gradient is good for now */
|
||||||
|
|
||||||
@@ -143,6 +159,36 @@ export default {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Overrides for Light Theme */
|
||||||
|
&.theme-light {
|
||||||
|
.chart-wrapper, .map-chart-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e6ebf5;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
border-color: #dcdfe6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Overrides for Orange Theme (optional, if we want glass effect to be different) */
|
||||||
|
&.theme-orange {
|
||||||
|
.chart-wrapper, .map-chart-wrapper {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user