Files
dukang/apps/mini-user/src/components/StoreRedeemMarquee.tsx
T

126 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useReady } from '@tarojs/taro';
type StoreRedeemMarqueeProps = {
lines: string[];
};
/** 估算单行像素宽(12px 字号,DOM 测量失败时兜底) */
function estimateTextWidth(text: string): number {
let w = 0;
for (const ch of text) {
w += /[^\x00-\xff]/.test(ch) ? 12 : 7;
}
return Math.max(Math.ceil(w), 80);
}
/**
* 门店核销动态走马灯(H5 + 微信小程序)
*
* - 用 Text 测自然内容宽(weapp 的 View 默认撑满父级,不能用来测宽)
* - 两段显式同宽 + 轨道宽 = 2×段宽,CSS translateX(-50%) 无缝循环
* - 测量失败时用字宽估算,仍能滚动
*/
export default function StoreRedeemMarquee({ lines }: StoreRedeemMarqueeProps) {
const segment = useMemo(() => {
const joined = lines
.map((s) => String(s || '').trim())
.filter(Boolean)
.join('  ');
return joined ? `${joined}  ` : '';
}, [lines]);
const measureId = useMemo(
() => `sm${Math.random().toString(36).slice(2, 10)}`,
[segment],
);
const [segWidth, setSegWidth] = useState(0);
const measure = useCallback(() => {
if (!segment) {
setSegWidth(0);
return;
}
const fallback = estimateTextWidth(segment);
Taro.nextTick(() => {
try {
Taro.createSelectorQuery()
.select(`#${measureId}`)
.boundingClientRect()
.exec((res) => {
const w = Number(res?.[0]?.width || 0);
// weapp 若误测成容器宽,会接近视口;用估算兜底纠正
if (w > 8 && w < fallback * 2.5) {
setSegWidth(Math.ceil(w));
} else {
setSegWidth(fallback);
}
});
} catch {
setSegWidth(fallback);
}
});
}, [segment, measureId]);
useReady(() => {
measure();
});
useEffect(() => {
if (!segment) {
setSegWidth(0);
return;
}
setSegWidth(0);
measure();
const t1 = setTimeout(measure, 100);
const t2 = setTimeout(measure, 350);
const t3 = setTimeout(() => {
setSegWidth((prev) => (prev > 0 ? prev : estimateTextWidth(segment)));
}, 600);
return () => {
clearTimeout(t1);
clearTimeout(t2);
clearTimeout(t3);
};
}, [segment, measure]);
if (!segment) return null;
const running = segWidth > 0;
const durationSec = Math.max(10, Math.min(90, segWidth / 42));
const trackStyle: CSSProperties | undefined = running
? {
width: `${segWidth * 2}px`,
animationDuration: `${durationSec}s`,
// 微信 / Safari
WebkitAnimationDuration: `${durationSec}s`,
}
: undefined;
const segStyle: CSSProperties | undefined = running
? { width: `${segWidth}px` }
: undefined;
return (
<View className="store-detail-marquee">
<View
className={`store-detail-marquee-track${running ? ' is-running' : ''}`}
style={trackStyle}
>
<Text
id={measureId}
className="store-detail-marquee-seg store-detail-marquee-text"
style={segStyle}
>
{segment}
</Text>
<Text className="store-detail-marquee-seg store-detail-marquee-text" style={segStyle}>
{segment}
</Text>
</View>
</View>
);
}