Initial commit

This commit is contained in:
admin
2025-12-09 17:52:52 +08:00
parent 602ce92418
commit 9292ef8d8d
12 changed files with 481 additions and 133 deletions

View File

@@ -36,6 +36,35 @@
</el-dropdown-menu>
</el-dropdown>
</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>
</template>
@@ -53,13 +82,17 @@ export default {
...mapGetters([
'sidebar',
'avatar',
'name'
'name',
'theme'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
setTheme(theme) {
this.$store.dispatch('settings/setTheme', theme)
},
async logout() {
await this.$store.dispatch('user/logout')
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>

View File

@@ -46,7 +46,7 @@ export default {
width: 100%;
height: 50px;
line-height: 50px;
background: #2b333e;
background: var(--menuBg);
text-align: center;
overflow: hidden;
@@ -64,7 +64,7 @@ export default {
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
color: var(--menuText);
font-weight: 600;
line-height: 50px;
font-size: 14px;
@@ -78,5 +78,5 @@ export default {
margin-right: 0px;
}
}
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="{'has-logo':showLogo}">
<div :class="{'has-logo':showLogo}" :style="cssVars">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
@@ -22,14 +22,14 @@
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
'sidebar',
'theme'
]),
activeMenu() {
const route = this.$route
@@ -43,8 +43,47 @@ export default {
showLogo() {
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() {
return variables
const vars = this.cssVars
return {
menuBg: vars['--menuBg'],
menuText: vars['--menuText'],
menuActiveText: vars['--menuActiveText']
}
},
isCollapse() {
return !this.sidebar.opened

View File

@@ -5,6 +5,7 @@ const getters = {
avatar: state => state.user.avatar,
name: state => state.user.name,
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

View File

@@ -5,7 +5,8 @@ const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
const state = {
showSettings: showSettings,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo
sidebarLogo: sidebarLogo,
theme: 'dark'
}
const mutations = {
@@ -13,12 +14,18 @@ const mutations = {
if (state.hasOwnProperty(key)) {
state[key] = value
}
},
SET_THEME: (state, theme) => {
state.theme = theme
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
},
setTheme({ commit }, theme) {
commit('SET_THEME', theme)
}
}

View File

@@ -10,7 +10,7 @@
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
background-color: var(--menuBg);
height: 100%;
position: fixed;
font-size: 0px;
@@ -67,21 +67,21 @@
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
background-color: var(--menuHover) !important;
}
}
.is-active>.el-submenu__title {
color: $subMenuActiveText !important;
color: var(--subMenuActiveText) !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
background-color: $subMenuBg !important;
background-color: var(--subMenuBg) !important;
&:hover {
background-color: $subMenuHover !important;
background-color: var(--subMenuHover) !important;
}
}
}

View File

@@ -25,6 +25,10 @@ export default {
chartData: {
type: Object,
default: () => ({ names: [], counts: [], title: '机构学员' })
},
theme: {
type: String,
default: 'dark'
}
},
data() {
@@ -32,6 +36,17 @@ export default {
chart: null
}
},
watch: {
theme() {
this.initChart()
},
chartData: {
deep: true,
handler() {
this.initChart()
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
@@ -46,17 +61,25 @@ export default {
},
methods: {
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')
const names = this.chartData.names || []
const counts = this.chartData.counts || []
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({
title: {
text: title,
textStyle: {
color: '#fff'
color: textColor
},
left: 'center'
},
@@ -80,11 +103,11 @@ export default {
alignWithLabel: true
},
axisLabel: {
color: '#fff'
color: axisLineColor
},
axisLine: {
lineStyle: {
color: '#fff'
color: axisLineColor
}
}
}],
@@ -95,16 +118,16 @@ export default {
show: false
},
axisLabel: {
color: '#fff'
color: axisLineColor
},
axisLine: {
lineStyle: {
color: '#fff'
color: axisLineColor
}
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
color: splitLineColor
}
}
}],

View File

@@ -29,6 +29,10 @@ export default {
chartData: {
type: Object,
required: true
},
theme: {
type: String,
default: 'dark'
}
},
data() {
@@ -37,6 +41,9 @@ export default {
}
},
watch: {
theme() {
this.setOptions(this.chartData)
},
chartData: {
deep: true,
handler(val) {
@@ -58,10 +65,17 @@ export default {
},
methods: {
initChart() {
if (this.chart) {
this.chart.dispose()
}
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
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({
xAxis: {
data: xAxis && xAxis.length ? xAxis : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
@@ -70,11 +84,11 @@ export default {
show: false
},
axisLabel: {
color: '#fff'
color: textColor
},
axisLine: {
lineStyle: {
color: '#fff'
color: textColor
}
}
},
@@ -97,23 +111,23 @@ export default {
show: false
},
axisLabel: {
color: '#fff'
color: textColor
},
axisLine: {
lineStyle: {
color: '#fff'
color: textColor
}
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
color: splitLineColor
}
}
},
legend: {
data: ['上周', '本周'],
textStyle: {
color: '#fff'
color: textColor
}
},
series: [{

View File

@@ -25,6 +25,10 @@ export default {
chartData: {
type: Array,
default: () => []
},
theme: {
type: String,
default: 'dark'
}
},
data() {
@@ -33,6 +37,9 @@ export default {
}
},
watch: {
theme() {
this.setOptions(this.chartData)
},
chartData: {
deep: true,
handler(val) {
@@ -54,6 +61,9 @@ export default {
},
methods: {
initChart() {
if (this.chart) {
this.chart.dispose()
}
this.chart = echarts.init(this.$el, 'macarons')
// Check if map is already registered
@@ -76,6 +86,29 @@ export default {
setOptions(data) {
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({
backgroundColor: 'transparent',
title: {
@@ -83,7 +116,7 @@ export default {
left: 'center',
top: 20,
textStyle: {
color: '#fff',
color: textColor,
fontSize: 18,
fontWeight: 'bold'
}
@@ -104,11 +137,11 @@ export default {
bottom: '50',
text: ['', ''],
textStyle: {
color: '#fff'
color: textColor
},
calculable: true,
inRange: {
color: ['#e0ffff', '#006edd']
color: visualMapColor
}
},
series: [
@@ -120,7 +153,7 @@ export default {
zoom: 1.2,
label: {
show: true,
color: '#fff',
color: textColor,
fontSize: 10,
formatter: function(params) {
if (params.value > 0) {
@@ -132,7 +165,7 @@ export default {
emphasis: {
label: {
show: true,
color: '#fff'
color: textColor
},
itemStyle: {
areaColor: '#fbc2eb',
@@ -141,8 +174,8 @@ export default {
}
},
itemStyle: {
areaColor: 'rgba(20, 41, 87, 0.6)',
borderColor: '#4facfe',
areaColor: areaColor,
borderColor: borderColor,
borderWidth: 1
},
data: data

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div :class="'theme-' + theme">
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel card-panel-blue" @click="handleSetLineChartData('students')">
@@ -104,11 +104,17 @@
<script>
import CountTo from 'vue-count-to'
import { mapGetters } from 'vuex'
export default {
components: {
CountTo
},
computed: {
...mapGetters([
'theme'
])
},
props: {
panelData: {
type: Object,
@@ -148,15 +154,59 @@ export default {
font-size: 12px;
position: relative;
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;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
display: flex;
align-items: center;
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);
&:hover {
@@ -170,73 +220,103 @@ export default {
}
.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);
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
}
.card-panel-icon {
float: left;
font-size: 32px;
fill: #fff !important; /* Force white icon */
color: #fff; /* Some svgs use color */
fill: #fff !important;
color: #fff;
}
.card-panel-description {
float: none;
font-weight: bold;
margin: 0;
margin-left: 20px;
flex: 1;
text-align: left;
.card-panel-text {
line-height: 18px;
color: rgba(255, 255, 255, 0.85);
font-size: 16px;
margin-bottom: 8px;
}
.card-panel-num {
font-size: 24px;
color: #fff;
font-weight: 600;
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%); }
}
}
/* Theme: Light & Orange */
.theme-light, .theme-orange {
.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;
}
}
/* Specific Gradient Backgrounds */
.card-panel-blue {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
.card-panel-icon-wrapper {
background: #f0f2f5; /* 稍微深一点的背景 */
transition: all 0.3s ease-out;
}
.card-panel-purple {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.card-panel-icon {
font-size: 32px;
fill: currentColor !important;
}
.card-panel-orange {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); /* Lighter peach */
/* Let's try stronger orange */
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
.card-panel-text {
color: rgba(0, 0, 0, 0.65); /* 加深文字颜色 */
font-weight: bold;
}
.card-panel-green {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
.card-panel-num {
color: #333; /* 加深数字颜色 */
}
.card-panel-red {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
/* 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-yellow {
background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%);
&.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%);
}
}

View File

@@ -29,6 +29,10 @@ export default {
legendData: {
type: Array,
default: () => []
},
theme: {
type: String,
default: 'dark'
}
},
data() {
@@ -36,6 +40,17 @@ export default {
chart: null
}
},
watch: {
theme() {
this.initChart()
},
chartData: {
deep: true,
handler() {
this.initChart()
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
@@ -50,14 +65,20 @@ export default {
},
methods: {
initChart() {
if (this.chart) {
this.chart.dispose()
}
this.chart = echarts.init(this.$el, 'macarons')
const isDark = this.theme === 'dark';
const textColor = isDark ? '#fff' : '#333';
this.chart.setOption({
title: {
text: '项目人数',
left: 'center',
textStyle: {
color: '#fff'
color: textColor
}
},
tooltip: {
@@ -69,7 +90,7 @@ export default {
bottom: '10',
data: this.legendData && this.legendData.map(item => item.name),
textStyle: {
color: '#fff'
color: textColor
}
},
series: [
@@ -85,12 +106,12 @@ export default {
label: {
show: true,
formatter: '{b}',
color: '#fff'
color: textColor
},
labelLine: {
show: true,
lineStyle: {
color: '#fff'
color: textColor
}
},
itemStyle: {

View File

@@ -1,5 +1,5 @@
<template>
<div class="dashboard-container">
<div class="dashboard-container" :class="'theme-' + theme">
<div v-if="loading" class="chart-wrapper">加载中...</div>
<div v-else>
<panel-group
@@ -10,18 +10,18 @@
<el-row :gutter="32">
<el-col :xs="24" :sm="24" :lg="12">
<div class="chart-wrapper">
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" />
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" :theme="theme" />
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="12">
<div class="chart-wrapper">
<bar-chart :chart-data="barChartData" />
<bar-chart :chart-data="barChartData" :theme="theme" />
</div>
</el-col>
</el-row>
<el-row class="map-chart-wrapper">
<map-chart :chart-data="mapChartData" />
<map-chart :chart-data="mapChartData" :theme="theme" />
</el-row>
<div v-if="error" class="chart-wrapper">{{ error }}</div>
</div>
@@ -29,8 +29,8 @@
</template>
<script>
import { mapGetters } from 'vuex'
import PanelGroup from './components/PanelGroup'
import LineChart from './components/LineChart'
import MapChart from './components/MapChart'
import PieChart from './components/PieChart'
import BarChart from './components/BarChart'
@@ -40,11 +40,15 @@ export default {
name: 'Dashboard',
components: {
PanelGroup,
LineChart,
MapChart,
PieChart,
BarChart
},
computed: {
...mapGetters([
'theme'
])
},
data() {
return {
loading: true,
@@ -110,10 +114,22 @@ export default {
<style lang="scss" scoped>
.dashboard-container {
padding: 32px;
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
min-height: 100vh;
position: relative;
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 */
@@ -143,6 +159,36 @@ export default {
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
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);
}
}
}
}