添加滚动公告小组件及其配置,支持水平和垂直滚动效果
This commit is contained in:
parent
d5cc3eb084
commit
84b31e5523
431
src/widgets/marquee/Config.vue
Normal file
431
src/widgets/marquee/Config.vue
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
<template>
|
||||||
|
<div class="marquee-config">
|
||||||
|
<h2>滚动公告小组件设置</h2>
|
||||||
|
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-divider>预设样式</el-divider>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button-group>
|
||||||
|
<el-button type="primary" @click="applyPreset('classic')">经典滚动</el-button>
|
||||||
|
<el-button type="suc modern: {
|
||||||
|
conten gaming: {
|
||||||
|
conten vertical: {
|
||||||
|
content: '正在直播中\n精彩内容不要错过\n欢迎弹幕互动\n点个关注吧',
|
||||||
|
direction: 'vertical' as const,'击败BOSS! 连击! AMAZING! LEGENDARY!',
|
||||||
|
direction: 'horizontal' as const,'正在直播游戏 | LIVE | 欢迎互动聊天!',
|
||||||
|
direction: 'horizontal' as const,s" @click="applyPreset('modern')">现代风格</el-button>
|
||||||
|
<el-button type="warning" @click="applyPreset('gaming')">游戏主题</el-button>
|
||||||
|
<el-button type="danger" @click="applyPreset('vertical')">垂直轮播</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>基本设置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="公告内容">
|
||||||
|
<el-input
|
||||||
|
v-model="localConfig.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入公告内容,支持多行(垂直滚动模式下每行单独显示)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="滚动方向">
|
||||||
|
<el-select v-model="localConfig.direction" placeholder="选择滚动方向">
|
||||||
|
<el-option label="水平滚动" value="horizontal" />
|
||||||
|
<el-option label="垂直滚动" value="vertical" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 水平滚动配置 -->
|
||||||
|
<template v-if="localConfig.direction === 'horizontal'">
|
||||||
|
<el-form-item label="滚动方向">
|
||||||
|
<el-select v-model="localConfig.horizontalDirection">
|
||||||
|
<el-option label="向左滚动" value="left" />
|
||||||
|
<el-option label="向右滚动" value="right" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="滚动速度">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.speed"
|
||||||
|
:min="10"
|
||||||
|
:max="200"
|
||||||
|
:step="5"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val} px/s`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 垂直滚动配置 -->
|
||||||
|
<template v-if="localConfig.direction === 'vertical'">
|
||||||
|
<el-form-item label="行间隔时间">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.verticalInterval"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
:step="0.5"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val} 秒`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="行高倍数(调节行间距)">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.lineHeight"
|
||||||
|
:min="1"
|
||||||
|
:max="3"
|
||||||
|
:step="0.1"
|
||||||
|
show-input
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="每次显示行数">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.displayLines"
|
||||||
|
:min="1"
|
||||||
|
:max="5"
|
||||||
|
:step="1"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val} 行`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-divider>文本样式</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="使用渐变色">
|
||||||
|
<el-switch v-model="localConfig.useGradient" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!localConfig.useGradient" label="文本颜色">
|
||||||
|
<el-color-picker v-model="localConfig.color" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="localConfig.useGradient">
|
||||||
|
<el-form-item label="渐变颜色1">
|
||||||
|
<el-color-picker v-model="localConfig.gradientColors[0]" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="渐变颜色2">
|
||||||
|
<el-color-picker v-model="localConfig.gradientColors[1]" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item label="字体大小">
|
||||||
|
<el-slider v-model="localConfig.fontSize" :min="12" :max="72" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="字体">
|
||||||
|
<el-select v-model="localConfig.fontFamily">
|
||||||
|
<el-option label="Arial" value="Arial" />
|
||||||
|
<el-option label="微软雅黑" value="Microsoft YaHei" />
|
||||||
|
<el-option label="黑体" value="SimHei" />
|
||||||
|
<el-option label="宋体" value="SimSun" />
|
||||||
|
<el-option label="Times New Roman" value="Times New Roman" />
|
||||||
|
<el-option label="Courier New" value="Courier New" />
|
||||||
|
<el-option label="Impact" value="Impact" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="字重">
|
||||||
|
<el-select v-model="localConfig.fontWeight">
|
||||||
|
<el-option label="普通" value="normal" />
|
||||||
|
<el-option label="粗体" value="bold" />
|
||||||
|
<el-option label="细体" value="100" />
|
||||||
|
<el-option label="轻体" value="300" />
|
||||||
|
<el-option label="中等" value="500" />
|
||||||
|
<el-option label="加粗" value="700" />
|
||||||
|
<el-option label="特粗" value="900" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="文本阴影">
|
||||||
|
<el-switch v-model="localConfig.textShadow" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="localConfig.textShadow">
|
||||||
|
<el-form-item label="阴影颜色">
|
||||||
|
<el-color-picker v-model="shadowColor" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="阴影模糊度">
|
||||||
|
<el-slider v-model="localConfig.shadowBlur" :min="0" :max="20" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-divider>容器样式</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="宽度">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.width"
|
||||||
|
:min="100"
|
||||||
|
:max="800"
|
||||||
|
:step="10"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val}px`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="高度">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.height"
|
||||||
|
:min="30"
|
||||||
|
:max="200"
|
||||||
|
:step="5"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val}px`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="背景颜色">
|
||||||
|
<el-color-picker v-model="backgroundColor" show-alpha />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="圆角">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.borderRadius"
|
||||||
|
:min="0"
|
||||||
|
:max="30"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val}px`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内边距">
|
||||||
|
<el-slider
|
||||||
|
v-model="localConfig.padding"
|
||||||
|
:min="0"
|
||||||
|
:max="50"
|
||||||
|
:step="2"
|
||||||
|
show-input
|
||||||
|
:format-tooltip="(val: number) => `${val}px`"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>其他设置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<span>循环播放</span>
|
||||||
|
<el-switch v-model="localConfig.loop" style="margin-left: 10px;" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>鼠标悬停暂停</span>
|
||||||
|
<el-switch v-model="localConfig.pauseOnHover" style="margin-left: 10px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed, onMounted } from 'vue';
|
||||||
|
import type { MarqueeConfig } from './types';
|
||||||
|
|
||||||
|
// Define props with default values
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
config: Partial<MarqueeConfig>;
|
||||||
|
}>(), {
|
||||||
|
config: () => ({
|
||||||
|
content: '这是一条滚动公告\n支持多行内容\n可以自定义样式和滚动效果',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.5)',
|
||||||
|
shadowBlur: 2,
|
||||||
|
useGradient: false,
|
||||||
|
gradientColors: ['#ff6b6b', '#4ecdc4'],
|
||||||
|
direction: 'horizontal',
|
||||||
|
horizontalDirection: 'left',
|
||||||
|
speed: 50,
|
||||||
|
verticalInterval: 3,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
displayLines: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
width: 400,
|
||||||
|
height: 60,
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define emit
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:config', config: MarqueeConfig): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Local config for two-way binding
|
||||||
|
const localConfig = ref<MarqueeConfig>({
|
||||||
|
content: '这是一条滚动公告\n支持多行内容\n可以自定义样式和滚动效果',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.5)',
|
||||||
|
shadowBlur: 2,
|
||||||
|
useGradient: false,
|
||||||
|
gradientColors: ['#ff6b6b', '#4ecdc4'],
|
||||||
|
direction: 'horizontal',
|
||||||
|
horizontalDirection: 'left',
|
||||||
|
speed: 50,
|
||||||
|
verticalInterval: 3,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
displayLines: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
width: 400,
|
||||||
|
height: 60,
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync with parent config on mount
|
||||||
|
onMounted(() => {
|
||||||
|
// Merge default config with provided config
|
||||||
|
localConfig.value = { ...localConfig.value, ...props.config };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 背景颜色处理(支持透明度)
|
||||||
|
const backgroundColor = computed({
|
||||||
|
get: () => {
|
||||||
|
return localConfig.value.backgroundColor;
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
localConfig.value.backgroundColor = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 阴影颜色处理
|
||||||
|
const shadowColor = computed({
|
||||||
|
get: () => {
|
||||||
|
return localConfig.value.shadowColor;
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
localConfig.value.shadowColor = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 预设样式
|
||||||
|
const presets = {
|
||||||
|
classic: {
|
||||||
|
content: '欢迎关注我的直播间 ★ 记得点赞订阅哦 ♥ 感谢大家的支持!',
|
||||||
|
direction: 'horizontal' as const,
|
||||||
|
horizontalDirection: 'left' as const,
|
||||||
|
speed: 80,
|
||||||
|
verticalInterval: 3,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
displayLines: 1,
|
||||||
|
fontSize: 24,
|
||||||
|
fontFamily: 'Microsoft YaHei',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: '#ffffff',
|
||||||
|
useGradient: false,
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.7)',
|
||||||
|
shadowBlur: 3,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
borderRadius: 25,
|
||||||
|
padding: 12,
|
||||||
|
width: 600,
|
||||||
|
height: 50,
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: true
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
content: '🎮 正在直播游戏 | ● LIVE | 欢迎互动聊天!',
|
||||||
|
direction: 'horizontal' as const,
|
||||||
|
horizontalDirection: 'left' as const,
|
||||||
|
speed: 60,
|
||||||
|
verticalInterval: 3,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
displayLines: 1,
|
||||||
|
fontSize: 26,
|
||||||
|
fontFamily: 'Arial',
|
||||||
|
fontWeight: '500',
|
||||||
|
useGradient: true,
|
||||||
|
gradientColors: ['#667eea', '#764ba2'],
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.5)',
|
||||||
|
shadowBlur: 2,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
width: 500,
|
||||||
|
height: 60,
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: true
|
||||||
|
},
|
||||||
|
gaming: {
|
||||||
|
content: '🎮 正在直播游戏 | ● LIVE | 欢迎互动聊天!',
|
||||||
|
direction: 'horizontal' as const,
|
||||||
|
horizontalDirection: 'right' as const,
|
||||||
|
speed: 100,
|
||||||
|
verticalInterval: 3,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
displayLines: 1,
|
||||||
|
fontSize: 32,
|
||||||
|
fontFamily: 'Impact',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
useGradient: true,
|
||||||
|
gradientColors: ['#ff6b35', '#f7931e'],
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.8)',
|
||||||
|
shadowBlur: 6,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 20,
|
||||||
|
width: 550,
|
||||||
|
height: 70,
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: false
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
content: '正在直播中\n🎬 精彩内容不要错过\n 欢迎弹幕互动\n⭐ 点个关注吧',
|
||||||
|
direction: 'vertical' as const,
|
||||||
|
verticalInterval: 2.5,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
displayLines: 2,
|
||||||
|
fontSize: 22,
|
||||||
|
fontFamily: 'Microsoft YaHei',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: '#ffffff',
|
||||||
|
useGradient: false,
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.6)',
|
||||||
|
shadowBlur: 2,
|
||||||
|
backgroundColor: 'rgba(30,30,30,0.85)',
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 18,
|
||||||
|
width: 280,
|
||||||
|
height: 90,
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用预设
|
||||||
|
const applyPreset = (presetName: keyof typeof presets) => {
|
||||||
|
const preset = presets[presetName];
|
||||||
|
localConfig.value = { ...localConfig.value, ...preset };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for local changes and emit to parent
|
||||||
|
watch(localConfig, (newConfig) => {
|
||||||
|
emit('update:config', { ...newConfig });
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.marquee-config {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
352
src/widgets/marquee/Widget.vue
Normal file
352
src/widgets/marquee/Widget.vue
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="marquee-container"
|
||||||
|
:style="containerStyle"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="config.direction === 'horizontal'"
|
||||||
|
class="marquee-horizontal"
|
||||||
|
:style="horizontalStyle"
|
||||||
|
ref="horizontalRef"
|
||||||
|
>
|
||||||
|
<span class="marquee-text" :style="textStyle">{{ config.content }}</span>
|
||||||
|
<span class="marquee-text marquee-duplicate" :style="textStyle">{{ config.content }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="marquee-vertical"
|
||||||
|
:style="verticalStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="marquee-vertical-content"
|
||||||
|
:style="verticalContentStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in contentLines"
|
||||||
|
:key="index"
|
||||||
|
class="marquee-line"
|
||||||
|
:style="lineStyle"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import type { MarqueeConfig } from './types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
config: MarqueeConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const horizontalRef = ref<HTMLElement>();
|
||||||
|
const currentLineIndex = ref(0);
|
||||||
|
const horizontalOffset = ref(0);
|
||||||
|
const isPaused = ref(false);
|
||||||
|
let animationId: number | null = null;
|
||||||
|
let verticalTimer: number | null = null;
|
||||||
|
let lastTime = 0;
|
||||||
|
|
||||||
|
// 计算样式
|
||||||
|
const containerStyle = computed(() => ({
|
||||||
|
width: `${props.config.width}px`,
|
||||||
|
height: `${props.config.height}px`,
|
||||||
|
backgroundColor: props.config.backgroundColor,
|
||||||
|
borderRadius: `${props.config.borderRadius}px`,
|
||||||
|
padding: `${props.config.padding}px`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative' as const,
|
||||||
|
boxSizing: 'border-box' as const
|
||||||
|
}));
|
||||||
|
|
||||||
|
const textStyle = computed(() => {
|
||||||
|
const style: any = {
|
||||||
|
fontSize: `${props.config.fontSize}px`,
|
||||||
|
fontWeight: props.config.fontWeight,
|
||||||
|
fontFamily: props.config.fontFamily,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
lineHeight: '1.2', // 固定行高,避免文本被遮挡
|
||||||
|
margin: 0,
|
||||||
|
padding: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.config.useGradient && props.config.gradientColors.length >= 2) {
|
||||||
|
style.background = `linear-gradient(45deg, ${props.config.gradientColors.join(', ')})`;
|
||||||
|
style.WebkitBackgroundClip = 'text';
|
||||||
|
style.WebkitTextFillColor = 'transparent';
|
||||||
|
style.backgroundClip = 'text';
|
||||||
|
} else {
|
||||||
|
style.color = props.config.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.config.textShadow) {
|
||||||
|
style.textShadow = `0 0 ${props.config.shadowBlur}px ${props.config.shadowColor}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
const horizontalStyle = computed(() => ({
|
||||||
|
transform: `translateX(${horizontalOffset.value}px)`,
|
||||||
|
transition: isPaused.value ? 'none' : 'transform 0.1s linear',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const verticalStyle = computed(() => {
|
||||||
|
// 根据显示行数调整容器高度
|
||||||
|
const lineHeight = props.config.fontSize * props.config.lineHeight;
|
||||||
|
const containerHeight = lineHeight * props.config.displayLines;
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: `${containerHeight}px`,
|
||||||
|
display: 'flex' as const,
|
||||||
|
flexDirection: 'column' as const,
|
||||||
|
justifyContent: 'flex-start' as const,
|
||||||
|
overflow: 'hidden'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const verticalContentStyle = computed(() => {
|
||||||
|
const lineHeight = props.config.fontSize * props.config.lineHeight;
|
||||||
|
const offset = currentLineIndex.value * lineHeight;
|
||||||
|
return {
|
||||||
|
transform: `translateY(${-offset}px)`,
|
||||||
|
transition: 'transform 0.5s ease-in-out'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineStyle = computed(() => {
|
||||||
|
const lineHeight = props.config.fontSize * props.config.lineHeight;
|
||||||
|
const style: any = {
|
||||||
|
fontSize: `${props.config.fontSize}px`,
|
||||||
|
fontWeight: props.config.fontWeight,
|
||||||
|
fontFamily: props.config.fontFamily,
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: `${lineHeight}px`,
|
||||||
|
lineHeight: props.config.lineHeight,
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.config.useGradient && props.config.gradientColors.length >= 2) {
|
||||||
|
style.background = `linear-gradient(45deg, ${props.config.gradientColors.join(', ')})`;
|
||||||
|
style.WebkitBackgroundClip = 'text';
|
||||||
|
style.WebkitTextFillColor = 'transparent';
|
||||||
|
style.backgroundClip = 'text';
|
||||||
|
} else {
|
||||||
|
style.color = props.config.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.config.textShadow) {
|
||||||
|
style.textShadow = `0 0 ${props.config.shadowBlur}px ${props.config.shadowColor}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分割内容为行
|
||||||
|
const contentLines = computed(() => {
|
||||||
|
return props.config.content.split('\n').filter(line => line.trim() !== '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 水平滚动动画
|
||||||
|
const startHorizontalAnimation = () => {
|
||||||
|
if (!horizontalRef.value || props.config.direction !== 'horizontal') return;
|
||||||
|
|
||||||
|
const container = horizontalRef.value.parentElement;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 等待下一帧确保DOM已渲染
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const textElement = horizontalRef.value?.querySelector('.marquee-text') as HTMLElement;
|
||||||
|
if (!textElement) return;
|
||||||
|
|
||||||
|
const containerWidth = container.offsetWidth;
|
||||||
|
const textWidth = textElement.offsetWidth;
|
||||||
|
const spacing = 50; // 两个文本之间的间距
|
||||||
|
const totalWidth = textWidth + spacing;
|
||||||
|
|
||||||
|
const animate = (currentTime: number) => {
|
||||||
|
if (lastTime === 0) lastTime = currentTime;
|
||||||
|
const deltaTime = currentTime - lastTime;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
if (!isPaused.value) {
|
||||||
|
const direction = props.config.horizontalDirection === 'left' ? -1 : 1;
|
||||||
|
const pixelsPerSecond = props.config.speed;
|
||||||
|
const deltaPixels = (pixelsPerSecond * deltaTime) / 1000;
|
||||||
|
|
||||||
|
horizontalOffset.value += direction * deltaPixels;
|
||||||
|
|
||||||
|
// 无缝循环重置位置
|
||||||
|
if (props.config.horizontalDirection === 'left') {
|
||||||
|
if (horizontalOffset.value <= -totalWidth) {
|
||||||
|
horizontalOffset.value = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (horizontalOffset.value >= totalWidth) {
|
||||||
|
horizontalOffset.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.config.loop) {
|
||||||
|
animationId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置初始位置
|
||||||
|
if (props.config.horizontalDirection === 'left') {
|
||||||
|
horizontalOffset.value = containerWidth;
|
||||||
|
} else {
|
||||||
|
horizontalOffset.value = -totalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = requestAnimationFrame(animate);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 垂直滚动动画
|
||||||
|
const startVerticalAnimation = () => {
|
||||||
|
if (props.config.direction !== 'vertical' || contentLines.value.length <= props.config.displayLines) return;
|
||||||
|
|
||||||
|
const showNextLines = () => {
|
||||||
|
const totalLines = contentLines.value.length;
|
||||||
|
const displayLines = props.config.displayLines;
|
||||||
|
|
||||||
|
// 按照显示行数来滚动
|
||||||
|
let nextIndex = currentLineIndex.value + displayLines;
|
||||||
|
|
||||||
|
// 如果超出范围,从头开始
|
||||||
|
if (nextIndex >= totalLines) {
|
||||||
|
nextIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLineIndex.value = nextIndex;
|
||||||
|
|
||||||
|
if (props.config.loop || nextIndex !== 0) {
|
||||||
|
verticalTimer = window.setTimeout(showNextLines, props.config.verticalInterval * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
verticalTimer = window.setTimeout(showNextLines, props.config.verticalInterval * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标事件处理
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (props.config.pauseOnHover) {
|
||||||
|
isPaused.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (props.config.pauseOnHover) {
|
||||||
|
isPaused.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止动画
|
||||||
|
const stopAnimations = () => {
|
||||||
|
if (animationId) {
|
||||||
|
cancelAnimationFrame(animationId);
|
||||||
|
animationId = null;
|
||||||
|
}
|
||||||
|
if (verticalTimer) {
|
||||||
|
clearTimeout(verticalTimer);
|
||||||
|
verticalTimer = null;
|
||||||
|
}
|
||||||
|
lastTime = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动动画
|
||||||
|
const startAnimation = () => {
|
||||||
|
stopAnimations();
|
||||||
|
currentLineIndex.value = 0;
|
||||||
|
|
||||||
|
if (props.config.direction === 'horizontal') {
|
||||||
|
startHorizontalAnimation();
|
||||||
|
} else {
|
||||||
|
startVerticalAnimation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
watch(() => props.config, () => {
|
||||||
|
startAnimation();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAnimations();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.marquee-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-horizontal {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-vertical {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-vertical-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-line {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-line.active {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-text {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-duplicate {
|
||||||
|
margin-left: 50px; /* 两个文本之间的间距 */
|
||||||
|
}
|
||||||
|
</style>
|
14
src/widgets/marquee/index.ts
Normal file
14
src/widgets/marquee/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Widget from './Widget.vue';
|
||||||
|
import Config from './Config.vue';
|
||||||
|
import { defaultConfig } from './types';
|
||||||
|
import { createWidget } from '../createWidget';
|
||||||
|
|
||||||
|
export default createWidget({
|
||||||
|
label: '滚动公告',
|
||||||
|
value: 'marquee',
|
||||||
|
icon: '📢',
|
||||||
|
description: '可定制内容、样式和滚动效果的公告滚动条,支持水平和垂直滚动',
|
||||||
|
component: Widget,
|
||||||
|
configComponent: Config,
|
||||||
|
defaultConfig
|
||||||
|
});
|
67
src/widgets/marquee/types.ts
Normal file
67
src/widgets/marquee/types.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 滚动公告小组件配置接口
|
||||||
|
export interface MarqueeConfig {
|
||||||
|
// 公告内容
|
||||||
|
content: string;
|
||||||
|
// 文本样式
|
||||||
|
color: string;
|
||||||
|
fontSize: number;
|
||||||
|
fontWeight: string;
|
||||||
|
fontFamily: string;
|
||||||
|
textShadow: boolean;
|
||||||
|
shadowColor: string;
|
||||||
|
shadowBlur: number;
|
||||||
|
useGradient: boolean;
|
||||||
|
gradientColors: string[];
|
||||||
|
|
||||||
|
// 滚动配置
|
||||||
|
direction: 'horizontal' | 'vertical';
|
||||||
|
horizontalDirection: 'left' | 'right';
|
||||||
|
speed: number; // 水平滚动速度 (px/s)
|
||||||
|
|
||||||
|
// 垂直滚动特有配置
|
||||||
|
verticalInterval: number; // 垂直滚动时行间隔时间 (秒)
|
||||||
|
lineHeight: number; // 行高倍数
|
||||||
|
displayLines: number; // 每次显示的行数
|
||||||
|
|
||||||
|
// 容器样式
|
||||||
|
backgroundColor: string;
|
||||||
|
borderRadius: number;
|
||||||
|
padding: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
// 其他配置
|
||||||
|
loop: boolean; // 是否循环播放
|
||||||
|
pauseOnHover: boolean; // 鼠标悬停时是否暂停
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动公告小组件默认配置
|
||||||
|
export const defaultConfig: MarqueeConfig = {
|
||||||
|
content: '欢迎关注我的直播间 ★ 记得点赞订阅哦 ♥ 感谢大家的支持!',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
fontFamily: 'Microsoft YaHei',
|
||||||
|
textShadow: true,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.7)',
|
||||||
|
shadowBlur: 3,
|
||||||
|
useGradient: false,
|
||||||
|
gradientColors: ['#ff6b6b', '#4ecdc4'],
|
||||||
|
|
||||||
|
direction: 'horizontal',
|
||||||
|
horizontalDirection: 'left',
|
||||||
|
speed: 80,
|
||||||
|
|
||||||
|
verticalInterval: 3,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
displayLines: 1,
|
||||||
|
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
borderRadius: 25,
|
||||||
|
padding: 12,
|
||||||
|
width: 600,
|
||||||
|
height: 50,
|
||||||
|
|
||||||
|
loop: true,
|
||||||
|
pauseOnHover: true
|
||||||
|
};
|
@ -4,6 +4,7 @@ import DateWidget from './date';
|
|||||||
import TextWidget from './text';
|
import TextWidget from './text';
|
||||||
import ImageWidget from './image';
|
import ImageWidget from './image';
|
||||||
import TimerWidget from './timer';
|
import TimerWidget from './timer';
|
||||||
|
import MarqueeWidget from './marquee';
|
||||||
|
|
||||||
|
|
||||||
// 导入类型定义
|
// 导入类型定义
|
||||||
@ -16,6 +17,7 @@ export const widgets: WidgetRegistration[] = [
|
|||||||
TextWidget,
|
TextWidget,
|
||||||
ImageWidget,
|
ImageWidget,
|
||||||
TimerWidget,
|
TimerWidget,
|
||||||
|
MarqueeWidget,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 获取小组件显示信息列表(用于 UI 展示)
|
// 获取小组件显示信息列表(用于 UI 展示)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user