列表自动滚动

This commit is contained in:
Teo
2025-05-09 18:41:08 +08:00
parent 609b4ba543
commit 8890fcfd95
8 changed files with 472 additions and 131 deletions

View File

@ -55,6 +55,7 @@
"vue-router": "4.4.5",
"vue-types": "5.1.3",
"vue3-print-nb": "^0.1.4",
"vue3-scroll-seamless": "^1.0.6",
"vxe-table": "4.5.22"
},
"devDependencies": {

58
src/api/gis/index.ts Normal file
View File

@ -0,0 +1,58 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { QualityVO, Query, ConstructionUserVO, MachineryrVO, MaterialsVO } from './type';
/**
* 查询大屏质量信息
* @param query
* @returns {*}
*/
export const getQualityList = (query?: Query): AxiosPromise<QualityVO> => {
return request({
url: '/quality/qualityInspection/gis',
method: 'get',
params: query
});
};
/**
* 查询施工人员大屏数据
* @param query
* @returns {*}
*/
export const getConstructionUserList = (query?: Query): AxiosPromise<ConstructionUserVO> => {
return request({
url: '/project/constructionUser/gis',
method: 'get',
params: query
});
};
/**
* 查询大屏机械列表
* @param query
* @returns {*}
*/
export const getMachineryrList = (query?: Query): AxiosPromise<MachineryrVO[]> => {
return request({
url: '/machinery/machinery/list/gis',
method: 'get',
params: query
});
};
/**
* 查询大屏材料信息
* @param query
* @returns {*}
*/
export const getMaterialsList = (query?: Query): AxiosPromise<MaterialsVO[]> => {
return request({
url: '/materials/materials/list/gis',
method: 'get',
params: query
});
};

43
src/api/gis/type.ts Normal file
View File

@ -0,0 +1,43 @@
export interface QualityVO {
count: number;
correctSituation: string;
list: Qualitylist[];
}
export interface Qualitylist {
id: number;
inspectionTypeLabel: string;
inspectionHeadline: string;
createTime: string;
}
export interface Query {
projectId: string | number;
pageSize?: string | number;
}
export interface ConstructionUserVO {
peopleCount: number;
attendanceCount: number;
attendanceRate: string;
}
export interface MachineryrVO {
//机械名称
machineryName: string;
//机械数量
machineryCount: string;
}
export interface MaterialsVO {
//材料名称
materialsName: string;
//计量单位
weightId: string;
//预计材料数量
quantityCount: string;
//入库数量
putCount: string;
//出库数量
outCount: string;
value: number;
}

View File

@ -49,6 +49,8 @@ VXETable.config({
// 修改 el-dialog 默认点击遮照为不关闭
/*import { ElDialog } from 'element-plus';
ElDialog.props.closeOnClickModal.default = false;*/
// **main.js**
import { vue3ScrollSeamless } from 'vue3-scroll-seamless';
const app = createApp(App);
@ -60,6 +62,7 @@ app.use(print);
app.use(i18n);
app.use(VXETable);
app.use(plugins);
app.component('vue3ScrollSeamless', vue3ScrollSeamless);
// 自定义指令
directive(app);

View File

@ -0,0 +1,125 @@
<template>
<div class="auto-scroll-container" @mouseenter="pauseScroll" @mouseleave="resumeScroll" ref="container">
<div class="auto-scroll-content" ref="content">
<div class="auto-scroll-item" v-for="(item, index) in duplicatedList" :key="index">
<slot :item="item">{{ item }}</slot>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
// Props
const props = defineProps({
items: {
type: Array,
required: true
},
speed: {
type: Number,
default: 0.5 // px/frame
},
height: {
type: Number,
default: 100 // px
},
minItems: {
type: Number,
default: 2 // 小于这个数量不滚动
},
autoScroll: {
type: Boolean,
default: true // 控制是否自动滚动
}
});
// Refs and Computed
const container = ref(null);
const content = ref(null);
const duplicatedList = computed(() => [...props.items, ...props.items]);
const shouldScroll = computed(() => props.items.length >= props.minItems);
let scrollY = 0;
let animationFrameId = null;
let manualPaused = false; // 记录是否因为滚轮手动停止
// 滚动核心逻辑
function normalizeScrollY(contentHeight) {
if (scrollY <= -contentHeight) scrollY += contentHeight;
if (scrollY >= 0) scrollY -= contentHeight;
}
function step() {
const contentHeight = content.value.offsetHeight / 2;
scrollY -= props.speed;
normalizeScrollY(contentHeight);
content.value.style.transform = `translateY(${Math.round(scrollY)}px)`;
animationFrameId = requestAnimationFrame(step);
}
function startScroll() {
if (!animationFrameId) {
animationFrameId = requestAnimationFrame(step);
}
}
function pauseScroll() {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 鼠标滚轮事件:手动滚动并停止自动滚动
function onWheel(e) {
pauseScroll();
manualPaused = true; // 标记为手动停止
const contentHeight = content.value.offsetHeight / 2;
scrollY -= e.deltaY;
normalizeScrollY(contentHeight);
content.value.style.transform = `translateY(${Math.round(scrollY)}px)`;
}
// 鼠标移出时恢复自动滚动(如果不是手动暂停)
function resumeScroll() {
if (props.autoScroll && shouldScroll.value) {
manualPaused = false; // 重置手动暂停标志
startScroll();
}
}
// 生命周期
onMounted(() => {
if (shouldScroll.value && props.autoScroll) {
startScroll();
}
container.value.addEventListener('wheel', onWheel);
});
onUnmounted(() => {
pauseScroll();
container.value.removeEventListener('wheel', onWheel);
});
// 响应 items 数量变化
watch(shouldScroll, (newVal) => {
if (!newVal) {
pauseScroll();
} else if (props.autoScroll && !manualPaused) {
startScroll();
}
});
</script>
<style scoped>
.auto-scroll-container {
overflow: hidden;
position: relative;
}
.auto-scroll-content {
display: flex;
flex-direction: column;
will-change: transform;
}
</style>

View File

@ -21,21 +21,30 @@
<img src="@/assets/images/totalnumber.png" alt="" />
</div>
<p>总人数</p>
<div class="peopleNum"><span>259</span></div>
<div class="peopleNum">
<span>{{ constructionUserData?.peopleCount }}</span
>
</div>
</div>
<div class="card">
<div class="iconImg">
<img src="@/assets/images/attendanceperson.png" alt="" />
</div>
<p>出勤人</p>
<div class="peopleNum"><span>259</span></div>
<div class="peopleNum">
<span>{{ constructionUserData?.attendanceCount }}</span
>
</div>
</div>
<div class="card">
<div class="iconImg">
<img src="@/assets/images/Attendancerate.png" alt="" />
</div>
<p>出勤率</p>
<div class="peopleNum"><span>100</span>%</div>
<div class="peopleNum">
<span>{{ constructionUserData?.attendanceRate }}</span
>%
</div>
</div>
</div>
<div class="title">
@ -75,20 +84,57 @@
</template>
<script lang="ts" setup>
import { getConstructionUserList, getMachineryrList, getMaterialsList } from '@/api/gis';
import { ConstructionUserVO, MachineryrVO, MaterialsVO } from '@/api/gis/type';
import * as echarts from 'echarts';
import { useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook();
//echarts节点
const myMachineryChart = ref(null);
const myOrderChart = ref(null);
type EChartsOption = echarts.EChartsOption;
const constructionUserData = ref<ConstructionUserVO>(null);
const machineryOption = ref<MachineryrVO[]>([]); //机械
const orderOption = ref<MaterialsVO[]>([]); //材料
const machineryDataAxis = computed(() => machineryOption.value.map((item) => item.machineryName)); //x轴数据
const machineryData = computed(() => machineryOption.value.map((item) => item.machineryCount)); //柱状图数据
const orderDataAxis = computed(() => orderOption.value.map((item) => item.materialsName)); //材料x轴数据
const orderPutData = computed(() => orderOption.value.map((item) => item.putCount)); //柱状图领用量数据
const orderOutData = computed(() => orderOption.value.map((item) => item.outCount)); //柱状图出库量数据
const orderRankingData = computed(() => orderOption.value.map((item) => item.value)); //柱状图库存数据
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
//获取施工人员信息
const getConstructionUserData = async () => {
const res = await getConstructionUserList({ projectId: currentProject.value.id });
if (res.code !== 200) return;
constructionUserData.value = res.data;
};
//查询大屏机械列表
const getMachineryData = async () => {
const res = await getMachineryrList({ projectId: currentProject.value.id });
if (res.code !== 200) return;
machineryOption.value = res.data;
initMachinerycharts();
};
//查询大屏材料信息
const getOrderData = async () => {
const res = await getMaterialsList({ projectId: currentProject.value.id });
if (res.code !== 200) return;
orderOption.value = res.data;
initOrderChart();
console.log(orderDataAxis);
};
const initMachinerycharts = () => {
let chartDom = document.getElementById('machineryMain');
let myMachineryChart = echarts.init(chartDom);
myMachineryChart.value = markRaw(echarts.init(chartDom));
let option: EChartsOption;
// prettier-ignore
let dataAxis = ['水泥机', '搅拌机', '拖拉机', '推土机', '推土机', '推土机','推土机', ];
// prettier-ignore
let data = [11, 23, 21, 20, 22, 24, 24];
option = {
title: {
subtext: '单位:台数'
@ -99,7 +145,7 @@ const initMachinerycharts = () => {
bottom: '50vh'
},
xAxis: {
data: dataAxis,
data: machineryDataAxis.value,
axisLabel: {
// inside: true,
color: 'rgba(202, 218, 226, 1)'
@ -163,7 +209,6 @@ const initMachinerycharts = () => {
},
showDataShadow: false,
// 手柄大小
handleSize: 0,
showDetail: false, //即拖拽时候是否显示详细数值信息 默认true
moveHandleSize: 0, //移动手柄的大小
// 滚动条高度
@ -190,7 +235,7 @@ const initMachinerycharts = () => {
},
start: 0,
// 计算初始结束百分比
end: (6 / data.length) * 100
end: (6 / machineryData.value.length) * 100
}
],
@ -205,21 +250,19 @@ const initMachinerycharts = () => {
])
},
barWidth: '13vh',
data: data
data: machineryData.value
}
]
};
option && myMachineryChart.setOption(option);
option && myMachineryChart.value.setOption(option);
};
const initOrderChart = () => {
let chartDom = document.getElementById('orderMain');
let myMachineryChart = echarts.init(chartDom);
let option: EChartsOption;
// prettier-ignore
let data = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
myOrderChart.value = markRaw(echarts.init(chartDom));
let option: EChartsOption;
option = {
tooltip: {
@ -244,20 +287,21 @@ const initOrderChart = () => {
}
},
grid: {
left: '3%',
left: '5%', //距离dom间距
right: '4%',
bottom: '0',
containLabel: true,
width: '90%'
top: '20%',
bottom: '1%',
height: '80%'
},
xAxis: {
type: 'value',
show: false
},
yAxis: {
type: 'category',
data,
offset: 60,
data: orderDataAxis.value,
offset: 0,
axisLine: {
show: false
},
@ -266,13 +310,38 @@ const initOrderChart = () => {
},
inverse: true,
axisLabel: {
formatter: function (value, index) {
return `{${data[index]}|No.${index + 1}} {value|${value}}`;
formatter: function (value) {
let bgType = '';
let index = orderDataAxis.value.indexOf(value);
switch (index) {
case 0:
bgType = 'a';
break;
case 1:
bgType = 'b';
break;
case 2:
bgType = 'c';
break;
default:
return `No.${index + 1} {value|${value}}`;
}
return `{${bgType}|No.${index + 1}} {value|${value}}`;
},
align: 'left',
verticalAlign: 'bottom',
yAxisIndex: 0,
// 横坐标 分割线等取消显示
padding: [0, 10, 10, 6],
axisTick: {
show: false
},
axisLine: {
show: false
},
color: 'rgba(230, 247, 255, 1)',
rich: {
Mon: {
a: {
color: 'rgba(230, 247, 255, 1)',
fontSize: 12,
align: 'center',
@ -283,7 +352,7 @@ const initOrderChart = () => {
{ offset: 1, color: 'rgba(255, 208, 59, 0)' }
])
},
Tue: {
b: {
color: 'rgba(230, 247, 255, 1)',
fontSize: 12,
align: 'left',
@ -294,7 +363,7 @@ const initOrderChart = () => {
{ offset: 1, color: 'rgba(31, 189, 237, 0)' }
])
},
Wed: {
c: {
color: 'rgba(230, 247, 255, 1)',
fontSize: 12,
align: 'left',
@ -408,7 +477,7 @@ const initOrderChart = () => {
emphasis: {
focus: 'series'
},
data: [320, 302, 301, 334, 345, 356, 367],
data: orderPutData.value,
barWidth: 3
},
{
@ -421,7 +490,7 @@ const initOrderChart = () => {
emphasis: {
focus: 'series'
},
data: [120, 132, 101, 134, 152, 103, 150],
data: orderOutData.value,
showBackground: true,
barWidth: 3,
backgroundStyle: {
@ -430,12 +499,34 @@ const initOrderChart = () => {
}
]
};
option && myMachineryChart.setOption(option);
option && myOrderChart.value.setOption(option);
};
onMounted(() => {
// 防抖函数
const debounce = <T,>(func: (this: T, ...args: any[]) => void, delay: number) => {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: T, ...args: any[]) {
const context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
};
// 窗口大小变化时触发的函数
const handleResize = () => {
myMachineryChart.value && myMachineryChart.value.dispose();
myOrderChart.value && myOrderChart.value.dispose();
initMachinerycharts();
initOrderChart();
};
const debouncedHandleResize = debounce(handleResize, 300);
onMounted(() => {
getOrderData();
getConstructionUserData();
getMachineryData();
window.addEventListener('resize', debouncedHandleResize); //监听窗口变化重新生成echarts
});
</script>

View File

@ -17,19 +17,22 @@
</div>
<div class="events">
<div class="content events-content event_s">
<ul
class="events-list"
@mouseenter.native="autoScrollTable(true, 'projectRef')"
@mouseleave.native="autoScrollTable(false, 'projectRef')"
id="event_scroll"
ref="projectScroll"
>
<li v-for="(item, index) in events" :key="index">
<ul class="events-list">
<!-- <li v-for="(item, index) in events" :key="index">
<span class="text detail" style="display: inline"> {{ item.headline }}...</span>
<span class="more" v-if="!item.show" @click="onMore(item, true)">查看详情</span>
<span class="more" style="color: #ffb100eb" v-else @click="onMore(item, false)">关闭详情</span>
</li>
</li> -->
</ul>
<AutoScroller :items="events" class="events-list">
<template #default="{ item }">
<li>
<span class="text detail" style="display: inline"> {{ item.headline }}...</span>
<span class="more" v-if="!item.show" @click="onMore(item, true)">查看详情</span>
<span class="more" style="color: #ffb100eb" v-else @click="onMore(item, false)">关闭详情</span>
</li>
</template>
</AutoScroller>
<!-- <span v-else style="font-size: 20px; letter-spacing: 10px">暂无数据</span> -->
</div>
</div>
@ -93,31 +96,28 @@
<el-table-column prop="status" label="操作" />
</el-table>
</div>
<div
class="tbody"
ref="tableScroll"
@mouseenter.native="autoScrollTable(true, 'tableRef')"
@mouseleave.native="autoScrollTable(false, 'tableRef')"
>
<el-table
:data="safetyData"
stripe
row-class-name="bg-transparent"
cell-class-name="bg-transparent"
header-row-class-name="header-row-bg-transparent"
header-cell-class-name="bg-transparent"
style="--el-table-border-color: none"
>
<el-table-column prop="teamName" label="" />
<el-table-column prop="name" label="" />
<el-table-column prop="meetingDate" label="" />
<el-table-column prop="status" label="">
<template #default="scope">
<el-link type="primary" :underline="false">查看</el-link>
</template>
</el-table-column>
</el-table>
</div>
<AutoScroller :items="safetyData" class="tbody">
<template #default="{ item }">
<el-table
:data="safetyData"
stripe
row-class-name="bg-transparent"
cell-class-name="bg-transparent"
header-row-class-name="header-row-bg-transparent"
header-cell-class-name="bg-transparent"
style="--el-table-border-color: none"
>
<el-table-column prop="teamName" label="" class-name="teamNameWidth" />
<el-table-column prop="name" label="" class-name="nameWidth" />
<el-table-column prop="meetingDate" label="" class-name="meetingDateWidth" />
<el-table-column prop="status" label="" class-name="statusWidth">
<template #default="scope">
<el-link :underline="false">查看</el-link>
</template>
</el-table-column>
</el-table>
</template>
</AutoScroller>
</div>
</div>
<div class="title">
@ -141,30 +141,27 @@
</div>
<div class="qualityNum">
<div>巡检记录 <b></b></div>
<p>14<span></span></p>
<p>{{ qualityData?.count }}<span></span></p>
</div>
<div class="qualityNum ml-15">
<div>整改情况 <b></b></div>
<p>20<span>%</span></p>
<p>{{ qualityData?.correctSituation }}<span>%</span></p>
</div>
</div>
<div
class="qualityList"
@mouseenter.native="autoScrollTable(true, 'qualityRef')"
@mouseleave.native="autoScrollTable(false, 'qualityRef')"
ref="qualityScroll"
>
<div class="qualityItem flex items-center" v-for="item in 6">
<div>
<img src="@/assets/images/timeIcon.png" alt="" />
<span class="text-white">2024-11-15</span>
<AutoScroller :items="qualityData?.list" class="qualityList">
<template #default="{ item }">
<div class="qualityItem flex items-center">
<div>
<img src="@/assets/images/timeIcon.png" alt="" />
<span class="text-white">{{ item.createTime }}</span>
</div>
<div class="text-#43E2CB record">{{ item.inspectionTypeLabel }}</div>
<div class="text-#E6F7FF text-truncate">
<el-tooltip :content="item.inspectionHeadline" placement="top"> {{ item.inspectionHeadline }}</el-tooltip>
</div>
</div>
<div class="text-#43E2CB record">巡检记录</div>
<div class="text-#E6F7FF text-truncate">
<el-tooltip content="桩基钻孔深度、直径不足11111111" placement="top"> 桩基钻孔深度直径不足</el-tooltip>
</div>
</div>
</div>
</template>
</AutoScroller>
</div>
<div class="title">
<div class="flex items-center">
@ -189,7 +186,12 @@
<script lang="ts" setup>
import * as echarts from 'echarts';
//页面高度
import { QualityVO } from '@/api/gis/type';
import { useUserStoreHook } from '@/store/modules/user';
import { getQualityList } from '@/api/gis';
import AutoScroller from './autoScroller.vue';
const userStore = useUserStoreHook();
type EChartsOption = echarts.EChartsOption;
const option = ref<EChartsOption>(null);
const myMachineryChart = ref(null);
@ -210,13 +212,23 @@ const scrollList = reactive({
intervalId: null
}
});
const autoScrollTable = (isAuto, ref) => {
if (isAuto) {
clearInterval(scrollList[ref].intervalId);
} else {
startScroll(ref);
}
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const qualityData = ref<QualityVO>({ list: [], correctSituation: null, count: null });
const classOptions = {
limitMoveNum: 3,
hoverStop: true,
step: 1
};
//获取质量信息
const getQualityData = async () => {
const res = await getQualityList({ projectId: currentProject.value.id });
if (res.code !== 200) return;
qualityData.value = res.data;
};
const events = ref([
{
'id': 23,
@ -390,23 +402,6 @@ const initUserChart = () => {
option.value && myMachineryChart.value.setOption(option.value);
};
//开始滚动
const startScroll = (ref) => {
const { dom } = scrollList[ref];
const scrollContainer = dom.parentNode;
scrollList[ref].intervalId = setInterval(() => {
dom.scrollTop += 1;
if (dom.scrollHeight == dom.clientHeight + dom.scrollTop) {
dom.scrollTop = 0;
}
}, 50);
};
//停止滚动
const stopScroll = (intervalId) => {
clearInterval(intervalId);
};
// 防抖函数
const debounce = <T,>(func: (this: T, ...args: any[]) => void, delay: number) => {
let timer: ReturnType<typeof setTimeout> | null = null;
@ -429,19 +424,11 @@ const debouncedHandleResize = debounce(handleResize, 300);
onMounted(() => {
initUserChart();
console.log(scrollList, 'scrollList');
getQualityData();
window.addEventListener('resize', debouncedHandleResize);
for (const key in scrollList) {
startScroll(key);
}
});
onUnmounted(() => {
for (const key in scrollList) {
stopScroll(scrollList[key].intervalId);
}
window.removeEventListener('resize', debouncedHandleResize);
});
</script>
@ -617,8 +604,9 @@ onUnmounted(() => {
.qualityList {
margin-left: vw(21);
display: block;
height: vh(90);
overflow: auto;
overflow: hidden;
margin-right: vw(10);
font-size: vw(14);
.qualityItem {
@ -682,12 +670,12 @@ p {
height: vh(82);
.events-list {
height: 100%;
overflow: auto;
overflow: hidden;
padding-right: vw(10);
margin: 0;
padding: 0;
> li {
li {
width: 100%;
padding-left: vw(20);
background: url('@/assets/images/li.png') no-repeat 0 20%;
@ -724,11 +712,11 @@ p {
}
}
> li:last-child {
margin-bottom: 0;
}
// li:last-child {
// margin-bottom: 0;
// }
> li::after {
li::after {
content: '';
border-left: vw(1) dashed rgba(0, 190, 247, 0.3);
position: absolute;
@ -739,10 +727,10 @@ p {
display: block;
}
> li:last-child::after {
content: '';
border-left: none;
}
// li:last-child::after {
// content: '';
// border-left: none;
// }
}
}
@ -763,7 +751,7 @@ p {
.tbody {
height: vh(94);
overflow: auto;
overflow: hidden;
padding-right: vw(14);
}
@ -781,10 +769,22 @@ p {
border: none; //这是设置透明边框
color: rgba(255, 255, 255, 1); //这是设置字体颜色
font-size: vw(12);
text-align: center;
text-align: left;
padding: vh(4) 0;
height: vh(26) !important;
}
.nameWidth {
width: vw(114);
}
.meetingDateWidth {
width: vw(114);
}
.statusWidth {
width: vw(44);
}
.teamNameWidth {
width: vw(114);
}
.el-table__row--striped {
background: transparent !important; //这是设置透明背景色
@ -815,6 +815,8 @@ p {
/* 滚动条整体样式 */
::-webkit-scrollbar {
width: vw(6);
display: none;
/* 纵向滚动条宽度 */
}

View File

@ -17,12 +17,30 @@
</div>
</div>
</div>
<div>
<AutoScroller :items="list2" :speed="0.7" class="h25" />
<!-- <AutoScroller :items="list2" :height="150" :speed="1.2" /> -->
</div>
</template>
<script setup name="Index" lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import AutoScroller from './gisHome/component/autoScroller.vue';
const list1 = ['列表1 - 内容 A', '列表1 - 内容 B', '列表1 - 内容 C'];
const list2 = [
'列表2 - 第一条长内容测试',
'列表2 - 第二条长长长内容继续测试',
'列表2 - 第三条内容',
'列表2 - 第一条长内容测试',
'列表2 - 第二条长长长内容继续测试',
'列表2 - 第三条内容',
'列表2 - 第一条长内容测试',
'列表2 - 第二条长长长内容继续测试',
'列表2 - 第三条内容'
];
const router = useRouter();
// 模拟数据