git_rabbit 7a3429dbe0 feat(看板管理): 添加无缝滚动组件并优化看板样式
添加vue-seamless-scroll组件实现表格无缝滚动,优化看板布局和样式适配1920*1080分辨率
移除自定义滚动逻辑,增加时间范围选择功能,调整表格结构为固定表头+滚动内容
2025-11-01 20:04:58 +08:00

999 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="quality-statistics-card">
<!-- 标题部分使用kbHeader组件统一风格 -->
<kb-header>质量统计报表</kb-header>
<!-- 第一行统计数据翻牌器区域 -->
<div class="first-row">
<div class="statistics-section">
<div class="flip-card-container">
<div class="flip-card">
<div class="flip-card-label">报表数量</div>
<div class="flip-card-value">{{ statistics.totalRecords }}</div>
</div>
<div class="flip-card">
<div class="flip-card-label">投入总数</div>
<div class="flip-card-value">{{ statistics.totalInput }}</div>
</div>
<div class="flip-card">
<div class="flip-card-label">合格总数</div>
<div class="flip-card-value completed">{{ statistics.totalQualified }}</div>
</div>
<div class="flip-card">
<div class="flip-card-label">合格率</div>
<div class="flip-card-value">{{ statistics.passRate }}</div>
</div>
<div class="flip-card">
<div class="flip-card-label">抛光总数</div>
<div class="flip-card-value">{{ statistics.totalPolish }}</div>
</div>
<div class="flip-card">
<div class="flip-card-label">打磨总数</div>
<div class="flip-card-value">{{ statistics.totalGrind }}</div>
</div>
<div class="flip-card">
<div class="flip-card-label">报废总数</div>
<div class="flip-card-value scrap">{{ statistics.totalScrap }}</div>
</div>
</div>
<!-- 时间范围切换 -->
<div class="time-range-switch">
<div class="switch-label">数据范围</div>
<div class="switch-options">
<div
class="switch-option"
:class="{ active: timeRange === 'daily' }"
@click="setTimeRange('daily')"
>
每日
</div>
<div
class="switch-option"
:class="{ active: timeRange === 'weekly' }"
@click="setTimeRange('weekly')"
>
每周
</div>
</div>
</div>
</div>
</div>
<!-- 第二行表格和图表区域 -->
<div class="second-row">
<!-- 左侧质量统计表格 -->
<div
class="table-section"
style="flex: 3"
>
<div class="section-title">质量统计列表</div>
<div class="custom-table-wrapper" ref="tableWrapper">
<!-- 固定表头 -->
<table
class="custom-table custom-table-header"
v-if="!loading"
>
<thead>
<tr>
<th width="140">工单号</th>
<th width="120">零件号</th>
<th width="100">颜色</th>
<th width="150">描述</th>
<th width="80">班次</th>
<th width="120">生产投入数</th>
<th width="80">合格数</th>
<th width="80">合格率</th>
<th width="80">抛光总数</th>
<th width="80">打磨总数</th>
<th width="80">报废总数</th>
<th width="108">开始时间</th>
<th width="108">结束时间</th>
</tr>
</thead>
</table>
<!-- 可滚动的表格内容 -->
<div class="table-content-wrapper">
<vue-seamless-scroll
:data="qualityStatisticsTable"
:class-option="scrollOptions"
class="table-scroll"
>
<table class="custom-table custom-table-body">
<tbody>
<tr
v-for="(row, index) in qualityStatisticsTable"
:key="index"
class="table-row"
>
<td width="140">{{ row.workorderId || "-" }}</td>
<td width="120">{{ row.finishedPartNumber || "-" }}</td>
<td width="100">{{ row.color || "-" }}</td>
<td width="150" class="ellipsis">{{ row.productDescription || "-" }}</td>
<td width="80">{{ row.team || "-" }}</td>
<td width="120">{{ row.requireNumber || "-" }}</td>
<td width="80">{{ row.qualifiedNumber || "-" }}</td>
<td width="80">{{ offsetRate(row.qualifiedRate) }}%</td>
<td width="80">{{ row.paoguangTotal || "-" }}</td>
<td width="80">{{ row.damoTotal || "-" }}</td>
<td width="80">{{ row.baofeiTotal || "-" }}</td>
<td width="108">{{ row.startTime || "-" }}</td>
<td width="108">{{ row.endTime || "-" }}</td>
</tr>
</tbody>
</table>
</vue-seamless-scroll>
</div>
<div
class="loading-overlay"
v-if="loading"
>
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</div>
<!-- 右侧图表区域 -->
<div class="charts-section">
<!-- 上半部分合格率分布饼图 -->
<div class="chart-container">
<div class="section-title">合格率分布(86%/86%)</div>
<div
id="qualifiedRatePieChart"
style="width: 100%; height: 200px"
></div>
</div>
<!-- 下半部分平均合格率TOP5柱状图 -->
<div class="chart-container">
<div class="section-title">平均合格率TOP5</div>
<div
id="top5Chart"
style="width: 100%; height: 280px"
></div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getQualityStatisticsCarouselBoardData } from "@/api/kanbanManagement/carouselBoard.js";
import * as echarts from "echarts";
import kbHeader from "./kbHeader.vue";
import dayjs from "dayjs";
import vueSeamlessScroll from "vue-seamless-scroll";
export default {
name: "QualityStatisticsCard",
components: {
kbHeader,
vueSeamlessScroll,
},
data() {
return {
loading: false,
qualityStatisticsTable: [],
tableHeight: "500px",
search: {
startTime: null,
endTime: null,
team: null,
workorderid: null,
partnumber: null,
sortType: 0,
reportType: 0,
product_description: "",
},
// 统计数据对象
statistics: {
totalRecords: 0,
totalInput: 0,
totalQualified: 0,
passRate: "0%",
totalPolish: 0,
totalGrind: 0,
totalScrap: 0,
},
// 时间范围选择
timeRange: "daily", // 'daily'或'weekly'
allDataList: [],
// 图表实例
qualifiedRatePieChart: null,
top5Chart: null,
refreshTimer: null,
};
},
mounted() {
this.init();
this.getQualityData();
window.addEventListener("resize", this.handleResize);
// 设置5分钟定时刷新
this.refreshTimer = setInterval(() => {
this.getQualityData();
}, 5 * 60 * 1000); // 5分钟 = 5 * 60 * 1000毫秒
},
updated() {
// 数据更新后初始化图表
this.$nextTick(() => {
if (this.qualityStatisticsTable.length > 0) {
this.initQualifiedRatePieChart();
this.initTop5Chart();
}
});
},
beforeDestroy() {
window.removeEventListener("resize", this.handleResize);
// 清理图表实例
if (this.qualifiedRatePieChart) {
this.qualifiedRatePieChart.dispose();
this.qualifiedRatePieChart = null;
}
if (this.top5Chart) {
this.top5Chart.dispose();
this.top5Chart = null;
}
// 清理数据刷新定时器
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
},
computed: {
// vue-seamless-scroll配置
scrollOptions() {
return {
step: 0.5, // 滚动速度,值越大速度越快
limitMoveNum: 1, // 开始无缝滚动的数据量,滚动的数据列表要大于等于这个值
hoverStop: false, // 是否开启鼠标悬停停止
direction: 1, // 0 向下 1 向上 2 向左 3 向右
openWatch: true, // 开启数据监听,数据变化时自动滚动
singleHeight: 0, // 单步运动停止的高度(默认值0是无缝不停止的滚动)只有direction为0或1时生效
singleWidth: 0, // 单步运动停止的宽度(默认值0是无缝不停止的滚动)只有direction为2或3时生效
waitTime: 1000, // 单步运动停止的时间(默认值1000ms)
switchOffset: 0, // 左右切换的偏移量
autoPlay: true, // 是否自动播放
switchSingleStep: 150, // 切换步数
switchDelay: 3000, // 切换延迟
ease: "easeInOutQuad", // 缓动函数
easing: "linear", // 动画过渡效果
};
},
},
methods: {
init() {
// 根据默认时间范围设置时间
this.setTimeRange("daily");
},
// 合格率去除小数
offsetRate(num) {
if (num === null || num === "" || isNaN(num)) {
return "0";
}
return parseInt(num);
},
// 设置时间范围
setTimeRange(type) {
this.timeRange = type;
const today = dayjs();
if (type === "daily") {
// 今天
this.search.startTime = today.startOf("day").toDate();
this.search.endTime = today.endOf("day").toDate();
} else if (type === "weekly") {
// 本周(周一到周日)
this.search.startTime = today.startOf("week").toDate();
this.search.endTime = today.endOf("week").toDate();
}
// 重新查询数据
this.getQualityData();
},
// 格式化日期为字符串(传给后台)
formatDateForBackend(date) {
return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
},
// 获取质量数据
getQualityData() {
this.loading = true;
// 设置默认查询参数,将日期转换为字符串格式
let query = {
...this.search,
startTime: this.formatDateForBackend(this.search.startTime),
endTime: this.formatDateForBackend(this.search.endTime),
pageNum: 1,
pageSize: 20,
};
getQualityStatisticsCarouselBoardData(query)
.then((res) => {
if (res.code == 200) {
this.qualityStatisticsTable = res.data || [];
this.allDataList = res.data || [];
// 计算统计数据
this.calculateStatistics();
// 数据加载完成后立即启动滚动
this.$nextTick(() => {
this.startTableAutoScroll();
});
}
})
.finally(() => {
this.loading = false;
});
},
// 计算统计数据
calculateStatistics() {
// 过滤掉倒车雷达数据
const filteredData = this.allDataList.filter((data) => !data.productDescription?.includes("倒车雷达"));
// 统计报表数量
this.statistics.totalRecords = filteredData.length;
// 确保准确计算数字翻牌器数据
// 统计投入数
this.statistics.totalInput = Math.trunc(
filteredData.reduce((acc, data) => {
// 确保数据准确性,只加有效数字
const value = Number(data.requireNumber);
return acc + (isNaN(value) ? 0 : value);
}, 0) / 3
);
// 统计合格数
this.statistics.totalQualified = Math.trunc(
filteredData.reduce((acc, data) => {
const value = Number(data.qualifiedNumber);
return acc + (isNaN(value) ? 0 : value);
}, 0) / 3
);
// 计算合格率
if (this.statistics.totalInput > 0) {
const rate = (this.statistics.totalQualified / this.statistics.totalInput) * 100;
this.statistics.passRate = rate.toFixed(1) + "%";
} else {
this.statistics.passRate = "0%";
}
// 统计抛光数
this.statistics.totalPolish = Math.trunc(
filteredData.reduce((acc, data) => {
const value = Number(data.paoguangTotal);
return acc + (isNaN(value) ? 0 : value);
}, 0) / 3
);
// 统计打磨数
this.statistics.totalGrind = Math.trunc(
filteredData.reduce((acc, data) => {
const value = Number(data.damoTotal);
return acc + (isNaN(value) ? 0 : value);
}, 0) / 3
);
// 统计报废数
this.statistics.totalScrap = Math.trunc(
filteredData.reduce((acc, data) => {
const value = Number(data.baofeiTotal);
return acc + (isNaN(value) ? 0 : value);
}, 0) / 3
);
},
// 初始化合格率分布饼图80%以上和以下的占比)
initQualifiedRatePieChart() {
const chartDom = document.getElementById("qualifiedRatePieChart");
if (!chartDom) return;
if (this.qualifiedRatePieChart) {
this.qualifiedRatePieChart.dispose();
}
this.qualifiedRatePieChart = echarts.init(chartDom);
// 过滤有合格数据的记录
const validData = this.allDataList.filter((item) => item.qualifiedRate !== null && item.qualifiedRate !== "" && !isNaN(item.qualifiedRate));
// 统计合格率86%以上和86%以下的数量
const above86 = validData.filter((item) => parseFloat(item.qualifiedRate) >= 86).length;
const below86 = validData.filter((item) => parseFloat(item.qualifiedRate) < 86).length;
const option = {
tooltip: {
trigger: "item",
formatter: "{b}: {c} ({d}%)",
},
legend: {
orient: "horizontal",
bottom: 10,
data: ["合格率≥86%", "合格率<86%"],
textStyle: {
color: "#00ffff",
fontSize: 12,
},
},
series: [
{
name: "合格率分布",
type: "pie",
radius: ["40%", "70%"],
center: ["50%", "40%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: "rgba(0, 20, 40, 0.6)",
borderWidth: 2,
},
label: {
show: true,
formatter: "{b}\n{d}%",
color: "#ffffff",
fontSize: 12,
},
emphasis: {
label: {
show: true,
fontSize: "14",
fontWeight: "bold",
color: "#00ffff",
},
},
data: [
{ value: above86, name: "合格率≥86%", itemStyle: { color: "#67c23a" } },
{ value: below86, name: "合格率<86%", itemStyle: { color: "#f56c6c" } },
].filter((item) => item.value > 0), // 过滤掉数量为0的类别
},
],
};
this.qualifiedRatePieChart.setOption(option);
},
// 初始化平均合格率TOP5横向柱状图
initTop5Chart() {
const chartDom = document.getElementById("top5Chart");
if (!chartDom) return;
if (this.top5Chart) {
this.top5Chart.dispose();
}
this.top5Chart = echarts.init(chartDom);
// 过滤有合格数据的记录并按合格率降序排序,确保合格率高的显示在顶部
const validData = this.allDataList
.filter((item) => item.qualifiedRate !== null && item.qualifiedRate !== "" && !isNaN(item.qualifiedRate))
.sort((a, b) => parseFloat(b.qualifiedRate) - parseFloat(a.qualifiedRate));
// 获取合格率最高的前5个
const top5 = validData.slice(0, 5);
// 准备横向柱状图数据y轴显示产品描述
const labels = top5.map((item) => {
const desc = item.productDescription || item.finishedPartNumber || "未知";
// 截取前20个字符优先显示描述
const shortDesc = desc.length > 20 ? desc.substring(0, 20) + "..." : desc;
return shortDesc;
});
const rates = top5.map((item) => parseFloat(item.qualifiedRate) || 0);
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
formatter: function (params) {
const data = params[0];
return data.name + "<br/>合格率: " + data.value + "%";
},
},
grid: {
left: "5%", // 减小左侧边距,让图表整体向左移动
right: "10%",
top: "10%",
bottom: "10%",
containLabel: true,
},
xAxis: {
type: "value",
min: 0,
max: 100,
axisLabel: {
formatter: "{value}%",
color: "#ffffff",
fontSize: 12,
},
splitLine: {
lineStyle: {
color: "rgba(255, 255, 255, 0.1)",
},
},
},
yAxis: {
type: "category",
data: labels,
axisLabel: {
color: "#00ffff",
fontSize: 12,
interval: 0,
formatter: function (value) {
// 自动换行每行最多显示8个字符
const maxLength = 8;
let result = "";
for (let i = 0; i < value.length; i += maxLength) {
result += value.substring(i, i + maxLength) + "\n";
}
return result.trim();
},
},
},
series: [
{
name: "合格率",
type: "bar",
data: rates,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: "#0066ff" },
{ offset: 1, color: "#00ffff" },
]),
},
barWidth: "50%",
label: {
show: true,
position: "right",
color: "#ffffff",
formatter: "{c}%",
},
},
],
};
this.top5Chart.setOption(option);
},
// 处理窗口大小变化
handleResize() {
// 调整图表大小
if (this.qualifiedRatePieChart) {
this.qualifiedRatePieChart.resize();
}
if (this.top5Chart) {
this.top5Chart.resize();
}
},
},
};
</script>
<style scoped>
.quality-statistics-card {
padding: 15px;
background: transparent;
width: 100%;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 20px;
overflow-x: auto; /* 允许容器水平滚动,确保内容不被截断 */
}
/* 时间范围切换样式 */
.time-range-switch {
padding: 8px 0;
text-align: center;
background: #f5f7fa;
border-radius: 4px;
}
/* 第一行布局 - 1920*1080优化 */
.first-row {
width: 100%;
flex-shrink: 0;
height: 200px !important;
margin-bottom: 10px; /* 减小底部间距 */
}
/* 第二行布局 - 1920*1080优化 */
.second-row {
display: flex;
gap: 20px;
flex: 1;
min-height: 0;
overflow: hidden;
}
.table-section {
flex: 2; /* 调整为2:1的比例 */
min-width: 0; /* 允许flex项收缩 */
display: flex;
flex-direction: column;
}
.charts-section {
flex: 1; /* 调整为2:1的比例 */
min-width: 0; /* 允许flex项收缩 */
}
/* 统计翻牌器样式 */
.statistics-section {
background: rgba(0, 20, 40, 0.6);
border-radius: 8px;
border: 1px solid rgba(0, 255, 255, 0.3);
padding: 15px;
display: flex;
flex-direction: column;
}
/* 时间范围切换样式 */
.time-range-switch {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
padding: 10px;
background: rgba(0, 60, 120, 0.5);
border-radius: 6px;
border: 1px solid rgba(0, 255, 255, 0.2);
}
.switch-label {
color: #00ffff;
font-size: 14px;
margin-right: 15px;
font-weight: bold;
}
.switch-options {
display: flex;
gap: 10px;
}
.switch-option {
padding: 6px 20px;
background: rgba(0, 40, 80, 0.8);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 4px;
color: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.switch-option:hover {
background: rgba(0, 255, 255, 0.2);
border-color: #00ffff;
color: #00ffff;
}
.switch-option.active {
background: rgba(0, 255, 255, 0.3);
border-color: #00ffff;
color: #00ffff;
font-weight: bold;
}
.flip-card-container {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.flip-card {
flex: 1;
min-width: calc(100% / 7 - 15px * 6 / 7); /* 大致保持7列布局 */
min-height: 0; /* 允许flex项收缩 */
}
.flip-card {
background: rgba(0, 60, 120, 0.8);
border-radius: 6px;
padding: 15px 10px;
text-align: center;
border: 1px solid rgba(0, 255, 255, 0.2);
transition: all 0.3s ease;
}
.flip-card:hover {
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
transform: translateY(-2px);
}
.flip-card-label {
color: #ffffff;
font-size: 12px;
margin-bottom: 8px;
}
.flip-card-value {
color: #00ffff;
font-size: 24px;
font-weight: bold;
font-family: "Courier New", monospace;
letter-spacing: 2px;
}
.flip-card-value.completed {
color: #67c23a;
}
.flip-card-value.scrap {
color: #f56c6c;
}
/* 通用区域样式 */
.table-section,
.charts-section,
.chart-container {
background: rgba(0, 20, 40, 0.6);
border-radius: 8px;
border: 1px solid rgba(0, 255, 255, 0.3);
padding: 15px;
display: flex;
flex-direction: column;
}
.charts-section {
gap: 15px;
}
.chart-container {
flex: 1;
min-height: 0;
}
.section-title {
color: #00ffff;
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
text-align: center;
padding-bottom: 8px;
border-bottom: 1px solid rgba(0, 255, 255, 0.3);
}
/* 自定义表格样式 */
.custom-table-wrapper {
flex: 1;
overflow: hidden;
background: rgba(0, 60, 120, 0.8);
border-radius: 6px;
position: relative;
display: flex;
flex-direction: column;
}
/* 固定表头样式 */
.custom-table-header {
width: 100%;
border-collapse: collapse;
color: #ffffff;
background: rgba(0, 60, 120, 1);
}
.custom-table-header th {
background: rgba(0, 60, 120, 1);
color: #00ffff;
font-weight: bold;
text-align: center;
padding: 12px 5px;
border-bottom: 1px solid rgba(0, 255, 255, 0.3);
font-size: 13px;
box-sizing: border-box;
}
/* 内容区域样式 */
.table-content-wrapper {
flex: 1;
overflow: hidden;
max-height: calc(100% - 46px); /* 减去表头高度 */
}
/* vue-seamless-scroll样式 */
.table-scroll {
height: 100%;
width: 100%;
}
/* 隐藏滚动条 */
.table-scroll::-webkit-scrollbar {
display: none;
}
.table-scroll {
-ms-overflow-style: none;
scrollbar-width: none;
}
.custom-table-body {
width: 100%;
border-collapse: collapse;
color: #ffffff;
}
.custom-table-body td {
text-align: center;
padding: 10px 5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 13px;
box-sizing: border-box;
}
.table-row:hover td {
background-color: rgba(0, 255, 255, 0.1);
}
/* 加载样式 */
.loading-overlay {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #00ffff;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(0, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #00ffff;
animation: spin 1s ease-in-out infinite;
margin-bottom: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 14px;
}
/* 超出省略 */
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
/* 滚动条样式 */
.custom-table-wrapper::-webkit-scrollbar {
width: 8px;
}
.custom-table-wrapper::-webkit-scrollbar-track {
background: rgba(0, 60, 120, 0.5);
border-radius: 4px;
}
.custom-table-wrapper::-webkit-scrollbar-thumb {
background: rgba(0, 255, 255, 0.5);
border-radius: 4px;
}
.custom-table-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(0, 255, 255, 0.8);
}
/* 响应式调整 */
/* 1920*1080分辨率专属样式 */
@media (min-width: 1920px) and (min-height: 1080px) {
.quality-statistics-card {
padding: 20px;
}
.section-title {
font-size: 18px;
}
.flip-card-label {
font-size: 14px;
}
.flip-card-value {
font-size: 28px;
}
.custom-table th,
.custom-table td {
font-size: 14px;
}
.flip-card {
padding: 20px;
}
}
/* 大屏响应式调整 */
@media (min-width: 1600px) and (max-width: 1919px) {
.first-row {
height: 200px !important;
}
.flip-card-value {
font-size: 24px;
}
}
/* 中小屏响应式调整 */
@media (max-width: 1400px) {
.flip-card {
min-width: calc(100% / 5 - 15px * 4 / 5); /* 调整为5列布局 */
}
}
@media (max-width: 1200px) {
.flip-card {
min-width: calc(100% / 4 - 15px * 3 / 4); /* 调整为4列布局 */
}
.second-row {
flex-direction: column;
}
.table-section,
.charts-section {
flex: 1;
width: 100%;
}
.charts-section {
gap: 10px;
}
}
@media (max-width: 1024px) {
.flip-card {
min-width: calc(100% / 3 - 15px * 2 / 3); /* 调整为3列布局 */
}
.second-row {
flex-direction: column;
}
.charts-section {
flex-direction: row;
}
}
@media (max-width: 768px) {
.flip-card {
min-width: calc(100% / 2 - 15px * 1 / 2); /* 调整为2列布局 */
}
.charts-section {
flex-direction: column;
}
.custom-table {
font-size: 12px;
}
.custom-table th,
.custom-table td {
padding: 8px 2px;
font-size: 11px;
}
}
</style>