Files
vueadmin/src/pages/home/index.vue
2026-01-20 21:21:39 +08:00

959 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 模式切换work 或 widget
const currentMode = ref('work')
const showCustomizeModal = ref(false)
// 工作台模式的快捷链接配置
const workLinks = ref([
{ id: 1, name: '用户管理', icon: '👥', route: '/system/user', enabled: true, color: '#1890ff' },
{ id: 2, name: '角色管理', icon: '👑', route: '/system/role', enabled: true, color: '#52c41a' },
{ id: 3, name: '菜单管理', icon: '📋', route: '/system/menu', enabled: true, color: '#fa8c16' },
{ id: 4, name: '系统设置', icon: '⚙️', route: '/system/setting', enabled: true, color: '#722ed1' },
{ id: 5, name: '文件上传', icon: '📤', route: '/upload', enabled: true, color: '#13c2c2' },
{ id: 6, name: '个人中心', icon: '👤', route: '#', enabled: false, color: '#eb2f96' },
])
// 组件模式的组件配置
const widgets = ref([
{ id: 1, name: '用户统计', icon: '📊', enabled: true, color: '#1890ff' },
{ id: 2, name: '数据图表', icon: '📈', enabled: true, color: '#52c41a' },
{ id: 3, name: '最近活动', icon: '🔔', enabled: true, color: '#fa8c16' },
{ id: 4, name: '快速操作', icon: '⚡', enabled: true, color: '#722ed1' },
{ id: 5, name: '系统状态', icon: '💻', enabled: false, color: '#13c2c2' },
{ id: 6, name: '待办事项', icon: '✅', enabled: false, color: '#eb2f96' },
])
// 临时配置(用于自定义弹窗)
const tempWorkLinks = ref([])
const tempWidgets = ref([])
// 计算属性:过滤出已启用的链接
const enabledWorkLinks = computed(() => workLinks.value.filter(link => link.enabled))
const enabledWidgets = computed(() => widgets.value.filter(widget => widget.enabled))
// 模式切换
const switchMode = (mode) => {
currentMode.value = mode
saveDashboardConfig()
}
// 打开自定义弹窗
const openCustomizeModal = () => {
showCustomizeModal.value = true
tempWorkLinks.value = JSON.parse(JSON.stringify(workLinks.value))
tempWidgets.value = JSON.parse(JSON.stringify(widgets.value))
}
// 关闭自定义弹窗
const closeCustomizeModal = () => {
showCustomizeModal.value = false
}
// 切换work链接状态
const toggleWorkLink = (id) => {
const link = tempWorkLinks.value.find(l => l.id === id)
if (link) {
link.enabled = !link.enabled
}
}
// 切换widget状态
const toggleWidget = (id) => {
const widget = tempWidgets.value.find(w => w.id === id)
if (widget) {
widget.enabled = !widget.enabled
}
}
// 保存自定义配置
const saveCustomConfig = () => {
workLinks.value = JSON.parse(JSON.stringify(tempWorkLinks.value))
widgets.value = JSON.parse(JSON.stringify(widgets.value))
saveDashboardConfig()
showCustomizeModal.value = false
}
// 导航到指定路由
const navigateTo = (route) => {
if (route && route !== '#') {
router.push(route)
}
}
// 保存配置到localStorage
const saveDashboardConfig = () => {
const config = {
mode: currentMode.value,
workLinks: workLinks.value,
widgets: widgets.value
}
localStorage.setItem('dashboardConfig', JSON.stringify(config))
}
// 从localStorage加载配置
const loadDashboardConfig = () => {
const saved = localStorage.getItem('dashboardConfig')
if (saved) {
try {
const config = JSON.parse(saved)
currentMode.value = config.mode || 'work'
workLinks.value = config.workLinks || workLinks.value
widgets.value = config.widgets || widgets.value
} catch (e) {
console.error('加载配置失败:', e)
}
}
}
// 组件挂载时加载配置
onMounted(() => {
loadDashboardConfig()
})
</script>
<template>
<div class="dashboard">
<!-- 头部区域 -->
<div class="dashboard-header">
<h1 class="page-title">仪表盘</h1>
<!-- 模式切换器 -->
<div class="mode-switcher">
<button
:class="['mode-btn', { active: currentMode === 'work' }]"
@click="switchMode('work')"
>
<span class="mode-icon">🏠</span>
<span>工作台</span>
</button>
<button
:class="['mode-btn', { active: currentMode === 'widget' }]"
@click="switchMode('widget')"
>
<span class="mode-icon">🧩</span>
<span>组件模式</span>
</button>
</div>
<!-- 自定义按钮 -->
<button class="customize-btn" @click="openCustomizeModal">
<span></span>
<span>自定义</span>
</button>
</div>
<!-- 工作台模式 -->
<div v-if="currentMode === 'work'" class="work-mode">
<div class="welcome-banner">
<div class="welcome-content">
<h2>欢迎回来</h2>
<p>今天也是高效工作的一天 🚀</p>
</div>
</div>
<div class="work-links-grid">
<div
v-for="link in enabledWorkLinks"
:key="link.id"
class="work-link-card"
:style="{ '--accent-color': link.color }"
@click="navigateTo(link.route)"
>
<div class="link-icon" :style="{ backgroundColor: link.color + '15', color: link.color }">
{{ link.icon }}
</div>
<div class="link-name">{{ link.name }}</div>
<div class="link-arrow"></div>
</div>
<div v-if="enabledWorkLinks.length === 0" class="empty-state">
<div class="empty-icon">📭</div>
<div class="empty-text">暂无快捷链接</div>
<div class="empty-desc">点击自定义按钮添加常用功能</div>
</div>
</div>
</div>
<!-- 组件模式 -->
<div v-if="currentMode === 'widget'" class="widget-mode">
<div class="widgets-grid">
<!-- 用户统计组件 -->
<div v-if="enabledWidgets.find(w => w.id === 1)" class="widget-card">
<div class="widget-header">
<div class="widget-title">
<span>📊</span>
用户统计
</div>
</div>
<div class="widget-body">
<div class="stat-row">
<div class="stat-item">
<div class="stat-label">总用户数</div>
<div class="stat-value">1,234</div>
</div>
<div class="stat-item">
<div class="stat-label">新增用户</div>
<div class="stat-value">+156</div>
</div>
</div>
</div>
</div>
<!-- 数据图表组件 -->
<div v-if="enabledWidgets.find(w => w.id === 2)" class="widget-card large">
<div class="widget-header">
<div class="widget-title">
<span>📈</span>
数据图表
</div>
</div>
<div class="widget-body">
<div class="chart-placeholder">
<div class="chart-bars">
<div class="chart-bar" style="height: 60%"></div>
<div class="chart-bar" style="height: 80%"></div>
<div class="chart-bar" style="height: 45%"></div>
<div class="chart-bar" style="height: 90%"></div>
<div class="chart-bar" style="height: 70%"></div>
<div class="chart-bar" style="height: 85%"></div>
<div class="chart-bar" style="height: 55%"></div>
</div>
<div class="chart-labels">
<span>周一</span>
<span>周二</span>
<span>周三</span>
<span>周四</span>
<span>周五</span>
<span>周六</span>
<span>周日</span>
</div>
</div>
</div>
</div>
<!-- 最近活动组件 -->
<div v-if="enabledWidgets.find(w => w.id === 3)" class="widget-card">
<div class="widget-header">
<div class="widget-title">
<span>🔔</span>
最近活动
</div>
</div>
<div class="widget-body">
<div class="activity-list">
<div class="activity-item">
<div class="activity-dot success"></div>
<div class="activity-content">
<div class="activity-text">新用户注册</div>
<div class="activity-time">5分钟前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot warning"></div>
<div class="activity-content">
<div class="activity-text">系统更新完成</div>
<div class="activity-time">30分钟前</div>
</div>
</div>
<div class="activity-item">
<div class="activity-dot info"></div>
<div class="activity-content">
<div class="activity-text">数据备份成功</div>
<div class="activity-time">1小时前</div>
</div>
</div>
</div>
</div>
</div>
<!-- 快速操作组件 -->
<div v-if="enabledWidgets.find(w => w.id === 4)" class="widget-card">
<div class="widget-header">
<div class="widget-title">
<span></span>
快速操作
</div>
</div>
<div class="widget-body">
<div class="quick-actions">
<button class="quick-action-btn"> 添加用户</button>
<button class="quick-action-btn">📤 上传文件</button>
<button class="quick-action-btn">📝 新建报告</button>
<button class="quick-action-btn">🔄 刷新数据</button>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="enabledWidgets.length === 0" class="empty-state">
<div class="empty-icon">🧩</div>
<div class="empty-text">暂无组件</div>
<div class="empty-desc">点击自定义按钮添加组件</div>
</div>
</div>
</div>
<!-- 自定义弹窗 -->
<div v-if="showCustomizeModal" class="modal-overlay" @click.self="closeCustomizeModal">
<div class="modal-content">
<div class="modal-header">
<h2>自定义仪表盘</h2>
<button class="modal-close" @click="closeCustomizeModal">×</button>
</div>
<div class="modal-body">
<!-- 工作台链接配置 -->
<div class="config-section">
<h3>工作台快捷链接</h3>
<div class="config-items">
<div
v-for="link in tempWorkLinks"
:key="link.id"
class="config-item"
>
<div class="item-info">
<span class="item-icon" :style="{ backgroundColor: link.color + '20', color: link.color }">
{{ link.icon }}
</span>
<span class="item-name">{{ link.name }}</span>
</div>
<label class="toggle-switch">
<input type="checkbox" v-model="link.enabled">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- 组件配置 -->
<div class="config-section">
<h3>组件显示</h3>
<div class="config-items">
<div
v-for="widget in tempWidgets"
:key="widget.id"
class="config-item"
>
<div class="item-info">
<span class="item-icon" :style="{ backgroundColor: widget.color + '20', color: widget.color }">
{{ widget.icon }}
</span>
<span class="item-name">{{ widget.name }}</span>
</div>
<label class="toggle-switch">
<input type="checkbox" v-model="widget.enabled">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeCustomizeModal">取消</button>
<button class="btn btn-primary" @click="saveCustomConfig">保存</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dashboard {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 头部样式 */
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #333;
margin: 0;
}
.mode-switcher {
display: flex;
gap: 8px;
background: #f5f5f5;
padding: 4px;
border-radius: 8px;
}
.mode-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
color: #666;
}
.mode-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.mode-btn.active {
background: white;
color: #1890ff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.mode-icon {
font-size: 16px;
}
.customize-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
}
.customize-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* 工作台模式样式 */
.welcome-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 32px;
margin-bottom: 24px;
color: white;
}
.welcome-content h2 {
font-size: 24px;
margin: 0 0 8px;
}
.welcome-content p {
font-size: 16px;
margin: 0;
opacity: 0.9;
}
.work-links-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.work-link-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
gap: 16px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid var(--accent-color);
}
.work-link-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.link-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.link-name {
flex: 1;
font-size: 16px;
font-weight: 600;
color: #333;
}
.link-arrow {
font-size: 20px;
color: var(--accent-color);
transition: transform 0.3s ease;
}
.work-link-card:hover .link-arrow {
transform: translateX(4px);
}
/* 组件模式样式 */
.widgets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
}
.widgets-grid .large {
grid-column: span 2;
}
.widget-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.widget-header {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #333;
}
.widget-body {
padding: 20px;
}
/* 统计组件 */
.stat-row {
display: flex;
gap: 24px;
}
.stat-item {
flex: 1;
}
.stat-label {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #333;
}
/* 图表组件 */
.chart-placeholder {
padding: 10px 0;
}
.chart-bars {
display: flex;
align-items: flex-end;
gap: 12px;
height: 120px;
margin-bottom: 12px;
}
.chart-bar {
flex: 1;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
border-radius: 4px 4px 0 0;
transition: height 0.3s ease;
}
.chart-labels {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
}
/* 活动列表 */
.activity-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.activity-item {
display: flex;
align-items: center;
gap: 12px;
}
.activity-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.activity-dot.success {
background: #52c41a;
}
.activity-dot.warning {
background: #fa8c16;
}
.activity-dot.info {
background: #1890ff;
}
.activity-content {
flex: 1;
}
.activity-text {
font-size: 14px;
color: #333;
margin-bottom: 2px;
}
.activity-time {
font-size: 12px;
color: #999;
}
/* 快速操作 */
.quick-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.quick-action-btn {
padding: 12px 16px;
background: #f8f9ff;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
color: #333;
}
.quick-action-btn:hover {
border-color: #667eea;
background: white;
}
/* 空状态 */
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: #fafafa;
border-radius: 12px;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #999;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
animation: fadeIn 0.2s ease;
}
.modal-content {
background: white;
border-radius: 16px;
width: 100%;
max-width: 600px;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h2 {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
.modal-close {
width: 32px;
height: 32px;
border: none;
background: #f5f5f5;
border-radius: 6px;
cursor: pointer;
font-size: 24px;
color: #666;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.modal-close:hover {
background: #e5e5e5;
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.config-section {
margin-bottom: 32px;
}
.config-section:last-child {
margin-bottom: 0;
}
.config-section h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 16px;
}
.config-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fafafa;
border-radius: 8px;
transition: background 0.3s ease;
}
.config-item:hover {
background: #f5f5f5;
}
.item-info {
display: flex;
align-items: center;
gap: 12px;
}
.item-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.item-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
/* 切换开关 */
.toggle-switch {
position: relative;
width: 48px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #d9d9d9;
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: '';
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(24px);
}
.modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
}
.btn-secondary:hover {
background: #e5e5e5;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.widgets-grid .large {
grid-column: span 1;
}
}
@media (max-width: 768px) {
.dashboard-header {
flex-direction: column;
align-items: stretch;
}
.page-title {
font-size: 22px;
}
.mode-switcher {
justify-content: center;
}
.customize-btn {
justify-content: center;
}
.work-links-grid {
grid-template-columns: 1fr;
}
.widgets-grid {
grid-template-columns: 1fr;
}
}
</style>