✨ feat: 数据大屏
This commit is contained in:
246
gear-ui3/src/views/dashboard/grid/index.vue
Normal file
246
gear-ui3/src/views/dashboard/grid/index.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<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"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'; // 导入持久化工具
|
||||
import { ElRow, ElCol } from 'element-plus'; // 导入Element布局组件
|
||||
|
||||
// 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';
|
||||
|
||||
// 2. 图表组件映射表:用于动态匹配组件(组件名 → 组件实例)
|
||||
const chartComponentMap = {
|
||||
OrderTrendChart,
|
||||
ProductSalesRank,
|
||||
CustomerFollowStatus,
|
||||
ReturnExchangeAnalysis,
|
||||
SalesByManagerChart,
|
||||
SalesByCustomerChart
|
||||
};
|
||||
|
||||
// 3. 图表默认配置数组:定义每个图表的基础信息、布局、数据源
|
||||
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', // 依赖orderDetails数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sales-manager',
|
||||
componentName: 'SalesByManagerChart',
|
||||
title: '负责人订单汇总',
|
||||
dataKey: 'orders', // 依赖orders数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'customer-follow',
|
||||
componentName: 'CustomerFollowStatus',
|
||||
title: '客户跟进状态图表',
|
||||
dataKey: 'customers', // 依赖customers数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'return-exchange',
|
||||
componentName: 'ReturnExchangeAnalysis',
|
||||
title: '退换货分析图表',
|
||||
dataKey: 'returnExchanges', // 依赖returnExchanges数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sales-customer',
|
||||
componentName: 'SalesByCustomerChart',
|
||||
title: '客户订单汇总',
|
||||
dataKey: 'orders', // 依赖orders数据
|
||||
layout: {
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 4. 持久化图表配置:用useStorage存入localStorage,key为"saleDashboardChartConfigs"
|
||||
// 逻辑:优先读取localStorage中的配置,若无则使用默认配置
|
||||
const persistedChartConfigs = useStorage(
|
||||
'saleDashboardChartConfigs', // 存储key(自定义,确保唯一)
|
||||
DEFAULT_CHART_CONFIGS, // 默认值
|
||||
localStorage, // 存储介质(localStorage/sessionStorage)
|
||||
{ mergeDefaults: true } // 合并默认值与存储值(避免字段缺失)
|
||||
);
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 工具函数:根据图表配置,动态生成组件所需的props
|
||||
const getChartProps = (chartConfig) => {
|
||||
// 映射数据源:根据chartConfig.dataKey匹配props中的数据
|
||||
const dataMap = {
|
||||
orders: props.orders,
|
||||
orderDetails: props.orderDetails,
|
||||
customers: props.customers,
|
||||
returnExchanges: props.returnExchanges
|
||||
};
|
||||
|
||||
// 返回该图表需要的props(如OrderTrendChart需要:orders,ProductSalesRank需要:order-details)
|
||||
return {
|
||||
// 驼峰转连字符(如orderDetails → order-details,匹配组件props定义)
|
||||
[chartConfig.dataKey.replace(/([A-Z])/g, '-$1').toLowerCase()]: dataMap[chartConfig.dataKey],
|
||||
title: chartConfig.title // 可选:传递标题给图表组件
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 图表容器(含滚动) */
|
||||
.charts-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
background-color: #0f172a; /* 继承父组件深色背景 */
|
||||
}
|
||||
|
||||
/* Element Row 容器:清除默认margin,确保高度自适应 */
|
||||
.charts-row {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start; /* 顶部对齐,避免空白 */
|
||||
}
|
||||
|
||||
/* Element Col 容器:控制列的高度与间距 */
|
||||
.chart-col {
|
||||
margin-bottom: 20px; /* 行间距,与gutter配合 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 图表项样式:保持原有设计,适配弹性布局 */
|
||||
.chart-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1e293b;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 小屏幕下优化:减少内边距 */
|
||||
@media (max-width: 768px) {
|
||||
.charts-container {
|
||||
padding: 10px;
|
||||
}
|
||||
.chart-item {
|
||||
padding: 12px;
|
||||
}
|
||||
.chart-col {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
416
gear-ui3/src/views/dashboard/grid/setting.vue
Normal file
416
gear-ui3/src/views/dashboard/grid/setting.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="chart-settings-panel">
|
||||
<!-- 面板标题与操作区 -->
|
||||
<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>
|
||||
<span>应用设置</span>
|
||||
</el-button>
|
||||
<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="显示状态">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.visible"
|
||||
size="small"
|
||||
active-color="#1677ff"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 布局尺寸配置 -->
|
||||
<el-table-column label="超小屏 (≤768px)">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中大屏 (≥992px)">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="moveChart(scope.$index, 'up')"
|
||||
:disabled="scope.$index === 0"
|
||||
>
|
||||
上移
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="moveChart(scope.$index, 'down')"
|
||||
:disabled="scope.$index === tempChartConfigs.length - 1"
|
||||
>
|
||||
下移
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="settings-footer">
|
||||
<el-alert
|
||||
title="提示:取消修改将恢复到打开设置时的状态"
|
||||
type="info"
|
||||
size="small"
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
|
||||
// 接收存储键名props
|
||||
const props = defineProps({
|
||||
storageKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'saleDashboardChartConfigs'
|
||||
}
|
||||
});
|
||||
|
||||
// 从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 []; // 解析失败时返回空数组
|
||||
} catch (e) {
|
||||
console.error('解析存储的图表配置失败,使用空配置:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
write: (v) => JSON.stringify(v)
|
||||
},
|
||||
mergeDefaults: false // 关闭默认值合并
|
||||
}
|
||||
);
|
||||
|
||||
// 临时配置(用于编辑)
|
||||
const tempChartConfigs = ref([]);
|
||||
// 保存弹窗打开时的初始配置快照(直接来自storage)
|
||||
const initialChartConfigs = ref([]);
|
||||
// 保存状态
|
||||
const saving = ref(false);
|
||||
// 窗口尺寸响应
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const tableHeight = ref(300);
|
||||
|
||||
// 监听窗口尺寸变化
|
||||
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 resetToSession = () => {
|
||||
try {
|
||||
// 从storage数据读取
|
||||
const source = Array.isArray(persistedChartConfigs.value)
|
||||
? persistedChartConfigs.value
|
||||
: [];
|
||||
|
||||
// 深拷贝保存到临时配置和初始快照
|
||||
const configCopy = JSON.parse(JSON.stringify(source));
|
||||
tempChartConfigs.value = configCopy;
|
||||
initialChartConfigs.value = JSON.parse(JSON.stringify(configCopy));
|
||||
|
||||
ElMessage.info('已恢复到打开设置时的状态');
|
||||
} catch (e) {
|
||||
console.error('重置配置失败:', e);
|
||||
ElMessage.error('重置配置失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 计算预览区域的span值
|
||||
const getPreviewSpan = (chart) => {
|
||||
return Math.floor(chart.layout.md / 4);
|
||||
};
|
||||
|
||||
// 移动图表位置
|
||||
const moveChart = (index, direction) => {
|
||||
if (direction === 'up' && index > 0) {
|
||||
[tempChartConfigs.value[index], tempChartConfigs.value[index - 1]] =
|
||||
[tempChartConfigs.value[index - 1], tempChartConfigs.value[index]];
|
||||
} else if (direction === 'down' && index < tempChartConfigs.value.length - 1) {
|
||||
[tempChartConfigs.value[index], tempChartConfigs.value[index + 1]] =
|
||||
[tempChartConfigs.value[index + 1], tempChartConfigs.value[index]];
|
||||
}
|
||||
};
|
||||
|
||||
// 应用设置
|
||||
const applySettings = async () => {
|
||||
try {
|
||||
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('保存配置失败,请重试');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义事件
|
||||
const emits = defineEmits(['config-updated', 'close']);
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
resetToSession,
|
||||
applySettings
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 保持原有样式不变 */
|
||||
.chart-settings-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 12px 0;
|
||||
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;
|
||||
}
|
||||
|
||||
.charts-config {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-settings-panel {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user