Files
m2pool_web_frontend/mining-pool/src/components/HorizontalBroadcast.vue

724 lines
18 KiB
Vue
Raw Normal View History

<template>
<div class="horizontal-broadcast-container" v-if="shouldShowBroadcast">
<!-- 广播标题区域 -->
<div class="broadcast-header" v-if="showTitle">
<i class="iconfont icon-tishishuoming" v-if="showIcon"></i>
<span class="broadcast-title">{{ $t('home.describeTitle') }}</span>
</div>
<!-- 横向滚动区域 -->
<div
class="horizontal-scroll-wrapper"
:class="{ 'is-hovering': isHovering, 'full-width': !showTitle }"
@mouseenter="stopHorizontalScroll"
@mouseleave="resumeHorizontalScroll"
@touchstart="stopHorizontalScroll"
@touchend="startHorizontalScrollDelayed"
:title="isHovering ? $t(`backendSystem.broadcastPause`) || '广播已暂停,移开鼠标继续滚动' : $t(`backendSystem.broadcastResume`) || '鼠标悬停可暂停滚动'"
>
<div
class="horizontal-scroll-content"
:style="horizontalScrollStyle"
ref="horizontalScrollContent"
>
<div
v-for="(item, index) in broadcastListForHorizontal"
:key="item.id + '-horizontal-' + index"
class="horizontal-broadcast-item"
@click="handleItemClick(item)"
>
<span class="horizontal-item-content">{{ item.content }}</span>
<!-- 动态渲染多个按钮 -->
<div class="button-group" v-if="item.buttonContent && item.buttonContent.length > 0">
<span
v-for="(buttonText, buttonIndex) in item.buttonContent"
:key="`button-${item.id}-${buttonIndex}`"
class="view"
@click.stop="handelJump(item.buttonPath[buttonIndex])"
>
{{ buttonText || $t(`home.view`) }}
</span>
</div>
<span class="horizontal-item-separator" v-if="index < broadcastListForHorizontal.length - 1"></span>
</div>
</div>
</div>
</div>
</template>
<script>
import { getBroadcast } from '../api/broadcast'
export default {
name: 'HorizontalBroadcast',
props: {
// 广播数据,如果不传则自动获取
broadcastData: {
type: Array,
default: () => []
},
// 是否显示标题
showTitle: {
type: Boolean,
default: true
},
// 是否显示图标
showIcon: {
type: Boolean,
default: true
},
// 自定义标题
title: {
type: String,
default: ''
},
// 滚动速度 (px/step)
scrollSpeed: {
type: Number,
default: 1
},
// 滚动间隔 (ms)
scrollInterval: {
type: Number,
default: 50
},
// 是否自动获取数据
autoFetch: {
type: Boolean,
default: true
},
// 获取数据的语言参数
lang: {
type: String,
default: 'zh'
},
// 最小显示条数(少于此数不显示组件)
minItems: {
type: Number,
default: 1
},
// 组件高度
height: {
type: String,
default: '40px'
},
// 是否启用点击事件
clickable: {
type: Boolean,
default: false
}
},
data() {
return {
// 内部广播数据
internalBroadcastList: [],
// 横向滚动相关
horizontalScrollTimer: null,
horizontalDelayTimer: null,
horizontalScrollOffset: 0,
isHorizontalScrolling: true,
// hover状态控制
isHovering: false,
wasScrollingBeforeHover: true,
// 组件状态
isLoading: false,
hasError: false,
}
},
computed: {
// 最终使用的广播数据
finalBroadcastList() {
let sourceData = this.broadcastData.length > 0
? this.broadcastData
: this.internalBroadcastList;
// 统一处理数据格式转换
const processedData = sourceData.map(item => {
const processedItem = { ...item };
// 处理 buttonContent如果是字符串则分割为数组
if (typeof processedItem.buttonContent === 'string' && processedItem.buttonContent.trim()) {
processedItem.buttonContent = processedItem.buttonContent
.split(/[,]\s*/) // 支持中英文逗号,后面可能跟空格
.map(btn => btn.trim())
.filter(btn => btn); // 过滤空字符串
} else if (!Array.isArray(processedItem.buttonContent)) {
processedItem.buttonContent = [];
}
// 处理 buttonPath如果是字符串则分割为数组
if (typeof processedItem.buttonPath === 'string' && processedItem.buttonPath.trim()) {
processedItem.buttonPath = processedItem.buttonPath
.split(/[,]\s*/) // 支持中英文逗号,后面可能跟空格
.map(path => path.trim())
.filter(path => path); // 过滤空字符串
} else if (!Array.isArray(processedItem.buttonPath)) {
processedItem.buttonPath = [];
}
// 只有当两个数组都有内容时才进行长度校验
if (processedItem.buttonContent.length > 0 && processedItem.buttonPath.length > 0) {
// 确保两个数组长度一致,以较短的为准
const minLength = Math.min(
processedItem.buttonContent.length,
processedItem.buttonPath.length
);
processedItem.buttonContent = processedItem.buttonContent.slice(0, minLength);
processedItem.buttonPath = processedItem.buttonPath.slice(0, minLength);
}
return processedItem;
});
return processedData;
},
// 是否应该显示广播组件
shouldShowBroadcast() {
return this.finalBroadcastList.length >= this.minItems && !this.hasError;
},
// 横向滚动用的数据(复制数据实现无缝循环)
broadcastListForHorizontal() {
const list = this.finalBroadcastList;
if (list.length === 0) return [];
let result;
if (list.length === 1) {
// 单条数据时复制多次以实现连续滚动
result = Array(3).fill().map((_, idx) => ({
...list[0],
id: list[0].id + '-copy-' + idx
}));
} else {
// 多条数据时在末尾添加第一条实现无缝循环
result = [...list, list[0]];
}
return result;
},
// 横向滚动样式
horizontalScrollStyle() {
return {
transform: `translateX(-${this.horizontalScrollOffset}px)`,
transition: this.isHorizontalScrolling ? 'none' : 'transform 0.3s ease',
};
},
},
watch: {
// 监听外部数据变化
broadcastData: {
handler(newData) {
if (newData && newData.length > 0) {
this.resetScroll();
}
},
deep: true,
immediate: true
},
// 监听滚动参数变化
scrollSpeed(newSpeed) {
this.horizontalScrollSpeed = newSpeed;
},
scrollInterval(newInterval) {
this.horizontalScrollInterval = newInterval;
if (this.isHorizontalScrolling) {
this.restartScroll();
}
}
},
mounted() {
// 如果没有外部数据且允许自动获取,则获取数据
if (this.autoFetch && this.broadcastData.length === 0) {
this.fetchBroadcastData();
}
// 启动滚动
this.$nextTick(() => {
this.startHorizontalScroll();
});
},
methods: {
/**
* 获取广播数据
*/
async fetchBroadcastData() {
if (this.isLoading) return;
try {
this.isLoading = true;
this.hasError = false;
const data = await getBroadcast({ lang: this.lang || (this.$i18n ? this.$i18n.locale : 'zh') });
if (data && data.code === 200 && Array.isArray(data.data)) {
this.internalBroadcastList = data.data;
this.resetScroll();
this.$emit('data-loaded', data.data);
} else {
this.hasError = true;
this.$emit('error', '获取广播数据失败');
}
} catch (error) {
console.error('获取广播数据失败:', error);
this.hasError = true;
this.$emit('error', error);
} finally {
this.isLoading = false;
}
},
/**
* 开始横向滚动
*/
startHorizontalScroll() {
// 如果正在hover或数据不足不启动滚动
if (this.isHovering || this.finalBroadcastList.length <= 1) return;
if (this.horizontalScrollTimer) {
clearInterval(this.horizontalScrollTimer);
}
this.isHorizontalScrolling = true;
this.horizontalScrollTimer = setInterval(() => {
this.horizontalScrollStep();
}, this.scrollInterval);
},
/**
* 停止横向滚动
*/
stopHorizontalScroll() {
this.wasScrollingBeforeHover = this.isHorizontalScrolling;
this.isHovering = true;
if (this.horizontalScrollTimer) {
clearInterval(this.horizontalScrollTimer);
this.horizontalScrollTimer = null;
}
this.isHorizontalScrolling = false;
},
/**
* 鼠标移开时恢复滚动
*/
resumeHorizontalScroll() {
this.isHovering = false;
if (this.wasScrollingBeforeHover && this.finalBroadcastList.length > 1) {
setTimeout(() => {
if (!this.isHovering) {
this.startHorizontalScroll();
}
}, 100);
}
},
handelJump(url) {
const lang = this.$i18n.locale;
/**
* 处理单个路径跳转
* @param {string} path - 路径
*/
const handleSinglePath = (path) => {
if (!path || typeof path !== 'string') {
console.warn('无效的路径:', path);
return;
}
// 清理路径
const cleanPath = path.trim();
if (!cleanPath) {
console.warn('路径为空');
return;
}
let targetPath;
// 如果是主页路径
if (cleanPath === '/' || cleanPath === '') {
targetPath = `/${lang}/`;
} else {
// 其他路径:去掉开头的斜杠(如果有的话)
const pathWithoutSlash = cleanPath.startsWith('/') ? cleanPath.substring(1) : cleanPath;
targetPath = `/${lang}/${pathWithoutSlash}`;
}
console.log('跳转路径:', targetPath);
try {
// 在当前页面跳转
this.$router.push(targetPath);
} catch (error) {
console.error('路由跳转失败:', error);
// 如果路由跳转失败,尝试直接跳转
window.location.href = targetPath;
}
};
// 处理传入的路径
if (url) {
handleSinglePath(url);
} else {
console.warn('未提供跳转路径');
}
},
/**
* 延迟启动横向滚动移动端触摸后
*/
startHorizontalScrollDelayed() {
if (this.horizontalDelayTimer) {
clearTimeout(this.horizontalDelayTimer);
}
this.horizontalDelayTimer = setTimeout(() => {
this.resumeHorizontalScroll();
}, 1000);
},
/**
* 横向滚动步进
*/
horizontalScrollStep() {
if (!this.$refs.horizontalScrollContent) return;
const contentElement = this.$refs.horizontalScrollContent;
const contentWidth = contentElement.scrollWidth;
// 计算单个循环的宽度
const singleCycleWidth = contentWidth / this.broadcastListForHorizontal.length * this.finalBroadcastList.length;
this.horizontalScrollOffset += this.scrollSpeed;
// 当滚动超过单个循环宽度时,重置到开始位置实现无缝循环
if (this.horizontalScrollOffset >= singleCycleWidth) {
this.horizontalScrollOffset = 0;
}
},
/**
* 重置滚动
*/
resetScroll() {
this.horizontalScrollOffset = 0;
this.$nextTick(() => {
if (this.finalBroadcastList.length > 1) {
this.startHorizontalScroll();
}
});
},
/**
* 重启滚动
*/
restartScroll() {
this.stopHorizontalScroll();
this.$nextTick(() => {
this.startHorizontalScroll();
});
},
/**
* 广播项点击事件
*/
handleItemClick(item) {
if (this.clickable) {
this.$emit('item-click', item);
}
},
/**
* 手动刷新数据
*/
refresh() {
if (this.autoFetch) {
this.fetchBroadcastData();
} else {
this.resetScroll();
}
},
/**
* 暂停/继续滚动
*/
togglePause() {
if (this.isHorizontalScrolling) {
this.stopHorizontalScroll();
} else {
this.resumeHorizontalScroll();
}
},
/**
* 获取组件状态
*/
getStatus() {
return {
isScrolling: this.isHorizontalScrolling,
isHovering: this.isHovering,
currentOffset: this.horizontalScrollOffset,
dataCount: this.finalBroadcastList.length,
isLoading: this.isLoading,
hasError: this.hasError
};
}
},
beforeDestroy() {
// 清理定时器
if (this.horizontalScrollTimer) {
clearInterval(this.horizontalScrollTimer);
}
if (this.horizontalDelayTimer) {
clearTimeout(this.horizontalDelayTimer);
}
// 重置状态
this.isHovering = false;
this.isHorizontalScrolling = false;
}
}
</script>
<style scoped lang="scss">
/* 横向滚动广播容器 */
.horizontal-broadcast-container {
width: 100%;
background: #fff;
// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
padding: 0px 20px !important;
position: relative;
z-index: 100;
background: #E7DFF3;
}
/* 广播标题区域 */
.broadcast-header {
display: flex;
align-items: center;
margin-right: 15px;
flex-shrink: 0;
i {
color: #5721e4;
font-size: 18px;
margin-right: 8px;
}
.broadcast-title {
color: #5721e4;
font-weight: 600;
font-size: 14px;
white-space: nowrap;
}
}
/* 横向滚动包装器 */
.horizontal-scroll-wrapper {
flex: 1;
overflow: hidden;
position: relative;
height: v-bind(height);
// background: #f8f9fa;
border-radius: 8px;
// border: 1px solid #e9ecef;
cursor: pointer;
display: flex;
align-items: center;
&.full-width {
margin-right: 0;
}
}
/* 横向滚动内容 */
.horizontal-scroll-content {
display: flex;
align-items: center;
height: 100%;
white-space: nowrap;
will-change: transform;
}
/* 横向广播项 */
.horizontal-broadcast-item {
display: inline-flex;
align-items: center;
height: 100%;
margin-right: 30px;
flex-shrink: 0;
font-size: 0.85rem;
.horizontal-item-content {
color: #333;
font-size: 14px;
line-height: 1.4;
padding: 0 15px;
white-space: nowrap;
transition: color 0.3s ease;
// &:hover {
// color: #5721e4;
// }
}
/* 按钮组样式 */
.button-group {
display: inline-flex;
align-items: center;
// margin-left: 10px;
gap: 0px; /* 按钮之间的间距 */
}
.horizontal-item-separator {
color: #ccc;
margin: 0 15px;
font-size: 12px;
}
}
/* 滚动暂停时的视觉反馈 */
.horizontal-scroll-wrapper:hover,
.horizontal-scroll-wrapper.is-hovering {
// background: linear-gradient(135deg, #f0f3ff, #e8f2ff);
border-color: #5721e4;
// box-shadow: 0 2px 8px rgba(87, 33, 228, 0.15);
transition: all 0.3s ease;
}
/* hover状态下的内容样式 */
.horizontal-scroll-wrapper.is-hovering .horizontal-item-content {
// color: #5721e4;
font-weight: 500;
transition: all 0.3s ease;
}
/* 暂停指示器圆点 */
.horizontal-scroll-wrapper.is-hovering::before {
content: '';
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
width: 6px;
height: 6px;
background: #5721e4;
border-radius: 50%;
animation: pausePulse 1.5s ease-in-out infinite;
z-index: 1;
}
.view{
color: #5721e4;
// font-size: 0.85rem;
// margin-left: 5px;
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
transition: all 0.3s ease;
white-space: nowrap;
border: 1px solid transparent;
&:hover {
background-color: #5721e4;
color: #fff;
border-color: #5721e4;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
@keyframes pausePulse {
0%, 100% {
opacity: 0.6;
transform: translateY(-50%) scale(1);
}
50% {
opacity: 1;
transform: translateY(-50%) scale(1.2);
}
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.horizontal-broadcast-container {
padding: 8px 15px;
.broadcast-header {
margin-right: 10px;
i {
font-size: 16px;
margin-right: 5px;
}
.broadcast-title {
font-size: 12px;
}
}
}
.horizontal-broadcast-item {
margin-right: 20px;
.horizontal-item-content {
font-size: 13px;
padding: 0 10px;
}
/* 移动端按钮组样式 */
.button-group {
margin-left: 5px;
gap: 5px;
.view {
font-size: 12px;
padding: 1px 6px;
margin-left: 0;
}
}
.horizontal-item-separator {
margin: 0 10px;
}
}
.horizontal-scroll-wrapper.is-hovering::before {
right: 5px;
width: 4px;
height: 4px;
}
}
/* 美化滚动条 */
.horizontal-scroll-wrapper::-webkit-scrollbar {
display: none;
}
</style>