Merge remote-tracking branch 'origin/master'
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 19 KiB |
@@ -6,12 +6,21 @@
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { handleThemeStyle } from '@/utils/theme'
|
||||
import useProductStore from '@/store/modules/product'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const isDark = useDark()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 初始化主题样式
|
||||
console.log(isDark.value)
|
||||
if (!isDark.value) {
|
||||
useSettingsStore().toggleTheme()
|
||||
}
|
||||
handleThemeStyle(useSettingsStore().theme)
|
||||
useProductStore().fetchProductMap()
|
||||
if (getToken()) {
|
||||
useProductStore().fetchProductMap()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
BIN
gear-ui3/src/assets/images/back.jpg
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
gear-ui3/src/assets/images/back.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
gear-ui3/src/assets/images/right.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
gear-ui3/src/assets/images/right.png
Normal file
|
After Width: | Height: | Size: 978 KiB |
BIN
gear-ui3/src/assets/logo/logo-full.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,71 +1,151 @@
|
||||
<template>
|
||||
<div class="all-applications-container">
|
||||
<h3 class="title">全部应用</h3>
|
||||
<el-tabs v-model="activeTabName" class="app-tabs">
|
||||
<el-tab-pane
|
||||
v-for="menu in filteredMenus"
|
||||
:key="menu.path"
|
||||
:label="menu.meta?.title"
|
||||
:name="menu.path"
|
||||
>
|
||||
<div class="app-grid">
|
||||
<div
|
||||
v-for="child in menu.children"
|
||||
:key="child.path"
|
||||
class="app-item"
|
||||
@click="handleAppClick(menu, child)"
|
||||
>
|
||||
<!-- 图标区域 -->
|
||||
<div class="app-icon-wrapper">
|
||||
<svg-icon :icon-class="child.meta.icon || 'documentation'" class="app-icon" />
|
||||
</div>
|
||||
<!-- 文字区域 -->
|
||||
<span class="app-name">{{ child.meta?.title }}</span>
|
||||
|
||||
<!-- 三点菜单区域 -->
|
||||
<div class="app-actions">
|
||||
<el-dropdown
|
||||
trigger="hover"
|
||||
@click.native.stop
|
||||
@command="(command) => handleCommand(command, menu, child)"
|
||||
>
|
||||
<span class="actions-icon">
|
||||
<More />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="addFavorites">
|
||||
添加到常用
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="applications-wrapper">
|
||||
<!-- 一、业务功能区:常用应用 -->
|
||||
<div class="business-area">
|
||||
<!-- 常用应用标题栏 -->
|
||||
<div class="business-area-header">
|
||||
<h2 class="business-area-title">常用应用</h2>
|
||||
<button class="edit-btn" @click="showSettingDialog = true">
|
||||
<svg-icon icon-class="edit" /> 编辑常用
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 常用应用列表 -->
|
||||
<div class="business-modules">
|
||||
<div
|
||||
v-for="(module, index) in businessModules"
|
||||
:key="index"
|
||||
class="business-module"
|
||||
@click="handleLink(module.link)"
|
||||
>
|
||||
<div class="business-module-icon">
|
||||
<svg-icon :icon-class="module.icon" />
|
||||
</div>
|
||||
<h3 class="business-module-title">{{ module.title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二、全部应用区域(原功能保留) -->
|
||||
<div class="all-applications-container">
|
||||
<h3 class="title">全部应用</h3>
|
||||
<el-tabs v-model="activeTabName" class="app-tabs">
|
||||
<el-tab-pane
|
||||
v-for="menu in filteredMenus"
|
||||
:key="menu.path"
|
||||
:label="menu.meta?.title"
|
||||
:name="menu.path"
|
||||
>
|
||||
<div class="app-grid">
|
||||
<div
|
||||
v-for="child in menu.children"
|
||||
:key="child.path"
|
||||
class="app-item"
|
||||
@click="handleAppClick(menu, child)"
|
||||
>
|
||||
<!-- 图标区域 -->
|
||||
<div class="app-icon-wrapper">
|
||||
<svg-icon :icon-class="child.meta.icon || 'documentation'" class="app-icon" />
|
||||
</div>
|
||||
<!-- 文字区域 -->
|
||||
<span class="app-name">{{ child.meta?.title }}</span>
|
||||
|
||||
<!-- 三点菜单区域 -->
|
||||
<div class="app-actions">
|
||||
<el-dropdown
|
||||
trigger="hover"
|
||||
@click.native.stop
|
||||
@command="(command) => handleCommand(command, menu, child)"
|
||||
>
|
||||
<span class="actions-icon">
|
||||
<More />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="addFavorites">
|
||||
添加到常用
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 三、设置弹窗:编辑常用应用 -->
|
||||
<div class="dialog-mask" v-if="showSettingDialog">
|
||||
<div class="setting-dialog">
|
||||
<div class="dialog-header">
|
||||
<h3>编辑常用应用</h3>
|
||||
<button class="close-btn" @click="showSettingDialog = false">
|
||||
<svg-icon icon-class="close" />
|
||||
</button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="dialog-content">
|
||||
<div class="empty-tip" v-if="businessModules.length === 0">
|
||||
暂无常用应用,可从下方"全部应用"中添加
|
||||
</div>
|
||||
<ul class="module-list" v-else>
|
||||
<li v-for="(module, index) in businessModules" :key="index" class="module-item">
|
||||
<div class="module-info">
|
||||
<span class="module-index">{{ index + 1 }}</span>
|
||||
<span class="module-name">{{ module.title }}</span>
|
||||
</div>
|
||||
<div class="module-actions">
|
||||
<el-button
|
||||
@click="moveModule(index, 'up')"
|
||||
:disabled="index === 0"
|
||||
icon="ArrowUp"
|
||||
type="primary"
|
||||
/>
|
||||
<el-button
|
||||
@click="moveModule(index, 'down')"
|
||||
:disabled="index === businessModules.length - 1"
|
||||
icon="ArrowDown"
|
||||
type="primary"
|
||||
/>
|
||||
<el-button
|
||||
@click="deleteModule(index)"
|
||||
icon="Delete"
|
||||
danger
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button class="cancel-btn" @click="showSettingDialog = false">取消</button>
|
||||
<button class="confirm-btn" @click="showSettingDialog = false">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRouters } from '@/api/menu'
|
||||
import { More } from '@element-plus/icons-vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
export default {
|
||||
name: 'AllApplications',
|
||||
data() {
|
||||
return {
|
||||
allMenus: [],
|
||||
activeTabName: ''
|
||||
activeTabName: '',
|
||||
showSettingDialog: false, // 控制弹窗显示
|
||||
businessModules: [] // 常用应用列表(从localStorage同步)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 过滤隐藏菜单和无children的菜单
|
||||
filteredMenus() {
|
||||
const filterHidden = (menus) => {
|
||||
console.log(menus)
|
||||
return menus
|
||||
.filter(menu => menu.hidden !== true)
|
||||
.filter(menu => menu.hidden !== true && menu?.meta?.title !== '综合大屏')
|
||||
.map(menu => {
|
||||
if (menu.children) {
|
||||
menu.children = filterHidden(menu.children)
|
||||
@@ -73,95 +153,244 @@ export default {
|
||||
return menu
|
||||
})
|
||||
}
|
||||
const topLevelMenus = filterHidden(this.allMenus).filter(
|
||||
// 只保留有子菜单的顶层菜单
|
||||
return filterHidden(this.allMenus).filter(
|
||||
menu => menu.children && menu.children.length > 0
|
||||
)
|
||||
return topLevelMenus
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 1. 初始化菜单数据
|
||||
this.fetchMenus()
|
||||
|
||||
// 2. 初始化常用应用(持久化到localStorage)
|
||||
this.$businessModulesRef = useStorage('businessModules', [
|
||||
{ title: '考勤日历', icon: 'code', link: '/people/calendar' },
|
||||
{ title: '系统设置', icon: 'system', link: '/system/menu' }
|
||||
])
|
||||
// 同步ref值到data(确保Options API正常操作)
|
||||
this.businessModules = this.$businessModulesRef.value
|
||||
// 监听常用应用变化,同步到localStorage
|
||||
this.$watch('businessModules', (newVal) => {
|
||||
this.$businessModulesRef.value = newVal
|
||||
}, { deep: true })
|
||||
},
|
||||
methods: {
|
||||
// 1. 获取菜单数据
|
||||
fetchMenus() {
|
||||
getRouters().then(response => {
|
||||
this.allMenus = response.data
|
||||
// 默认激活第一个tab
|
||||
if (this.filteredMenus.length > 0) {
|
||||
this.activeTabName = this.filteredMenus[0].path
|
||||
}
|
||||
})
|
||||
},
|
||||
handleAppClick(parentMenu, childMenu) {
|
||||
const basePath = parentMenu.path
|
||||
const fullPath = `${basePath}/${childMenu.path}`
|
||||
this.$router.push(fullPath)
|
||||
},
|
||||
// 处理添加到常用功能
|
||||
handleAddToFavorites(parentMenu, childMenu) {
|
||||
// 构建完整的应用信息
|
||||
const appInfo = {
|
||||
parent: parentMenu,
|
||||
child: childMenu,
|
||||
fullPath: `${parentMenu.path}/${childMenu.path}`,
|
||||
timestamp: new Date().getTime()
|
||||
|
||||
// 2. 应用跳转(常用应用/全部应用通用)
|
||||
handleLink(link) {
|
||||
if (link.startsWith('http') || link.endsWith('.html')) {
|
||||
window.open(link, '_blank')
|
||||
} else {
|
||||
this.$router.push(link)
|
||||
}
|
||||
console.log(appInfo)
|
||||
// 向父组件发送事件
|
||||
this.$emit('addFavorites', appInfo)
|
||||
},
|
||||
|
||||
// 3. 全部应用卡片点击跳转
|
||||
handleAppClick(parentMenu, childMenu) {
|
||||
const fullPath = `${parentMenu.path}/${childMenu.path}`
|
||||
this.handleLink(fullPath)
|
||||
},
|
||||
|
||||
// 4. 处理三点菜单命令(添加到常用)
|
||||
handleCommand(command, parentMenu, childMenu) {
|
||||
if (command === 'addFavorites') {
|
||||
this.handleAddToFavorites(parentMenu, childMenu)
|
||||
}
|
||||
},
|
||||
|
||||
// 5. 添加/移除常用应用
|
||||
handleAddToFavorites(parentMenu, childMenu) {
|
||||
const fullPath = `${parentMenu.path}/${childMenu.path}`
|
||||
const existIndex = this.businessModules.findIndex(
|
||||
module => module.link === fullPath
|
||||
)
|
||||
|
||||
// 已存在则移除,不存在则添加
|
||||
if (existIndex !== -1) {
|
||||
this.businessModules.splice(existIndex, 1)
|
||||
} else {
|
||||
this.businessModules.push({
|
||||
title: childMenu.meta.title,
|
||||
icon: childMenu.meta.icon,
|
||||
link: fullPath
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 6. 调整常用应用排序
|
||||
moveModule(index, direction) {
|
||||
const newModules = [...this.businessModules]
|
||||
// 上移:与前一个交换
|
||||
if (direction === 'up' && index > 0) {
|
||||
[newModules[index], newModules[index - 1]] = [newModules[index - 1], newModules[index]]
|
||||
}
|
||||
// 下移:与后一个交换
|
||||
if (direction === 'down' && index < newModules.length - 1) {
|
||||
[newModules[index], newModules[index + 1]] = [newModules[index + 1], newModules[index]]
|
||||
}
|
||||
// 触发watch同步到localStorage
|
||||
this.businessModules = newModules
|
||||
},
|
||||
|
||||
// 7. 删除常用应用
|
||||
deleteModule(index) {
|
||||
if (confirm('确定要移除该常用应用吗?')) {
|
||||
this.businessModules.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 外层容器统一样式:深色背景 */
|
||||
.applications-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 一、常用应用区域样式 */
|
||||
.business-area {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* 常用应用标题栏 */
|
||||
.business-area-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.business-area-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #E5E7EB;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: #2C2C3C;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #E5E7EB;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #3A3A4A;
|
||||
color: #F9FAFB;
|
||||
}
|
||||
}
|
||||
|
||||
/* 常用应用列表 */
|
||||
.business-modules {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.business-module {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: #2C2C3C;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-2px);
|
||||
background: #3A3A4A;
|
||||
}
|
||||
}
|
||||
|
||||
.business-module-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
background: #374151; /* 可根据图标色调整背景 */
|
||||
|
||||
:deep(svg) {
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.business-module-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #E5E7EB;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 二、全部应用区域样式 */
|
||||
.all-applications-container {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
background: #1E1E2E;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #303133;
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
/* 应用列表容器改为 flex 横向布局 */
|
||||
/* 应用列表网格 */
|
||||
.app-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
gap: 16px;
|
||||
padding-top: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 每个应用 item 样式 */
|
||||
/* 应用卡片 */
|
||||
.app-item {
|
||||
display: flex;
|
||||
flex-direction: row; /* 横向排列 */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
width: 160px;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); /* 初始阴影更浅 */
|
||||
position: relative; /* 为三点菜单定位做准备 */
|
||||
background-color: #2C2C3C;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); /* 悬停加深阴影 */
|
||||
transform: translateY(-2px); /* 轻微上浮增强交互感 */
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-2px);
|
||||
background-color: #3A3A4A;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图标区域 */
|
||||
/* 应用图标容器 */
|
||||
.app-icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -169,39 +398,36 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px; /* 与文字保持间距 */
|
||||
background-color: #f5f7fa; /* 浅背景突出图标 */
|
||||
margin-right: 12px;
|
||||
background-color: #374151;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
/* hover 时图标区域背景变化 */
|
||||
.app-item:hover & {
|
||||
background-color: #e6f0ff;
|
||||
background-color: #4B5563;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.app-icon {
|
||||
font-size: 24px;
|
||||
color: #409eff;
|
||||
font-size: 24px;
|
||||
color: #F3F4F6;
|
||||
}
|
||||
|
||||
/* 应用名称样式 */
|
||||
/* 应用名称 */
|
||||
.app-name {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
flex: 1; /* 占满剩余空间,让三点菜单靠右 */
|
||||
color: #E5E7EB;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 三点菜单样式 */
|
||||
/* 三点菜单 */
|
||||
.app-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
opacity: 0; /* 默认隐藏 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* 鼠标悬停在卡片上时显示三点菜单 */
|
||||
.app-item:hover .app-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -214,29 +440,201 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
/* 三、设置弹窗样式 */
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.setting-dialog {
|
||||
width: 500px;
|
||||
background: #2C2C3C;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #3A3A4A;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #9CA3AF;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: #F9FAFB;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 24px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: #2C2C3C;
|
||||
}
|
||||
|
||||
/* 空状态提示 */
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #9CA3AF;
|
||||
font-size: 14px;
|
||||
background: #374151;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 常用应用编辑列表 */
|
||||
.module-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #3A3A4A;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.module-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.module-index {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #374151;
|
||||
color: #E5E7EB;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size: 16px;
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
.module-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 弹窗底部按钮 */
|
||||
.dialog-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #3A3A4A;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
background: #2C2C3C;
|
||||
}
|
||||
|
||||
.cancel-btn, .confirm-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Tabs 样式优化 */
|
||||
.cancel-btn {
|
||||
background: #374151;
|
||||
color: #E5E7EB;
|
||||
|
||||
&:hover {
|
||||
background: #4B5563;
|
||||
color: #F9FAFB;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: #3B82F6;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
/* 四、Element组件样式穿透调整 */
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px;
|
||||
background-color: #3A3A4A;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 15px;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #E5E7EB;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式调整 */
|
||||
:deep(.el-dropdown-menu) {
|
||||
min-width: 120px;
|
||||
background-color: #2C2C3C;
|
||||
border: 1px solid #3A3A4A;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-item) {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
color: #E5E7EB;
|
||||
|
||||
&:hover {
|
||||
background-color: #3A3A4A;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
padding: 6px 12px;
|
||||
color: #E5E7EB;
|
||||
background-color: #374151;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #4B5563;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,16 +10,16 @@
|
||||
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
|
||||
<el-tooltip content="主题模式" effect="dark" placement="bottom">
|
||||
<!-- <el-tooltip content="主题模式" effect="dark" placement="bottom">
|
||||
<div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
|
||||
<svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
|
||||
<svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</el-tooltip> -->
|
||||
|
||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<!-- <el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
</el-tooltip> -->
|
||||
</template>
|
||||
|
||||
<el-dropdown @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover">
|
||||
|
||||
214
gear-ui3/src/views/dashboard/components/RecentOrdersTable.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="orders-table-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">最近订单列表</h3>
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索订单编号/客户名称"
|
||||
size="small"
|
||||
:prefix-icon="Search"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</div>
|
||||
<el-table
|
||||
:data="paginatedOrders"
|
||||
border
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#0a0f1a', color: '#e0e7ff' }"
|
||||
:row-style="{ background: '#0a0f1a' }"
|
||||
:row-class-name="stripeRowClassName"
|
||||
:show-header="false"
|
||||
>
|
||||
<el-table-column prop="orderCode" label="订单编号" />
|
||||
<el-table-column prop="customerId" label="客户" />
|
||||
<el-table-column prop="salesManager" label="销售经理" />
|
||||
<el-table-column
|
||||
prop="taxAmount"
|
||||
label="订单金额"
|
||||
align="right"
|
||||
:formatter="(row) => `¥${Number(row.taxAmount).toFixed(2)}`"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderStatus"
|
||||
label="订单状态"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<dict-tag :value="scope.row.orderStatus" :options="order_status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="操作" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="viewOrderDetail(scope.row)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
</el-table>
|
||||
<!-- <el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="totalOrders"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="mt-4 flex justify-end"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { order_status } = proxy.useDict('order_status');
|
||||
|
||||
const props = defineProps({
|
||||
orders: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
// 表格状态
|
||||
const searchKeyword = ref('');
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
// 计算属性:筛选与分页
|
||||
const totalOrders = computed(() => props.orders.length);
|
||||
const filteredOrders = computed(() => {
|
||||
return props.orders.filter(order =>
|
||||
order.orderCode.includes(searchKeyword.value) ||
|
||||
(order.customerName || '').includes(searchKeyword.value)
|
||||
);
|
||||
});
|
||||
const paginatedOrders = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value;
|
||||
return filteredOrders.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
|
||||
// 斑马纹行类名(增强可读性)
|
||||
const stripeRowClassName = ({ rowIndex }) => {
|
||||
return rowIndex % 2 === 1 ? 'stripe-row' : '';
|
||||
};
|
||||
|
||||
// 分页事件
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val;
|
||||
};
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val;
|
||||
};
|
||||
|
||||
// 查看订单详情
|
||||
const viewOrderDetail = (row) => {
|
||||
ElMessage.info(`查看订单 ${row.orderCode} 详情`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 卡片容器:深黑色背景 + 立体阴影 */
|
||||
.orders-table-card {
|
||||
background: #0a0f1a; /* 纯黑底色,可根据需求调深/浅 */
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); /* 增强阴影,突出层次 */
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 卡片头部:标题 + 搜索框布局 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f8fafc; /* 亮白色文字 */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 🔵 输入框样式覆盖(黑色主题) */
|
||||
.el-input {
|
||||
--el-input-bg-color: #0a0f1a; /* 输入框背景 */
|
||||
--el-input-border-color: #1e293b; /* 边框色 */
|
||||
--el-input-hover-border-color: #3b82f6; /* hover 时边框变蓝,增强交互 */
|
||||
--el-input-text-color: #f8fafc; /* 文字色 */
|
||||
--el-input-placeholder-color: #94a3b8;/* 占位符色 */
|
||||
}
|
||||
|
||||
/* 🔵 表格样式覆盖(黑色主题) */
|
||||
.el-table {
|
||||
--el-table-bg-color: #0a0f1a; /* 表格整体背景 */
|
||||
--el-table-text-color: #f8fafc; /* 单元格文字色 */
|
||||
--el-table-header-text-color: #e0e7ff;/* 表头文字色(更亮,突出表头) */
|
||||
--el-table-border-color: #1e293b; /* 边框色 */
|
||||
--el-table-row-hover-bg-color: #1e293b; /* hover 行背景(浅一级黑色) */
|
||||
}
|
||||
|
||||
/* 表头单元格:强化背景与边框 */
|
||||
.el-table th {
|
||||
border-bottom: 1px solid #1e293b !important;
|
||||
background-color: #0a0f1a !important;
|
||||
}
|
||||
|
||||
/* 内容单元格:边框样式 */
|
||||
.el-table td {
|
||||
border-bottom: 1px solid #1e293b !important;
|
||||
}
|
||||
|
||||
/* 斑马纹行:交替行背景(更浅的黑色,增强可读性) */
|
||||
.stripe-row {
|
||||
background-color: #0f172a !important;
|
||||
}
|
||||
|
||||
/* 选中行:背景色( hover 同色,保持统一) */
|
||||
.el-table__body tr.current-row > td {
|
||||
background-color: #1e293b !important;
|
||||
}
|
||||
|
||||
/* 🔵 Tag 组件样式覆盖(黑色主题) */
|
||||
.el-tag {
|
||||
--el-tag-bg-color: #0a0f1a;
|
||||
--el-tag-border-color: #1e293b;
|
||||
--el-tag-text-color: #f8fafc;
|
||||
--el-tag-hover-bg-color: #1e293b;
|
||||
}
|
||||
|
||||
/* 🔵 分页组件样式覆盖(黑色主题) */
|
||||
.el-pagination {
|
||||
--el-pagination-font-size: 14px;
|
||||
--el-pagination-text-color: #f8fafc;
|
||||
--el-pagination-disabled-color: #64748b;
|
||||
--el-pagination-item-bg-color: #0a0f1a;
|
||||
--el-pagination-item-border-color: #1e293b;
|
||||
--el-pagination-item-hover-bg-color: #1e293b;
|
||||
--el-pagination-item-hover-border-color: #3b82f6;
|
||||
--el-pagination-item-active-bg-color: #3b82f6; /* 激活页签用蓝色突出 */
|
||||
--el-pagination-item-active-border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* 🔵 滚动条美化(可选) */
|
||||
.el-table__body-wrapper::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.el-table__body-wrapper::-webkit-scrollbar-track {
|
||||
background: #0a0f1a;
|
||||
}
|
||||
.el-table__body-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #1e293b;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #334155;
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,8 @@ const props = defineProps({
|
||||
isRefreshing: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
console.log(props.orders, '客户订单汇总')
|
||||
|
||||
// 图表容器引用
|
||||
const barChartRef = ref(null);
|
||||
const radarChartRef = ref(null);
|
||||
|
||||
119
gear-ui3/src/views/dashboard/components/SalesMetricsCard.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="metrics-card">
|
||||
<div class="metrics-grid">
|
||||
<!-- 总客户数 -->
|
||||
<div class="metric-item">
|
||||
<div class="metric-value">{{ totalCustomers }}</div>
|
||||
<div class="metric-label">总客户数</div>
|
||||
<div class="metric-trend" :class="customerTrendClass">
|
||||
<el-icon :size="14"><ArrowUp /></el-icon> {{ customerTrend }}%
|
||||
</div>
|
||||
</div>
|
||||
<!-- 总订单数 -->
|
||||
<div class="metric-item">
|
||||
<div class="metric-value">{{ totalOrders }}</div>
|
||||
<div class="metric-label">总订单数</div>
|
||||
<div class="metric-trend" :class="orderTrendClass">
|
||||
<el-icon :size="14"><ArrowUp /></el-icon> {{ orderTrend }}%
|
||||
</div>
|
||||
</div>
|
||||
<!-- 总销售额 -->
|
||||
<div class="metric-item">
|
||||
<div class="metric-value">¥{{ totalSales.toFixed(2) }}</div>
|
||||
<div class="metric-label">总销售额</div>
|
||||
<div class="metric-trend" :class="salesTrendClass">
|
||||
<el-icon :size="14"><ArrowUp /></el-icon> {{ salesTrend }}%
|
||||
</div>
|
||||
</div>
|
||||
<!-- 退换货率 -->
|
||||
<div class="metric-item">
|
||||
<div class="metric-value">{{ returnRate.toFixed(2) }}%</div>
|
||||
<div class="metric-label">退换货率</div>
|
||||
<div class="metric-trend" :class="returnRateTrendClass">
|
||||
<el-icon :size="14"><ArrowDown /></el-icon> {{ returnRateTrend }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
customers: { type: Array, default: () => [] },
|
||||
orders: { type: Array, default: () => [] },
|
||||
returnExchanges: { type: Array, default: () => [] },
|
||||
// 趋势数据(模拟,可从接口扩展)
|
||||
customerTrend: { type: Number, default: 5.2 },
|
||||
orderTrend: { type: Number, default: 8.7 },
|
||||
salesTrend: { type: Number, default: 12.3 },
|
||||
returnRateTrend: { type: Number, default: -2.1 }
|
||||
});
|
||||
|
||||
// 计算核心指标
|
||||
const totalCustomers = computed(() => props.customers.length);
|
||||
const totalOrders = computed(() => props.orders.length);
|
||||
const totalSales = computed(() => {
|
||||
return props.orders.reduce((sum, order) => sum + Number(order.taxAmount || 0), 0);
|
||||
});
|
||||
const returnRate = computed(() => {
|
||||
if (totalOrders.value === 0) return 0;
|
||||
return (props.returnExchanges.length / totalOrders.value) * 100;
|
||||
});
|
||||
|
||||
// 趋势样式(升序绿色,降序红色)
|
||||
const customerTrendClass = computed(() => props.customerTrend > 0 ? 'up-trend' : 'down-trend');
|
||||
const orderTrendClass = computed(() => props.orderTrend > 0 ? 'up-trend' : 'down-trend');
|
||||
const salesTrendClass = computed(() => props.salesTrend > 0 ? 'up-trend' : 'down-trend');
|
||||
const returnRateTrendClass = computed(() => props.returnRateTrend > 0 ? 'up-trend' : 'down-trend');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.metrics-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.up-trend {
|
||||
color: #10b981; /* 绿色:上升趋势 */
|
||||
}
|
||||
|
||||
.down-trend {
|
||||
color: #ef4444; /* 红色:下降趋势 */
|
||||
}
|
||||
</style>
|
||||
212
gear-ui3/src/views/dashboard/grid/ChartLayoutPreview.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<!-- ChartLayoutPreview.vue -->
|
||||
<template>
|
||||
<div class="chart-layout-preview">
|
||||
<!-- 预览标题 -->
|
||||
<h3 class="preview-title">
|
||||
布局预览(同比缩放:{{ scaleRatio * 100 }}%)
|
||||
</h3>
|
||||
|
||||
<!-- 预览容器:通过transform实现同比缩小 -->
|
||||
<div
|
||||
class="preview-wrapper"
|
||||
:style="{
|
||||
transform: `scale(${scaleRatio})`,
|
||||
transformOrigin: 'top left', // 缩放原点:左上角(保证布局对齐)
|
||||
width: `${100 / scaleRatio}%`, // 反向计算宽度,避免缩放后内容溢出
|
||||
height: `${100 / scaleRatio}%` // 反向计算高度,配合overflow隐藏
|
||||
}"
|
||||
>
|
||||
<!-- 实际预览内容(与原逻辑一致,用el-row+el-col实现布局) -->
|
||||
<div class="preview-content">
|
||||
<!-- 空状态提示 -->
|
||||
<div v-if="!chartConfigs?.length" class="empty-preview">
|
||||
<el-empty description="暂无图表配置,无法预览" />
|
||||
</div>
|
||||
|
||||
<!-- 图表布局预览 -->
|
||||
<el-row
|
||||
v-else
|
||||
:gutter="16"
|
||||
class="chart-preview-row"
|
||||
>
|
||||
<el-col
|
||||
v-for="(chart, index) in chartConfigs"
|
||||
:key="chart.id || `chart-${index}`"
|
||||
:xs="chart.layout.xs"
|
||||
:sm="chart.layout.sm"
|
||||
:md="chart.layout.md"
|
||||
:lg="chart.layout.lg"
|
||||
:xl="chart.layout.xl"
|
||||
class="chart-preview-col"
|
||||
>
|
||||
<!-- 模拟图表卡片(高度同步配置) -->
|
||||
<el-card
|
||||
class="chart-preview-card"
|
||||
:style="{
|
||||
height: `${chart.height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'all 0.3s ease'
|
||||
}"
|
||||
>
|
||||
<!-- 卡片头部:图表名称+标识 -->
|
||||
<div class="card-header">
|
||||
<span class="chart-title">{{ chart.title }}</span>
|
||||
<span class="chart-id">[{{ chart.id }}]</span>
|
||||
</div>
|
||||
|
||||
<!-- 模拟图表内容区 -->
|
||||
<div class="chart-placeholder" :style="{ flex: 1, marginTop: '8px' }">
|
||||
<div class="placeholder-bg"></div>
|
||||
<!-- 配置信息提示 -->
|
||||
<div class="placeholder-info">
|
||||
<p>高度:{{ chart.height }}px</p>
|
||||
<p>布局:xs={{ chart.layout.xs }} / sm={{ chart.layout.sm }} / md={{ chart.layout.md }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElEmpty, ElRow, ElCol, ElCard } from 'element-plus';
|
||||
|
||||
// 组件Props:接收外部传递的配置数据和缩放比例
|
||||
const props = defineProps({
|
||||
/** 图表配置数组(与父组件tempChartConfigs结构一致) */
|
||||
chartConfigs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
/** 同比缩放比例(0~1之间,默认0.5即缩小至50%) */
|
||||
scaleRatio: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0.5,
|
||||
validator: (val) => val > 0 && val <= 1 // 限制缩放比例为0~1(避免放大导致溢出)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 组件外层容器:控制整体大小和溢出 */
|
||||
.chart-layout-preview {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 预览标题 */
|
||||
.preview-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 缩放容器:核心缩放逻辑载体 */
|
||||
.preview-wrapper {
|
||||
position: relative;
|
||||
/* 初始宽度100%,通过style绑定反向计算实际宽度(抵消scale缩小效果) */
|
||||
transform: scale(0.5);
|
||||
transform-origin: top left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 实际预览内容容器 */
|
||||
.preview-content {
|
||||
padding: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-preview {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 图表行:清除el-row默认margin */
|
||||
.chart-preview-row {
|
||||
margin: 0 !important;
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
|
||||
/* 图表列:控制间距 */
|
||||
.chart-preview-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 图表卡片:模拟实际图表容器 */
|
||||
.chart-preview-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 卡片头部:名称+标识 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
color: white;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chart-id {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 模拟图表内容区 */
|
||||
.chart-placeholder {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholder-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 配置信息提示:底部半透明悬浮 */
|
||||
.placeholder-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.placeholder-info p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* 响应式调整:小屏幕下优化缩放体验 */
|
||||
@media (max-width: 768px) {
|
||||
.chart-layout-preview {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,181 +1,185 @@
|
||||
<template>
|
||||
<!-- 图表容器(含滚动) -->
|
||||
<div class="charts-container">
|
||||
<!-- Element 布局:el-row 为行容器,gutter 控制间距 -->
|
||||
<el-row
|
||||
class="charts-row"
|
||||
:gutter="20"
|
||||
:style="{ height: `calc(100% - 40px)` }"
|
||||
>
|
||||
<!-- 动态渲染图表:遍历持久化后的配置数组 -->
|
||||
<el-col
|
||||
v-for="(chartConfig, index) in persistedChartConfigs"
|
||||
:key="chartConfig.id"
|
||||
class="chart-col"
|
||||
:xs="chartConfig.layout.xs"
|
||||
:sm="chartConfig.layout.sm"
|
||||
:md="chartConfig.layout.md"
|
||||
:lg="chartConfig.layout.lg"
|
||||
:xl="chartConfig.layout.xl"
|
||||
:style="{ height: `calc(${100 / Math.ceil(persistedChartConfigs.length / 2)}% - 10px)` }"
|
||||
>
|
||||
<!-- 动态加载图表组件 -->
|
||||
<component
|
||||
:is="chartComponentMap[chartConfig.componentName]"
|
||||
class="chart-item"
|
||||
v-bind="getChartProps(chartConfig)"
|
||||
:is-refreshing="isRefreshing"
|
||||
/>
|
||||
<!-- Element 布局:行容器(不变) -->
|
||||
<el-row class="charts-row" :gutter="20" :style="{ height: `calc(100% - 40px)` }">
|
||||
<!-- 动态渲染图表:应用高度配置(核心修改) -->
|
||||
<el-col v-for="(chartConfig, index) in persistedChartConfigs" :key="chartConfig.id" class="chart-col"
|
||||
:xs="chartConfig.layout.xs" :sm="chartConfig.layout.sm" :md="chartConfig.layout.md" :lg="chartConfig.layout.lg"
|
||||
:xl="chartConfig.layout.xl" :style="{ height: `${chartConfig.height || 400}px`, marginBottom: '20px' }">
|
||||
<!-- 动态加载图表组件:确保组件高度100% -->
|
||||
<component :is="chartComponentMap[chartConfig.componentName]" class="chart-item"
|
||||
v-bind="getChartProps(chartConfig)" :is-refreshing="isRefreshing" :chart-height="chartConfig.height || 400" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'; // 导入持久化工具
|
||||
import { ElRow, ElCol } from 'element-plus'; // 导入Element布局组件
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { ElRow, ElCol } from 'element-plus';
|
||||
|
||||
// 1. 导入所有图表子组件
|
||||
import OrderTrendChart from '../components/OrderTrendChart.vue';
|
||||
// 1. 导入所有图表子组件(不变)
|
||||
import OrderTrendChart from '../components/OrderTrendChart.vue';
|
||||
import ProductSalesRank from '../components/ProductSalesRank.vue';
|
||||
import CustomerFollowStatus from '../components/CustomerFollowStatus.vue';
|
||||
import ReturnExchangeAnalysis from '../components/ReturnExchangeAnalysis.vue';
|
||||
import SalesByManagerChart from '../components/SalesByManagerChart.vue';
|
||||
import SalesByCustomerChart from '../components/SalesByCustomerChart.vue';
|
||||
import SalesMetricsCard from '../components/SalesMetricsCard.vue';
|
||||
import RecentOrdersTable from '../components/RecentOrdersTable.vue';
|
||||
|
||||
// 2. 图表组件映射表:用于动态匹配组件(组件名 → 组件实例)
|
||||
// 2. 图表组件映射表(不变)
|
||||
const chartComponentMap = {
|
||||
OrderTrendChart,
|
||||
ProductSalesRank,
|
||||
CustomerFollowStatus,
|
||||
ReturnExchangeAnalysis,
|
||||
SalesByManagerChart,
|
||||
SalesByCustomerChart
|
||||
SalesByCustomerChart,
|
||||
SalesMetricsCard,
|
||||
RecentOrdersTable
|
||||
};
|
||||
|
||||
// 3. 图表默认配置数组:定义每个图表的基础信息、布局、数据源
|
||||
// 3. 图表默认配置数组:新增height默认值(核心修改)
|
||||
const DEFAULT_CHART_CONFIGS = [
|
||||
{
|
||||
id: 'order-trend', // 唯一标识(不可重复)
|
||||
componentName: 'OrderTrendChart', // 对应组件名(与chartComponentMap匹配)
|
||||
title: '订单趋势图表', // 图表标题(可用于组件内部或表头)
|
||||
dataKey: 'orders', // 数据源key(对应props中的数据字段)
|
||||
layout: { // Element Col 布局配置(span范围:1-24,24为整行)
|
||||
xs: 24, // 超小屏:独占1行
|
||||
sm: 24, // 小屏:独占1行
|
||||
md: 12, // 中屏:占1/2行
|
||||
lg: 12, // 大屏:占1/2行
|
||||
xl: 12 // 超大屏:占1/2行
|
||||
}
|
||||
"id": "product-rank",
|
||||
"componentName": "ProductSalesRank",
|
||||
"title": "产品销售排行图表",
|
||||
"dataKey": "orderDetails",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 8,
|
||||
"lg": "8",
|
||||
"xl": 12
|
||||
},
|
||||
"height": 400
|
||||
},
|
||||
{
|
||||
id: 'product-rank',
|
||||
componentName: 'ProductSalesRank',
|
||||
title: '产品销售排行图表',
|
||||
dataKey: 'orderDetails', // 依赖orderDetails数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
"id": "customer-follow",
|
||||
"componentName": "CustomerFollowStatus",
|
||||
"title": "客户跟进状态图表",
|
||||
"dataKey": "customers",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 8,
|
||||
"lg": "8",
|
||||
"xl": 12
|
||||
},
|
||||
"height": 400
|
||||
},
|
||||
{
|
||||
id: 'sales-manager',
|
||||
componentName: 'SalesByManagerChart',
|
||||
title: '负责人订单汇总',
|
||||
dataKey: 'orders', // 依赖orders数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
"id": "sales-metrics",
|
||||
"componentName": "SalesMetricsCard",
|
||||
"title": "销售指标图表",
|
||||
"dataKey": [
|
||||
"orders",
|
||||
"customers",
|
||||
"returnExchanges"
|
||||
],
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 8,
|
||||
"lg": "8",
|
||||
"xl": 12
|
||||
},
|
||||
"height": 200
|
||||
},
|
||||
{
|
||||
id: 'customer-follow',
|
||||
componentName: 'CustomerFollowStatus',
|
||||
title: '客户跟进状态图表',
|
||||
dataKey: 'customers', // 依赖customers数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
"id": "order-trend",
|
||||
"componentName": "OrderTrendChart",
|
||||
"title": "订单趋势图表",
|
||||
"dataKey": "orders",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 12,
|
||||
"lg": 12,
|
||||
"xl": 12
|
||||
},
|
||||
"height": 400
|
||||
},
|
||||
{
|
||||
id: 'return-exchange',
|
||||
componentName: 'ReturnExchangeAnalysis',
|
||||
title: '退换货分析图表',
|
||||
dataKey: 'returnExchanges', // 依赖returnExchanges数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
"id": "sales-manager",
|
||||
"componentName": "SalesByManagerChart",
|
||||
"title": "负责人订单汇总",
|
||||
"dataKey": "orders",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 12,
|
||||
"lg": 12,
|
||||
"xl": 12
|
||||
},
|
||||
"height": 400
|
||||
},
|
||||
{
|
||||
id: 'sales-customer',
|
||||
componentName: 'SalesByCustomerChart',
|
||||
title: '客户订单汇总',
|
||||
dataKey: 'orders', // 依赖orders数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
"id": "return-exchange",
|
||||
"componentName": "ReturnExchangeAnalysis",
|
||||
"title": "退换货分析图表",
|
||||
"dataKey": "returnExchanges",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 12,
|
||||
"lg": 12,
|
||||
"xl": 12
|
||||
},
|
||||
"height": 400
|
||||
},
|
||||
{
|
||||
"id": "sales-customer",
|
||||
"componentName": "SalesByCustomerChart",
|
||||
"title": "客户订单汇总",
|
||||
"dataKey": "orders",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": 12,
|
||||
"lg": 12,
|
||||
"xl": 12
|
||||
},
|
||||
"height": 400
|
||||
},
|
||||
{
|
||||
"id": "recent-orders",
|
||||
"componentName": "RecentOrdersTable",
|
||||
"title": "最近订单",
|
||||
"dataKey": "orders",
|
||||
"layout": {
|
||||
"xs": 24,
|
||||
"sm": 24,
|
||||
"md": "24",
|
||||
"lg": "24",
|
||||
"xl": "24"
|
||||
},
|
||||
"height": 400
|
||||
}
|
||||
];
|
||||
|
||||
// 4. 持久化图表配置:用useStorage存入localStorage,key为"saleDashboardChartConfigs"
|
||||
// 逻辑:优先读取localStorage中的配置,若无则使用默认配置
|
||||
// 4. 持久化图表配置(不变,但默认值包含height)
|
||||
const persistedChartConfigs = useStorage(
|
||||
'saleDashboardChartConfigs', // 存储key(自定义,确保唯一)
|
||||
DEFAULT_CHART_CONFIGS, // 默认值
|
||||
localStorage, // 存储介质(localStorage/sessionStorage)
|
||||
{ mergeDefaults: true } // 合并默认值与存储值(避免字段缺失)
|
||||
'saleDashboardChartConfigs',
|
||||
DEFAULT_CHART_CONFIGS,
|
||||
localStorage,
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
||||
// 5. 接收父组件传入的数据源与状态
|
||||
// 5. 接收父组件传入的数据源与状态(不变)
|
||||
const props = defineProps({
|
||||
orders: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
orderDetails: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
customers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
returnExchanges: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
isRefreshing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
}
|
||||
orders: { type: Array, required: true, default: () => [] },
|
||||
orderDetails: { type: Array, required: true, default: () => [] },
|
||||
customers: { type: Array, required: true, default: () => [] },
|
||||
returnExchanges: { type: Array, required: true, default: () => [] },
|
||||
isRefreshing: { type: Boolean, required: true, default: false }
|
||||
});
|
||||
|
||||
// 6. 工具函数:根据图表配置,动态生成组件所需的props
|
||||
// 6. 工具函数:传递props(不变)
|
||||
const getChartProps = (chartConfig) => {
|
||||
// 映射数据源:根据chartConfig.dataKey匹配props中的数据
|
||||
const dataMap = {
|
||||
orders: props.orders,
|
||||
orderDetails: props.orderDetails,
|
||||
@@ -183,45 +187,51 @@ const getChartProps = (chartConfig) => {
|
||||
returnExchanges: props.returnExchanges
|
||||
};
|
||||
|
||||
// 返回该图表需要的props(如OrderTrendChart需要:orders,ProductSalesRank需要:order-details)
|
||||
if (Array.isArray(chartConfig.dataKey)) {
|
||||
const o = { title: chartConfig.title };
|
||||
chartConfig.dataKey.forEach(key => {
|
||||
o[key.replace(/([A-Z])/g, '-$1').toLowerCase()] = dataMap[key];
|
||||
});
|
||||
return o;
|
||||
}
|
||||
return {
|
||||
// 驼峰转连字符(如orderDetails → order-details,匹配组件props定义)
|
||||
[chartConfig.dataKey.replace(/([A-Z])/g, '-$1').toLowerCase()]: dataMap[chartConfig.dataKey],
|
||||
title: chartConfig.title // 可选:传递标题给图表组件
|
||||
title: chartConfig.title
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图表容器(含滚动) */
|
||||
/* 图表容器(不变) */
|
||||
.charts-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
background-color: #0f172a; /* 继承父组件深色背景 */
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
/* Element Row 容器:清除默认margin,确保高度自适应 */
|
||||
/* 行容器(不变) */
|
||||
.charts-row {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start; /* 顶部对齐,避免空白 */
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
/* Element Col 容器:控制列的高度与间距 */
|
||||
/* 列容器:移除固定高度,由配置动态控制(核心修改) */
|
||||
.chart-col {
|
||||
margin-bottom: 20px; /* 行间距,与gutter配合 */
|
||||
box-sizing: border-box;
|
||||
/* 高度由父组件style动态设置,此处不固定 */
|
||||
}
|
||||
|
||||
/* 图表项样式:保持原有设计,适配弹性布局 */
|
||||
/* 图表项:100%高度继承列容器(核心修改) */
|
||||
.chart-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 关键:让组件填满列容器高度 */
|
||||
background-color: #1e293b;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
@@ -231,16 +241,14 @@ const getChartProps = (chartConfig) => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 小屏幕下优化:减少内边距 */
|
||||
/* 小屏幕优化(不变) */
|
||||
@media (max-width: 768px) {
|
||||
.charts-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
padding: 12px;
|
||||
}
|
||||
.chart-col {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,155 +3,168 @@
|
||||
<!-- 面板标题与操作区 -->
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">图表布局设置</h2>
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="applySettings"
|
||||
:loading="saving"
|
||||
>
|
||||
<el-icon v-if="saving"><Loading /></el-icon>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" type="primary" @click="applySettings" :loading="saving">
|
||||
<span>应用设置</span>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="resetToSession"
|
||||
:disabled="saving"
|
||||
>
|
||||
<el-button size="small" @click="resetToSession" :disabled="saving">
|
||||
取消修改
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置内容区 -->
|
||||
<!-- 配置内容区:表格 + 独立预览组件 -->
|
||||
<div class="settings-content">
|
||||
<!-- 布局预览 -->
|
||||
<div class="layout-preview">
|
||||
<h3 class="section-title">布局预览</h3>
|
||||
<div class="preview-container">
|
||||
<el-row :gutter="10" class="preview-row">
|
||||
<el-col
|
||||
v-for="(chart, index) in tempChartConfigs"
|
||||
:key="chart.id"
|
||||
:span="getPreviewSpan(chart)"
|
||||
class="preview-col"
|
||||
:class="{ 'preview-col-hidden': !chart.visible }"
|
||||
>
|
||||
<div class="preview-chart">
|
||||
<div class="chart-header">
|
||||
<span class="chart-name">{{ chart.title }}</span>
|
||||
<el-switch
|
||||
v-model="chart.visible"
|
||||
size="small"
|
||||
active-color="#1677ff"
|
||||
/>
|
||||
</div>
|
||||
<div class="chart-placeholder">
|
||||
<span class="chart-id">{{ chart.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表配置列表 -->
|
||||
<!-- 原有图表配置表格 -->
|
||||
<div class="charts-config">
|
||||
<h3 class="section-title">图表配置</h3>
|
||||
<el-table
|
||||
:data="tempChartConfigs"
|
||||
border
|
||||
size="small"
|
||||
:height="tableHeight"
|
||||
>
|
||||
<el-table-column
|
||||
prop="title"
|
||||
label="图表名称"
|
||||
width="200"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="标识"
|
||||
width="160"
|
||||
/>
|
||||
<el-table-column label="显示状态">
|
||||
<h3 class="section-title">
|
||||
图表配置
|
||||
</h3>
|
||||
<el-table
|
||||
border
|
||||
size="small"
|
||||
:height="tableHeight"
|
||||
row-key="id"
|
||||
:data="tempChartConfigs">
|
||||
<!-- 表格列内容保持不变(与原代码一致) -->
|
||||
<el-table-column prop="title" label="图表名称" min-width="120" />
|
||||
<el-table-column prop="id" label="标识" min-width="100" />
|
||||
<el-table-column label="图表高度" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.visible"
|
||||
size="small"
|
||||
active-color="#1677ff"
|
||||
/>
|
||||
<el-input
|
||||
v-model.number="scope.row.height"
|
||||
size="small"
|
||||
type="number"
|
||||
:min="100"
|
||||
:max="1000"
|
||||
placeholder="px (100-1000)"
|
||||
@input="validateHeight(scope.row, scope.$index)"
|
||||
:disabled="saving" />
|
||||
<div v-if="heightErrorIndex === scope.$index" class="height-error">
|
||||
请输入100-1000之间的有效数字
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 布局尺寸配置 -->
|
||||
<el-table-column label="超小屏 (≤768px)">
|
||||
<el-table-column label="超小屏 (≤768px)" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.layout.xs"
|
||||
size="small"
|
||||
:disabled="!scope.row.visible"
|
||||
>
|
||||
<el-option :value="24">独占一行</el-option>
|
||||
<el-option :value="12">半行宽度</el-option>
|
||||
<el-select
|
||||
v-model="scope.row.layout.xs"
|
||||
size="small"
|
||||
append-to="#full-dashboard-container"
|
||||
:disabled="saving">
|
||||
<el-option value="24">独占一行</el-option>
|
||||
<el-option value="12">半行宽度</el-option>
|
||||
<el-option value="8">三分之一</el-option>
|
||||
<el-option value="6">四分之一</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中大屏 (≥992px)">
|
||||
<el-table-column label="小屏 (≥768px)" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.layout.md"
|
||||
size="small"
|
||||
:disabled="!scope.row.visible"
|
||||
>
|
||||
<el-option :value="24">独占一行</el-option>
|
||||
<el-option :value="12">半行宽度</el-option>
|
||||
<el-select
|
||||
v-model="scope.row.layout.sm"
|
||||
size="small"
|
||||
append-to="#full-dashboard-container"
|
||||
:disabled="saving">
|
||||
<el-option value="24">独占一行</el-option>
|
||||
<el-option value="12">半行宽度</el-option>
|
||||
<el-option value="8">三分之一</el-option>
|
||||
<el-option value="6">四分之一</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<el-table-column label="中屏 (≥992px)" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
<el-select
|
||||
v-model="scope.row.layout.md"
|
||||
size="small"
|
||||
append-to="#full-dashboard-container"
|
||||
:disabled="saving">
|
||||
<el-option value="24">独占一行</el-option>
|
||||
<el-option value="12">半行宽度</el-option>
|
||||
<el-option value="8">三分之一</el-option>
|
||||
<el-option value="6">四分之一</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大屏 (≥1200px)" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.layout.lg"
|
||||
size="small"
|
||||
append-to="#full-dashboard-container"
|
||||
:disabled="saving">
|
||||
<el-option value="24">独占一行</el-option>
|
||||
<el-option value="12">半行宽度</el-option>
|
||||
<el-option value="8">三分之一</el-option>
|
||||
<el-option value="6">四分之一</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="超大屏 (≥1920px)" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.layout.xl"
|
||||
size="small"
|
||||
append-to="#full-dashboard-container"
|
||||
:disabled="saving">
|
||||
<el-option value="24">独占一行</el-option>
|
||||
<el-option value="12">半行宽度</el-option>
|
||||
<el-option value="8">三分之一</el-option>
|
||||
<el-option value="6">四分之一</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
text
|
||||
@click="moveChart(scope.$index, 'up')"
|
||||
:disabled="scope.$index === 0"
|
||||
>
|
||||
上移
|
||||
:disabled="scope.$index === 0 || saving"
|
||||
icon="ArrowUp">
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
text
|
||||
@click="moveChart(scope.$index, 'down')"
|
||||
:disabled="scope.$index === tempChartConfigs.length - 1"
|
||||
>
|
||||
下移
|
||||
:disabled="scope.$index === tempChartConfigs.length - 1 || saving"
|
||||
icon="ArrowDown">
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 引入独立预览组件:传递配置和缩放比例 -->
|
||||
<ChartLayoutPreview
|
||||
:chart-configs="tempChartConfigs"
|
||||
:scale-ratio="0.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="settings-footer">
|
||||
<el-alert
|
||||
title="提示:取消修改将恢复到打开设置时的状态"
|
||||
type="info"
|
||||
size="small"
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
title="提示:可通过上下按钮调整图表顺序,取消修改将恢复到打开设置时的状态"
|
||||
type="info"
|
||||
size="small"
|
||||
:closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox, ElEmpty, ElRow, ElCol, ElCard } from 'element-plus';
|
||||
// 引入独立预览组件
|
||||
import ChartLayoutPreview from './ChartLayoutPreview.vue';
|
||||
|
||||
// 接收存储键名props
|
||||
// 原有逻辑保持不变(与原代码一致)
|
||||
const props = defineProps({
|
||||
storageKey: {
|
||||
type: String,
|
||||
@@ -160,22 +173,17 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// 从storage读取配置,不设置预设默认值
|
||||
const persistedChartConfigs = useStorage(
|
||||
props.storageKey,
|
||||
[], // 空数组作为初始值,不预设默认配置
|
||||
[],
|
||||
localStorage,
|
||||
{
|
||||
serializer: {
|
||||
read: (v) => {
|
||||
try {
|
||||
if (typeof v === 'string') {
|
||||
return JSON.parse(v);
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
return []; // 解析失败时返回空数组
|
||||
if (typeof v === 'string') return JSON.parse(v);
|
||||
if (Array.isArray(v)) return v;
|
||||
return [];
|
||||
} catch (e) {
|
||||
console.error('解析存储的图表配置失败,使用空配置:', e);
|
||||
return [];
|
||||
@@ -183,55 +191,58 @@ const persistedChartConfigs = useStorage(
|
||||
},
|
||||
write: (v) => JSON.stringify(v)
|
||||
},
|
||||
mergeDefaults: false // 关闭默认值合并
|
||||
mergeDefaults: false
|
||||
}
|
||||
);
|
||||
|
||||
// 临时配置(用于编辑)
|
||||
const tempChartConfigs = ref([]);
|
||||
// 保存弹窗打开时的初始配置快照(直接来自storage)
|
||||
const initialChartConfigs = ref([]);
|
||||
// 保存状态
|
||||
const saving = ref(false);
|
||||
// 窗口尺寸响应
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const tableHeight = ref(300);
|
||||
const heightErrorIndex = ref(-1);
|
||||
const showBatchSettings = ref(false);
|
||||
const batchSettings = reactive({
|
||||
applyRange: 'all',
|
||||
height: null,
|
||||
layout: { xs: null, sm: null, md: null, lg: null, xl: null }
|
||||
});
|
||||
|
||||
// 监听窗口尺寸变化
|
||||
useEventListener('resize', () => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
calculateTableHeight();
|
||||
});
|
||||
|
||||
// 初始化:从storage读取数据,并保存初始快照
|
||||
onMounted(() => {
|
||||
// 确保初始数据是数组
|
||||
if (!Array.isArray(persistedChartConfigs.value)) {
|
||||
persistedChartConfigs.value = [];
|
||||
}
|
||||
// 初始化临时配置和初始快照
|
||||
resetToSession();
|
||||
calculateTableHeight();
|
||||
});
|
||||
|
||||
// 计算表格高度
|
||||
const calculateTableHeight = () => {
|
||||
tableHeight.value = Math.max(300, window.innerHeight - 500);
|
||||
const panelHeight = window.innerHeight - 400; // 预留预览区域高度
|
||||
tableHeight.value = Math.max(300, Math.min(500, panelHeight));
|
||||
};
|
||||
|
||||
// 重置临时配置为打开弹窗时的状态
|
||||
const resetToSession = () => {
|
||||
try {
|
||||
// 从storage数据读取
|
||||
const source = Array.isArray(persistedChartConfigs.value)
|
||||
? persistedChartConfigs.value
|
||||
: [];
|
||||
|
||||
// 深拷贝保存到临时配置和初始快照
|
||||
const configCopy = JSON.parse(JSON.stringify(source));
|
||||
const source = Array.isArray(persistedChartConfigs.value) ? persistedChartConfigs.value : [];
|
||||
const configCopy = JSON.parse(JSON.stringify(source)).map(config => ({
|
||||
...config,
|
||||
height: config.height || 400,
|
||||
visible: config.visible !== undefined ? config.visible : true,
|
||||
layout: {
|
||||
xs: config.layout?.xs || 24,
|
||||
sm: config.layout?.sm || 24,
|
||||
md: config.layout?.md || 12,
|
||||
lg: config.layout?.lg || 12,
|
||||
xl: config.layout?.xl || 6
|
||||
}
|
||||
}));
|
||||
tempChartConfigs.value = configCopy;
|
||||
initialChartConfigs.value = JSON.parse(JSON.stringify(configCopy));
|
||||
|
||||
ElMessage.info('已恢复到打开设置时的状态');
|
||||
} catch (e) {
|
||||
console.error('重置配置失败:', e);
|
||||
@@ -239,64 +250,143 @@ const resetToSession = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 计算预览区域的span值
|
||||
const getPreviewSpan = (chart) => {
|
||||
return Math.floor(chart.layout.md / 4);
|
||||
const validateHeight = (row, index) => {
|
||||
heightErrorIndex.value = -1;
|
||||
if (isNaN(row.height) || row.height < 100 || row.height > 1000) {
|
||||
heightErrorIndex.value = index;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 移动图表位置
|
||||
const moveChart = (index, direction) => {
|
||||
let newIndex;
|
||||
if (direction === 'up' && index > 0) {
|
||||
[tempChartConfigs.value[index], tempChartConfigs.value[index - 1]] =
|
||||
[tempChartConfigs.value[index - 1], tempChartConfigs.value[index]];
|
||||
newIndex = index - 1;
|
||||
[tempChartConfigs.value[index], tempChartConfigs.value[newIndex]] = [tempChartConfigs.value[newIndex], tempChartConfigs.value[index]];
|
||||
tempChartConfigs.value = [...tempChartConfigs.value];
|
||||
highlightRow(index);
|
||||
highlightRow(newIndex);
|
||||
} else if (direction === 'down' && index < tempChartConfigs.value.length - 1) {
|
||||
[tempChartConfigs.value[index], tempChartConfigs.value[index + 1]] =
|
||||
[tempChartConfigs.value[index + 1], tempChartConfigs.value[index]];
|
||||
newIndex = index + 1;
|
||||
[tempChartConfigs.value[index], tempChartConfigs.value[newIndex]] = [tempChartConfigs.value[newIndex], tempChartConfigs.value[index]];
|
||||
tempChartConfigs.value = [...tempChartConfigs.value];
|
||||
highlightRow(index);
|
||||
highlightRow(newIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// 应用设置
|
||||
const highlightRow = (index) => {
|
||||
const rowEl = document.querySelector(`.el-table__row:nth-child(${index + 1})`);
|
||||
if (rowEl) {
|
||||
rowEl.classList.add('row-highlight');
|
||||
setTimeout(() => rowEl.classList.remove('row-highlight'), 600);
|
||||
}
|
||||
};
|
||||
|
||||
const applyBatchSettings = () => {
|
||||
const targetCharts = batchSettings.applyRange === 'visible'
|
||||
? tempChartConfigs.value.filter(chart => chart.visible)
|
||||
: tempChartConfigs.value;
|
||||
|
||||
if (targetCharts.length === 0) {
|
||||
ElMessage.warning('没有符合条件的图表可应用设置');
|
||||
return;
|
||||
}
|
||||
|
||||
targetCharts.forEach(chart => {
|
||||
if (batchSettings.height !== null && !isNaN(batchSettings.height) && batchSettings.height >= 100 && batchSettings.height <= 1000) {
|
||||
chart.height = batchSettings.height;
|
||||
}
|
||||
Object.keys(batchSettings.layout).forEach(key => {
|
||||
if (batchSettings.layout[key] !== null) {
|
||||
chart.layout[key] = batchSettings.layout[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tempChartConfigs.value = [...tempChartConfigs.value];
|
||||
showBatchSettings.value = false;
|
||||
ElMessage.success(`已对 ${targetCharts.length} 个图表应用批量设置`);
|
||||
};
|
||||
|
||||
const applySettings = async () => {
|
||||
try {
|
||||
const hasInvalidHeight = tempChartConfigs.value.some((row, index) => !validateHeight(row, index));
|
||||
if (hasInvalidHeight) {
|
||||
ElMessage.error('存在无效的高度配置,请修正后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleCharts = tempChartConfigs.value.filter(chart => chart.visible);
|
||||
if (visibleCharts.length === 0) {
|
||||
await ElMessageBox.confirm(
|
||||
'所有图表都处于隐藏状态,确定要应用设置吗?',
|
||||
'确认提示',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
);
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
|
||||
if (Array.isArray(tempChartConfigs.value)) {
|
||||
persistedChartConfigs.value = JSON.parse(JSON.stringify(tempChartConfigs.value));
|
||||
emits('config-updated', persistedChartConfigs.value);
|
||||
// 更新初始快照为当前已应用的配置
|
||||
initialChartConfigs.value = JSON.parse(JSON.stringify(tempChartConfigs.value));
|
||||
ElMessage.success('配置已保存并生效');
|
||||
} else {
|
||||
throw new Error('配置数据格式错误,必须为数组');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
ElMessage.error('保存配置失败,请重试');
|
||||
if (error !== 'cancel') {
|
||||
console.error('保存配置失败:', error);
|
||||
ElMessage.error('保存配置失败,请重试');
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义事件
|
||||
const emits = defineEmits(['config-updated', 'close']);
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
resetToSession,
|
||||
applySettings
|
||||
});
|
||||
defineExpose({ resetToSession, applySettings });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 保持原有样式不变 */
|
||||
/* 原有样式保持不变(删除原预览相关样式,预览样式已封装在子组件中) */
|
||||
.chart-settings-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
min-width: 900px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 表格行动画效果 */
|
||||
.el-table__body .el-table-row {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.row-highlight {
|
||||
animation: rowHighlight 0.6s ease;
|
||||
}
|
||||
|
||||
@keyframes rowHighlight {
|
||||
0% { background-color: rgba(22, 119, 255, 0.2); }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
|
||||
.height-error {
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
margin-top: 4px;
|
||||
animation: shake 0.5s;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(2px); }
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -330,65 +420,9 @@ defineExpose({
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.layout-preview {
|
||||
padding: 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-row {
|
||||
width: 100%;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.preview-col {
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
.preview-col-hidden {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.preview-chart {
|
||||
background-color: white;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background-color: #f0f2f5;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
.chart-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.charts-config {
|
||||
@@ -403,14 +437,37 @@ defineExpose({
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.chart-settings-panel {
|
||||
min-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.chart-settings-panel {
|
||||
min-width: 700px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-settings-panel {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
gap: 12px;
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -85,17 +85,17 @@ const resizeFn = debounce(function () {
|
||||
}, 200);
|
||||
|
||||
// 组件挂载后自动全屏
|
||||
// onMounted(() => {
|
||||
// const documentWidth = document.body.offsetWidth;
|
||||
// const ratio = documentWidth / 1920;
|
||||
// if (documentWidth > 1920) {
|
||||
// document.body.style.transform = `scale(${ratio}, ${ratio})`;
|
||||
// }
|
||||
onMounted(() => {
|
||||
const documentWidth = document.body.offsetWidth;
|
||||
const ratio = documentWidth / 1920;
|
||||
if (documentWidth > 1920) {
|
||||
document.body.style.transform = `scale(${ratio}, ${ratio})`;
|
||||
}
|
||||
|
||||
// window.addEventListener('resize', resizeFn);
|
||||
window.addEventListener('resize', resizeFn);
|
||||
|
||||
// setTimeout(handleEnterFullscreen, 100);
|
||||
// });
|
||||
setTimeout(handleEnterFullscreen, 100);
|
||||
});
|
||||
|
||||
// 路由离开前退出全屏
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<el-dialog v-model="settingVisible" title="图表设置" width="50%">
|
||||
<el-dialog v-model="settingVisible" title="图表设置" width="80%">
|
||||
<ChartSetting />
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
@@ -2,150 +2,21 @@
|
||||
<div class="dashboard-root">
|
||||
<!-- 第一行:头像+欢迎语 -->
|
||||
<UserGreeting />
|
||||
|
||||
<!-- 业务功能区 -->
|
||||
<div class="business-area-header">
|
||||
<h2 class="business-area-title">常用应用</h2>
|
||||
<button class="edit-btn" @click="showSettingDialog = true">
|
||||
<svg-icon icon-class="edit" /> 编辑常用
|
||||
</button>
|
||||
</div>
|
||||
<div class="business-modules">
|
||||
<div v-for="(module, index) in businessModules" :key="index"
|
||||
class="business-module" @click="handleLink(module.link)">
|
||||
<div class="business-module-icon">
|
||||
<svg-icon :icon-class="module.icon" />
|
||||
</div>
|
||||
<h3 class="business-module-title">{{ module.title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全部应用 -->
|
||||
<AllApplications @addFavorites="handleAddFavorites" />
|
||||
|
||||
<!-- 设置弹窗 -->
|
||||
<div class="dialog-mask" v-if="showSettingDialog">
|
||||
<div class="setting-dialog">
|
||||
<div class="dialog-header">
|
||||
<h3>编辑常用应用</h3>
|
||||
<button class="close-btn" @click="showSettingDialog = false">
|
||||
<svg-icon icon-class="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div class="empty-tip" v-if="businessModules.length === 0">
|
||||
暂无常用应用,可从下方"全部应用"中添加
|
||||
</div>
|
||||
<ul class="module-list" v-else>
|
||||
<li v-for="(module, index) in businessModules" :key="index" class="module-item">
|
||||
<div class="module-info">
|
||||
<span class="module-index">{{ index + 1 }}</span>
|
||||
<span class="module-name">{{ module.title }}</span>
|
||||
</div>
|
||||
<div class="module-actions">
|
||||
<el-button
|
||||
@click="moveModule(index, 'up')"
|
||||
:disabled="index === 0"
|
||||
icon="ArrowUp" type="primary"
|
||||
>
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="moveModule(index, 'down')"
|
||||
:disabled="index === businessModules.length - 1"
|
||||
icon="ArrowDown" type="primary"
|
||||
>
|
||||
</el-button>
|
||||
<el-button @click="deleteModule(index)" icon="Delete" danger>
|
||||
</el-button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button class="cancel-btn" @click="showSettingDialog = false">取消</button>
|
||||
<button class="confirm-btn" @click="showSettingDialog = false">完成</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import AllApplications from '@/components/AllApplications.vue';
|
||||
import UserGreeting from '@/views/components/Hello.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
// 响应式数据
|
||||
const businessModules = useStorage('businessModules', [
|
||||
{
|
||||
title: '考勤日历',
|
||||
icon: 'code',
|
||||
link: '/people/calendar'
|
||||
},
|
||||
{
|
||||
title: '系统设置',
|
||||
icon: 'system',
|
||||
link: '/system/menu'
|
||||
},
|
||||
]);
|
||||
const showSettingDialog = ref(false); // 控制弹窗显示
|
||||
|
||||
// 路由实例
|
||||
const router = useRouter();
|
||||
|
||||
// 方法定义
|
||||
const handleLink = (link) => {
|
||||
if (link.startsWith('http') || link.endsWith('.html')) {
|
||||
window.open(link, '_blank');
|
||||
} else {
|
||||
router.push(link);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFavorites = (appInfo) => {
|
||||
// 检查是否已经存在,如果已经存在就移除,否则添加
|
||||
const index = businessModules.value.findIndex(module => module.link === appInfo.fullPath)
|
||||
if (index !== -1) {
|
||||
businessModules.value.splice(index, 1)
|
||||
} else {
|
||||
businessModules.value.push({
|
||||
title: appInfo.child.meta.title,
|
||||
icon: appInfo.child.meta.icon,
|
||||
link: appInfo.fullPath
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// 移动模块位置(排序功能)
|
||||
const moveModule = (index, direction) => {
|
||||
// 创建副本避免直接修改源数组导致的响应式问题
|
||||
const newModules = [...businessModules.value];
|
||||
// 上移:与前一个元素交换位置
|
||||
if (direction === 'up' && index > 0) {
|
||||
[newModules[index], newModules[index - 1]] = [newModules[index - 1], newModules[index]];
|
||||
}
|
||||
// 下移:与后一个元素交换位置
|
||||
if (direction === 'down' && index < newModules.length - 1) {
|
||||
[newModules[index], newModules[index + 1]] = [newModules[index + 1], newModules[index]];
|
||||
}
|
||||
// 更新响应式数据
|
||||
businessModules.value = newModules;
|
||||
};
|
||||
|
||||
// 删除模块
|
||||
const deleteModule = (index) => {
|
||||
if (confirm('确定要移除该常用应用吗?')) {
|
||||
businessModules.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-root {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
||||
/* background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); */
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +1,46 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="账号"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaEnabled">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
style="width: 63%"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
||||
<el-form-item style="width:100%;">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
size="large"
|
||||
type="primary"
|
||||
style="width:100%;"
|
||||
@click.prevent="handleLogin"
|
||||
>
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
<div style="float: right;" v-if="register">
|
||||
<router-link class="link-type" :to="'/register'">立即注册</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="login-container">
|
||||
<div class="login-right">
|
||||
<img :src="RightImage" alt="" class="right-img" />
|
||||
</div>
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaEnabled">
|
||||
<el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
||||
<el-form-item style="width:100%;">
|
||||
<el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
<div style="float: right;" v-if="register">
|
||||
<router-link class="link-type" :to="'/register'">立即注册</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="el-login-footer">
|
||||
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
|
||||
@@ -70,6 +54,8 @@ import Cookies from "js-cookie"
|
||||
import { encrypt, decrypt } from "@/utils/jsencrypt"
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useProductStore from '@/store/modules/product'
|
||||
import RightImage from '@/assets/images/right.png'
|
||||
import LogoImage from '@/assets/logo/logo.png'
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
const userStore = useUserStore()
|
||||
@@ -100,7 +86,7 @@ const register = ref(false)
|
||||
const redirect = ref(undefined)
|
||||
|
||||
watch(route, (newRoute) => {
|
||||
redirect.value = newRoute.query && newRoute.query.redirect
|
||||
redirect.value = newRoute.query && newRoute.query.redirect
|
||||
}, { immediate: true })
|
||||
|
||||
function handleLogin() {
|
||||
@@ -171,47 +157,95 @@ getCookie()
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-image: url("../assets/images/login-background.jpg");
|
||||
background-color: #1a1a1a;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
color: #707070;
|
||||
color: #ebebeb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
// height: 60%;
|
||||
width: 60%;
|
||||
padding: 25px 25px 5px 25px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #333333;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #444444;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
color: #ebebeb;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
width: 400px;
|
||||
padding: 25px 25px 5px 25px;
|
||||
z-index: 1;
|
||||
flex: 3;
|
||||
border-radius: 6px;
|
||||
background: #292929;
|
||||
padding: 25px;
|
||||
|
||||
.el-input {
|
||||
height: 40px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-right {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 33%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.el-login-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
@@ -224,6 +258,7 @@ getCookie()
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-code-img {
|
||||
height: 40px;
|
||||
padding-left: 12px;
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
style="margin-bottom: 20px;"
|
||||
/>
|
||||
|
||||
<!-- 页面标题:补充基础样式 -->
|
||||
<div style="font-size: 18px; font-weight: 500; margin-bottom: 20px;">
|
||||
数据分析页面
|
||||
</div>
|
||||
|
||||
<!-- 主内容区(数据加载成功才显示) -->
|
||||
<div v-if="!loading && !errorMsg">
|
||||
<!-- 第一行:四个指标卡 -->
|
||||
|
||||