mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-02 08:29:21 +00:00
refactor: 将组件前缀从Art统一替换为Fa
- 更新所有FA前缀的组件文件 - 新增Fa前缀的对应组件 - 更新所有引用路径和组件名 - 修改相关样式和变量名 - 调整部分组件目录结构
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
<!-- 数字滚动 -->
|
||||
<template>
|
||||
<span
|
||||
class="text-g-900 tabular-nums"
|
||||
:class="isRunning ? 'transition-opacity duration-300 ease-in-out' : ''"
|
||||
>
|
||||
{{ formattedValue }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, nextTick, onUnmounted, shallowRef } from "vue";
|
||||
import { useTransition, TransitionPresets } from "@vueuse/core";
|
||||
|
||||
// 类型定义
|
||||
interface CountToProps {
|
||||
/** 目标值 */
|
||||
target: number;
|
||||
/** 动画持续时间(毫秒) */
|
||||
duration?: number;
|
||||
/** 是否自动开始 */
|
||||
autoStart?: boolean;
|
||||
/** 小数位数 */
|
||||
decimals?: number;
|
||||
/** 小数点符号 */
|
||||
decimal?: string;
|
||||
/** 千分位分隔符 */
|
||||
separator?: string;
|
||||
/** 前缀 */
|
||||
prefix?: string;
|
||||
/** 后缀 */
|
||||
suffix?: string;
|
||||
/** 缓动函数 */
|
||||
easing?: keyof typeof TransitionPresets;
|
||||
/** 是否禁用动画 */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface CountToEmits {
|
||||
started: [value: number];
|
||||
finished: [value: number];
|
||||
paused: [value: number];
|
||||
reset: [];
|
||||
}
|
||||
|
||||
interface CountToExpose {
|
||||
start: (target?: number) => void;
|
||||
pause: () => void;
|
||||
reset: (newTarget?: number) => void;
|
||||
stop: () => void;
|
||||
setTarget: (target: number) => void;
|
||||
readonly isRunning: boolean;
|
||||
readonly isPaused: boolean;
|
||||
readonly currentValue: number;
|
||||
readonly targetValue: number;
|
||||
readonly progress: number;
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const EPSILON = Number.EPSILON;
|
||||
const MIN_DURATION = 100;
|
||||
const MAX_DURATION = 60000;
|
||||
const MAX_DECIMALS = 10;
|
||||
const DEFAULT_EASING = "easeOutExpo";
|
||||
const DEFAULT_DURATION = 2000;
|
||||
|
||||
const props = withDefaults(defineProps<CountToProps>(), {
|
||||
target: 0,
|
||||
duration: DEFAULT_DURATION,
|
||||
autoStart: true,
|
||||
decimals: 0,
|
||||
decimal: ".",
|
||||
separator: "",
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
easing: DEFAULT_EASING,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<CountToEmits>();
|
||||
|
||||
// 工具函数
|
||||
const validateNumber = (value: number, name: string, defaultValue: number): number => {
|
||||
if (!Number.isFinite(value)) {
|
||||
console.warn(`[CountTo] Invalid ${name} value:`, value);
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number => {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
};
|
||||
|
||||
const formatNumber = (
|
||||
value: number,
|
||||
decimals: number,
|
||||
decimal: string,
|
||||
separator: string
|
||||
): string => {
|
||||
let result = decimals > 0 ? value.toFixed(decimals) : Math.floor(value).toString();
|
||||
|
||||
// 处理小数点符号
|
||||
if (decimal !== "." && result.includes(".")) {
|
||||
result = result.replace(".", decimal);
|
||||
}
|
||||
|
||||
// 处理千分位分隔符
|
||||
if (separator) {
|
||||
const parts = result.split(decimal);
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
||||
result = parts.join(decimal);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 安全计算值
|
||||
const safeTarget = computed(() => validateNumber(props.target, "target", 0));
|
||||
const safeDuration = computed(() =>
|
||||
clamp(validateNumber(props.duration, "duration", DEFAULT_DURATION), MIN_DURATION, MAX_DURATION)
|
||||
);
|
||||
const safeDecimals = computed(() =>
|
||||
clamp(validateNumber(props.decimals, "decimals", 0), 0, MAX_DECIMALS)
|
||||
);
|
||||
const safeEasing = computed(() => {
|
||||
const easing = props.easing;
|
||||
if (!(easing in TransitionPresets)) {
|
||||
console.warn("[CountTo] Invalid easing value:", easing);
|
||||
return DEFAULT_EASING;
|
||||
}
|
||||
return easing;
|
||||
});
|
||||
|
||||
// 状态管理
|
||||
const currentValue = shallowRef(0);
|
||||
const targetValue = shallowRef(safeTarget.value);
|
||||
const isRunning = shallowRef(false);
|
||||
const isPaused = shallowRef(false);
|
||||
const pausedValue = shallowRef(0);
|
||||
|
||||
// 动画控制
|
||||
const transitionValue = useTransition(currentValue, {
|
||||
duration: safeDuration,
|
||||
transition: computed(() => TransitionPresets[safeEasing.value]),
|
||||
onStarted: () => {
|
||||
isRunning.value = true;
|
||||
isPaused.value = false;
|
||||
emit("started", targetValue.value);
|
||||
},
|
||||
onFinished: () => {
|
||||
isRunning.value = false;
|
||||
isPaused.value = false;
|
||||
emit("finished", targetValue.value);
|
||||
},
|
||||
});
|
||||
|
||||
// 格式化显示值
|
||||
const formattedValue = computed(() => {
|
||||
const value = isPaused.value ? pausedValue.value : transitionValue.value;
|
||||
|
||||
if (!Number.isFinite(value)) {
|
||||
return `${props.prefix}0${props.suffix}`;
|
||||
}
|
||||
|
||||
const formattedNumber = formatNumber(value, safeDecimals.value, props.decimal, props.separator);
|
||||
return `${props.prefix}${formattedNumber}${props.suffix}`;
|
||||
});
|
||||
|
||||
// 私有方法
|
||||
const shouldSkipAnimation = (target: number): boolean => {
|
||||
const current = isPaused.value ? pausedValue.value : transitionValue.value;
|
||||
return Math.abs(current - target) < EPSILON;
|
||||
};
|
||||
|
||||
const resetPauseState = (): void => {
|
||||
isPaused.value = false;
|
||||
pausedValue.value = 0;
|
||||
};
|
||||
|
||||
// 公共方法
|
||||
const start = (target?: number): void => {
|
||||
if (props.disabled) {
|
||||
console.warn("[CountTo] Animation is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
const finalTarget = target !== undefined ? target : targetValue.value;
|
||||
|
||||
if (!Number.isFinite(finalTarget)) {
|
||||
console.warn("[CountTo] Invalid target value for start:", finalTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
targetValue.value = finalTarget;
|
||||
|
||||
if (shouldSkipAnimation(finalTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从暂停值开始(如果存在)
|
||||
if (isPaused.value) {
|
||||
currentValue.value = pausedValue.value;
|
||||
resetPauseState();
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
currentValue.value = finalTarget;
|
||||
});
|
||||
};
|
||||
|
||||
const pause = (): void => {
|
||||
if (!isRunning.value || isPaused.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPaused.value = true;
|
||||
pausedValue.value = transitionValue.value;
|
||||
currentValue.value = pausedValue.value;
|
||||
|
||||
emit("paused", pausedValue.value);
|
||||
};
|
||||
|
||||
const reset = (newTarget = 0): void => {
|
||||
const target = validateNumber(newTarget, "reset target", 0);
|
||||
|
||||
currentValue.value = target;
|
||||
targetValue.value = target;
|
||||
resetPauseState();
|
||||
|
||||
emit("reset");
|
||||
};
|
||||
|
||||
const setTarget = (target: number): void => {
|
||||
if (!Number.isFinite(target)) {
|
||||
console.warn("[CountTo] Invalid target value for setTarget:", target);
|
||||
return;
|
||||
}
|
||||
|
||||
targetValue.value = target;
|
||||
|
||||
if ((isRunning.value || props.autoStart) && !props.disabled) {
|
||||
start(target);
|
||||
}
|
||||
};
|
||||
|
||||
const stop = (): void => {
|
||||
if (isRunning.value || isPaused.value) {
|
||||
currentValue.value = 0;
|
||||
resetPauseState();
|
||||
emit("paused", 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听器
|
||||
watch(
|
||||
safeTarget,
|
||||
(newTarget) => {
|
||||
if (props.autoStart && !props.disabled) {
|
||||
start(newTarget);
|
||||
} else {
|
||||
targetValue.value = newTarget;
|
||||
}
|
||||
},
|
||||
{ immediate: props.autoStart && !props.disabled }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(disabled) => {
|
||||
if (disabled && isRunning.value) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
if (isRunning.value) {
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露 API
|
||||
defineExpose<CountToExpose>({
|
||||
start,
|
||||
pause,
|
||||
reset,
|
||||
stop,
|
||||
setTarget,
|
||||
get isRunning() {
|
||||
return isRunning.value;
|
||||
},
|
||||
get isPaused() {
|
||||
return isPaused.value;
|
||||
},
|
||||
get currentValue() {
|
||||
return isPaused.value ? pausedValue.value : transitionValue.value;
|
||||
},
|
||||
get targetValue() {
|
||||
return targetValue.value;
|
||||
},
|
||||
get progress() {
|
||||
const current = isPaused.value ? pausedValue.value : transitionValue.value;
|
||||
const target = targetValue.value;
|
||||
if (target === 0) return current === 0 ? 1 : 0;
|
||||
return Math.abs(current / target);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- 节日 / 公告顶栏:文案来自 festival 配置(占位符 {{version}}、{{introduceUrl}}) -->
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden transition-[height] duration-300 ease-in-out"
|
||||
:style="{ height: showFestivalStrip ? '48px' : '0' }"
|
||||
>
|
||||
<FaTextScroll
|
||||
v-if="showFestivalStrip"
|
||||
class="!mb-3"
|
||||
type="primary"
|
||||
:text="festivalScrollDisplayHtml"
|
||||
height="40px"
|
||||
:speed="55"
|
||||
:always-scroll="true"
|
||||
show-close
|
||||
@close="closeFestivalScroll"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
||||
import { useCeremony } from "@/hooks/core/useCeremony";
|
||||
import { WEB_LINKS } from "@utils/constants";
|
||||
|
||||
defineOptions({ name: "FaFestivalTextScroll" });
|
||||
|
||||
const settingStore = useSettingsStore();
|
||||
const { showFestivalText } = storeToRefs(settingStore);
|
||||
const { currentFestivalData, closeFestivalScroll } = useCeremony();
|
||||
|
||||
function versionLabel(): string {
|
||||
const v = String(import.meta.env.VITE_VERSION ?? "").trim();
|
||||
if (!v) return "";
|
||||
return v.startsWith("v") ? v : `v${v}`;
|
||||
}
|
||||
|
||||
const festivalScrollDisplayHtml = computed(() => {
|
||||
const raw = currentFestivalData.value?.scrollText ?? "";
|
||||
const ver = versionLabel() || "v0.0.0";
|
||||
return raw.replace(/\{\{version\}\}/g, ver).replace(/\{\{introduceUrl\}\}/g, WEB_LINKS.INTRODUCE);
|
||||
});
|
||||
|
||||
const showFestivalStrip = computed(
|
||||
() =>
|
||||
showFestivalText.value &&
|
||||
!!currentFestivalData.value?.scrollText &&
|
||||
currentFestivalData.value.scrollText !== ""
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,331 @@
|
||||
<!-- 文字滚动 -->
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="relative overflow-hidden rounded-custom-sm border flex-c box-border text-sm"
|
||||
:class="themeClasses"
|
||||
:style="containerStyle"
|
||||
>
|
||||
<div class="flex-cc absolute left-0 h-full w-9 z-10" :style="{ backgroundColor: bgColor }">
|
||||
<FaSvgIcon icon="ri:volume-down-line" class="text-lg" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="contentRef"
|
||||
class="whitespace-nowrap inline-block transition-opacity duration-600 [&_a]:text-danger [&_a:hover]:underline [&_a:hover]:text-danger/80 px-9"
|
||||
:class="[contentClass, { 'opacity-0': !isReady, 'opacity-100': isReady }]"
|
||||
:style="contentStyle"
|
||||
@click="handleContentClick"
|
||||
>
|
||||
<!-- 原始内容 -->
|
||||
<span ref="textRef" class="inline-block">
|
||||
<slot>
|
||||
<span v-html="text"></span>
|
||||
</slot>
|
||||
</span>
|
||||
<!-- 克隆内容用于无缝循环 -->
|
||||
<span v-if="shouldClone" class="inline-block" :style="cloneSpacing">
|
||||
<slot>
|
||||
<span v-html="text"></span>
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showClose"
|
||||
class="flex-cc absolute right-0 h-full w-9 c-p"
|
||||
:style="{ backgroundColor: bgColor }"
|
||||
@click="handleClose"
|
||||
>
|
||||
<FaSvgIcon icon="ri:close-fill" class="text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useElementSize,
|
||||
useRafFn,
|
||||
useElementHover,
|
||||
useDebounceFn,
|
||||
useTimeoutFn,
|
||||
} from "@vueuse/core";
|
||||
import { useSettingsStore } from "@stores/modules/setting.store";
|
||||
|
||||
type ThemeType =
|
||||
| "theme"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "error"
|
||||
| "info"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger";
|
||||
|
||||
/**
|
||||
* 文本滚动组件属性接口
|
||||
*/
|
||||
export interface TextScrollProps {
|
||||
/** 滚动文本内容 */
|
||||
text?: string;
|
||||
/** 主题类型 */
|
||||
type?: ThemeType;
|
||||
/** 滚动方向 */
|
||||
direction?: "left" | "right" | "up" | "down";
|
||||
/** 滚动速度,单位:像素/秒 */
|
||||
speed?: number;
|
||||
/** 容器宽度 */
|
||||
width?: string;
|
||||
/** 容器高度 */
|
||||
height?: string;
|
||||
/** 鼠标悬停时是否暂停滚动 */
|
||||
pauseOnHover?: boolean;
|
||||
/** 是否显示关闭按钮 */
|
||||
showClose?: boolean;
|
||||
/** 始终滚动(即使文字未溢出) */
|
||||
alwaysScroll?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TextScrollProps>(), {
|
||||
text: "",
|
||||
direction: "left",
|
||||
speed: 80,
|
||||
width: "100%",
|
||||
height: "36px",
|
||||
pauseOnHover: true,
|
||||
type: "theme",
|
||||
showClose: false,
|
||||
alwaysScroll: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const handleClose = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const settingStore = useSettingsStore();
|
||||
const { isDark } = storeToRefs(settingStore);
|
||||
|
||||
const containerRef = ref<HTMLElement>();
|
||||
const contentRef = ref<HTMLElement>();
|
||||
const textRef = ref<HTMLElement>();
|
||||
const isReady = ref(false);
|
||||
|
||||
const currentPosition = ref(0);
|
||||
const textSize = ref(0);
|
||||
const containerSize = ref(0);
|
||||
const shouldClone = ref(false);
|
||||
|
||||
const isHorizontal = computed(() => props.direction === "left" || props.direction === "right");
|
||||
const isReverse = computed(() => props.direction === "right" || props.direction === "down");
|
||||
|
||||
// 使用 VueUse 的 useElementSize 监听容器尺寸变化
|
||||
const { width: containerWidth, height: containerHeight } = useElementSize(containerRef);
|
||||
|
||||
// 使用 VueUse 的 useElementHover 检测鼠标悬停
|
||||
const isHovered = useElementHover(containerRef);
|
||||
|
||||
// 计算是否应该暂停动画
|
||||
const isPaused = computed(() => {
|
||||
// 如果未启用 alwaysScroll,且文字未超出容器,则暂停滚动
|
||||
if (!props.alwaysScroll && textSize.value <= containerSize.value) {
|
||||
return true;
|
||||
}
|
||||
return props.pauseOnHover && isHovered.value;
|
||||
});
|
||||
|
||||
// 主题样式映射
|
||||
const themeClasses = computed(() => {
|
||||
const themeMap: Record<ThemeType, string> = {
|
||||
theme: "text-theme/90 !border-theme/50",
|
||||
primary: "text-primary/90 !border-primary/50",
|
||||
secondary: "text-secondary/90 !border-secondary/50",
|
||||
error: "text-error/90 !border-error/50",
|
||||
info: "text-info/90 !border-info/50",
|
||||
success: "text-success/90 !border-success/50",
|
||||
warning: "text-warning/90 !border-warning/50",
|
||||
danger: "text-danger/90 !border-danger/50",
|
||||
};
|
||||
return themeMap[props.type] || themeMap.theme;
|
||||
});
|
||||
|
||||
// 背景色
|
||||
const bgColor = computed(
|
||||
() =>
|
||||
`color-mix(in oklch, var(--color-${props.type}) ${isDark.value ? "25" : "10"}%, var(--fa-color))`
|
||||
);
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
backgroundColor: bgColor.value,
|
||||
}));
|
||||
|
||||
const contentClass = computed(() => {
|
||||
if (!isHorizontal.value) {
|
||||
return "flex flex-col";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
const transform = isHorizontal.value
|
||||
? `translateX(${currentPosition.value}px)`
|
||||
: `translateY(${currentPosition.value}px)`;
|
||||
|
||||
return {
|
||||
transform,
|
||||
willChange: "transform",
|
||||
};
|
||||
});
|
||||
|
||||
// 克隆元素的间距
|
||||
const cloneSpacing = computed(() => {
|
||||
const spacing = "2em";
|
||||
return isHorizontal.value ? { marginLeft: spacing } : { marginTop: spacing };
|
||||
});
|
||||
|
||||
const measureSizes = () => {
|
||||
if (!containerRef.value || !textRef.value) return;
|
||||
|
||||
const text = textRef.value;
|
||||
|
||||
if (isHorizontal.value) {
|
||||
containerSize.value = containerWidth.value;
|
||||
textSize.value = text.offsetWidth;
|
||||
} else {
|
||||
containerSize.value = containerHeight.value;
|
||||
textSize.value = text.offsetHeight;
|
||||
}
|
||||
|
||||
const isOverflow = textSize.value > containerSize.value;
|
||||
shouldClone.value = isOverflow;
|
||||
|
||||
// 居中显示
|
||||
currentPosition.value = (containerSize.value - textSize.value) / 2;
|
||||
|
||||
// 测量完成后才显示内容
|
||||
if (!isReady.value) {
|
||||
isReady.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 使用 VueUse 的 useDebounceFn 防抖测量
|
||||
const debouncedMeasure = useDebounceFn(measureSizes, 150);
|
||||
|
||||
let lastTimestamp = 0;
|
||||
|
||||
// 使用 VueUse 的 useRafFn 替代手动 requestAnimationFrame
|
||||
const { pause, resume } = useRafFn(
|
||||
({ timestamp }) => {
|
||||
if (!lastTimestamp) lastTimestamp = timestamp;
|
||||
|
||||
if (!isPaused.value) {
|
||||
const delta = (timestamp - lastTimestamp) / 1000;
|
||||
const distance = props.speed * delta;
|
||||
const spacing = textSize.value * 0.1;
|
||||
|
||||
currentPosition.value += isReverse.value ? distance : -distance;
|
||||
|
||||
// 循环边界检测
|
||||
if (isReverse.value) {
|
||||
if (currentPosition.value > containerSize.value) {
|
||||
currentPosition.value = -(textSize.value + spacing);
|
||||
}
|
||||
} else {
|
||||
if (currentPosition.value < -(textSize.value + spacing)) {
|
||||
currentPosition.value = containerSize.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
const handleContentClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === "A") {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听容器尺寸变化
|
||||
watch([containerWidth, containerHeight], () => {
|
||||
debouncedMeasure();
|
||||
});
|
||||
|
||||
// 监听属性变化
|
||||
watch(
|
||||
() => [props.direction, props.speed, props.text],
|
||||
() => {
|
||||
measureSizes();
|
||||
lastTimestamp = 0;
|
||||
}
|
||||
);
|
||||
|
||||
// 使用 VueUse 的 useTimeoutFn 替代 setTimeout
|
||||
const { start: startMeasure } = useTimeoutFn(() => {
|
||||
measureSizes();
|
||||
// 测量完成后立即开始动画
|
||||
resume();
|
||||
}, 100);
|
||||
|
||||
onMounted(() => {
|
||||
startMeasure();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
pause();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- <template>
|
||||
<div class="page-content space-y-5">
|
||||
<FaTextScroll
|
||||
text="Art Design Pro 是一款兼具设计美学与高效开发的后台系统 <a target='_blank' href='https://www.artd.pro/docs/'>点击我 </a>访问官方文档"
|
||||
showClose
|
||||
/>
|
||||
|
||||
<FaTextScroll type="success" text="这是一条成功类型的滚动公告" />
|
||||
|
||||
<FaTextScroll type="warning" text="这是一条警告类型的滚动公告" />
|
||||
|
||||
<FaTextScroll type="danger" text="这是一条危险类型的滚动公告" />
|
||||
|
||||
<FaTextScroll type="info" text="这是一条信息类型的滚动公告" />
|
||||
|
||||
<FaTextScroll text="这是一条可关闭的滚动公告" @close="handleClose" />
|
||||
|
||||
<FaTextScroll
|
||||
type="warning"
|
||||
text="这是一条速度较慢、向右滚动的公告"
|
||||
:speed="30"
|
||||
direction="right"
|
||||
/>
|
||||
|
||||
<FaTextScroll
|
||||
text="这是一条文字溢出才会滚动的公告,当文本内容超出容器宽度时才会开始滚动显示,否则保持静止状态"
|
||||
@close="handleClose"
|
||||
:alwaysScroll="false"
|
||||
/>
|
||||
|
||||
<FaTextScroll type="danger" direction="up" :speed="30" text="这是一条向上滚动的公告" />
|
||||
|
||||
<FaTextScroll type="info" direction="down" :speed="30" text="这是一条向下滚动的公告" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "FaTextScroll" });
|
||||
|
||||
|
||||
const handleClose = () => {
|
||||
console.log("文本滚动组件已关闭");
|
||||
ElMessage.info("已关闭");
|
||||
};
|
||||
</script> -->
|
||||
Reference in New Issue
Block a user