feat: 可视化大屏完善

This commit is contained in:
砂糖
2025-09-05 14:05:07 +08:00
parent d459072a11
commit b293cbd04f
8 changed files with 752 additions and 315 deletions

View File

@@ -63,9 +63,8 @@ export default {
computed: {
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)

View 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>

View File

@@ -20,6 +20,8 @@ const props = defineProps({
isRefreshing: { type: Boolean, default: false }
});
console.log(props.orders, '客户订单汇总')
// 图表容器引用
const barChartRef = ref(null);
const radarChartRef = ref(null);

View 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>

View File

@@ -1,13 +1,13 @@
<template>
<!-- 图表容器含滚动 -->
<div class="charts-container">
<!-- Element 布局el-row 行容器gutter 控制间距 -->
<!-- Element 布局行容器不变 -->
<el-row
class="charts-row"
:gutter="20"
:style="{ height: `calc(100% - 40px)` }"
>
<!-- 动态渲染图表遍历持久化后的配置数组 -->
<!-- 动态渲染图表应用高度配置核心修改 -->
<el-col
v-for="(chartConfig, index) in persistedChartConfigs"
:key="chartConfig.id"
@@ -17,14 +17,15 @@
:md="chartConfig.layout.md"
:lg="chartConfig.layout.lg"
:xl="chartConfig.layout.xl"
:style="{ height: `calc(${100 / Math.ceil(persistedChartConfigs.length / 2)}% - 10px)` }"
: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>
@@ -32,150 +33,118 @@
</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. 导入所有图表子组件
// 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-2424为整行
xs: 24, // 超小屏独占1行
sm: 24, // 小屏独占1行
md: 12, // 中屏占1/2行
lg: 12, // 大屏占1/2行
xl: 12 // 超大屏占1/2行
}
id: 'order-trend',
componentName: 'OrderTrendChart',
title: '订单趋势图表',
dataKey: 'orders',
height: 400, // 新增默认高度400px
layout: { xs:24, sm:24, md:12, lg:12, xl:12 }
},
{
id: 'product-rank',
componentName: 'ProductSalesRank',
title: '产品销售排行图表',
dataKey: 'orderDetails', // 依赖orderDetails数据
layout: {
xs: 24,
sm: 24,
md: 12,
lg: 12,
xl: 12
}
dataKey: 'orderDetails',
height: 400, // 新增默认高度400px
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
}
dataKey: 'orders',
height: 400, // 新增默认高度400px
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
}
dataKey: 'customers',
height: 400, // 新增默认高度400px
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
}
dataKey: 'returnExchanges',
height: 400, // 新增默认高度400px
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
}
dataKey: 'orders',
height: 400, // 新增默认高度400px
layout: { xs:24, sm:24, md:12, lg:12, xl:12 }
},
{
id: 'sales-metrics',
componentName: 'SalesMetricsCard',
title: '销售指标图表',
dataKey: ['orders', 'customers', 'returnExchanges'],
height: 200, // 新增指标卡高度较小默认200px
layout: { xs:24, sm:24, md:12, lg:12, xl:12 }
},
{
id: 'recent-orders',
componentName: 'RecentOrdersTable',
title: '最近订单',
dataKey: 'orders',
height: 500, // 新增表格高度较大默认500px
layout: { xs:24, sm:24, md:12, lg:12, xl:12 }
}
];
// 4. 持久化图表配置用useStorage存入localStoragekey为"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 +152,50 @@ const getChartProps = (chartConfig) => {
returnExchanges: props.returnExchanges
};
// 返回该图表需要的props如OrderTrendChart需要:ordersProductSalesRank需要: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%;
height: 100%; /* 关键:让组件填满列容器高度 */
background-color: #1e293b;
border-radius: 8px;
padding: 16px;
@@ -231,7 +205,7 @@ const getChartProps = (chartConfig) => {
flex-direction: column;
}
/* 小屏幕优化:减少内边距 */
/* 小屏幕优化(不变) */
@media (max-width: 768px) {
.charts-container {
padding: 10px;
@@ -239,8 +213,5 @@ const getChartProps = (chartConfig) => {
.chart-item {
padding: 12px;
}
.chart-col {
margin-bottom: 10px;
}
}
</style>

View File

@@ -3,21 +3,12 @@
<!-- 面板标题与操作区 -->
<div class="panel-header">
<h2 class="panel-title">图表布局设置</h2>
<div class="header-actions">
<el-button
size="small"
type="primary"
@click="applySettings"
:loading="saving"
>
<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 size="small" @click="resetToSession" :disabled="saving">
取消修改
</el-button>
</div>
@@ -25,106 +16,132 @@
<!-- 配置内容区 -->
<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>
<!-- 直接保留表格移除draggable包裹 -->
<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-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-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>
@@ -134,22 +151,22 @@
<!-- 提示信息 -->
<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 } from 'element-plus';
import { Loading, ArrowUp, ArrowDown, Grid } from '@element-plus/icons-vue';
// 移除draggable组件导入
// 接收存储键名props
const props = defineProps({
@@ -160,10 +177,10 @@ const props = defineProps({
}
});
// 从storage读取配置,不设置预设默认值
// 从storage读取配置
const persistedChartConfigs = useStorage(
props.storageKey,
[], // 空数组作为初始值,不预设默认配置
[],
localStorage,
{
serializer: {
@@ -175,7 +192,7 @@ const persistedChartConfigs = useStorage(
if (Array.isArray(v)) {
return v;
}
return []; // 解析失败时返回空数组
return [];
} catch (e) {
console.error('解析存储的图表配置失败,使用空配置:', e);
return [];
@@ -183,19 +200,37 @@ const persistedChartConfigs = useStorage(
},
write: (v) => JSON.stringify(v)
},
mergeDefaults: false // 关闭默认值合并
mergeDefaults: false
}
);
// 临时配置(用于编辑)
// 临时配置
const tempChartConfigs = ref([]);
// 保存弹窗打开时的初始配置快照直接来自storage
// 初始配置快照
const initialChartConfigs = ref([]);
// 保存状态
const saving = ref(false);
// 移除拖拽状态变量isDragging
// 窗口尺寸响应
const windowWidth = ref(window.innerWidth);
const tableHeight = ref(300);
// 高度输入错误标记
const heightErrorIndex = ref(-1);
// 批量设置弹窗
const showBatchSettings = ref(false);
// 批量设置数据
const batchSettings = reactive({
applyRange: 'all', // 'all' 或 'visible'
height: null,
layout: {
xs: null,
sm: null,
md: null,
lg: null,
xl: null
}
});
// 监听窗口尺寸变化
useEventListener('resize', () => {
@@ -203,35 +238,45 @@ useEventListener('resize', () => {
calculateTableHeight();
});
// 初始化:从storage读取数据并保存初始快照
// 初始化:补充height默认值
onMounted(() => {
// 确保初始数据是数组
if (!Array.isArray(persistedChartConfigs.value)) {
persistedChartConfigs.value = [];
}
// 初始化临时配置和初始快照
resetToSession();
calculateTableHeight();
});
// 计算表格高度
const calculateTableHeight = () => {
tableHeight.value = Math.max(300, window.innerHeight - 500);
// 根据屏幕高度动态计算表格高度,确保整体布局合理
const panelHeight = window.innerHeight - 220;
tableHeight.value = Math.max(300, Math.min(600, panelHeight));
};
// 重置临时配置为打开弹窗时的状态
// 重置临时配置补充height默认值
const resetToSession = () => {
try {
// 从storage数据读取
const source = Array.isArray(persistedChartConfigs.value)
? persistedChartConfigs.value
const source = Array.isArray(persistedChartConfigs.value)
? persistedChartConfigs.value
: [];
// 深拷贝保存到临时配置和初始快照
const configCopy = JSON.parse(JSON.stringify(source));
// 深拷贝 + 补充height默认值和visible属性
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,39 +284,127 @@ 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;
}
@@ -288,15 +421,47 @@ defineExpose({
</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 +495,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 +512,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>

View File

@@ -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) => {

View File

@@ -36,7 +36,7 @@
<slot></slot>
</main>
<el-dialog v-model="settingVisible" title="图表设置" width="50%">
<el-dialog v-model="settingVisible" title="图表设置" width="70%">
<ChartSetting />
</el-dialog>
</div>