724 lines
18 KiB
Vue
724 lines
18 KiB
Vue
<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> |