|
385 | 385 | const TickerComponent = { |
386 | 386 | template: ` |
387 | 387 | <div class="ticker" :class="className"> |
388 | | - <div v-for="(col, i) in renderedColumns" :key="i" class="ticker-column" :style="{ width: col.width + 'em' }"> |
| 388 | + <span v-if="prefix" class="ticker-prefix">{{ prefix }}</span> |
| 389 | + <div v-for="(col, i) in renderedColumns" :key="i" class="ticker-column" :style="{ width: col.width + 'em', opacity: col.opacity }"> |
389 | 390 | <div v-for="charObj in col.chars" :key="charObj.key" class="ticker-char" :style="{ transform: 'translateY(' + charObj.offset + 'em)' }"> |
390 | 391 | {{ charObj.char === '${EMPTY_CHAR}' ? '\u00A0' : charObj.char }} |
391 | 392 | </div> |
392 | 393 | </div> |
| 394 | + <span v-if="suffix" class="ticker-suffix">{{ suffix }}</span> |
393 | 395 | </div> |
394 | 396 | `, |
395 | 397 | props: { |
396 | 398 | value: { type: String, required: true }, |
397 | 399 | characterLists: { type: Array, default: () => ['0123456789'] }, |
398 | 400 | duration: { type: Number, default: 500 }, |
399 | 401 | direction: { type: String, default: 'ANY' }, |
400 | | - easing: { type: String, default: 'easeInOut' }, |
| 402 | + easing: { type: [String, Function], default: 'easeInOut' }, |
401 | 403 | className: { type: String, default: '' }, |
402 | 404 | charWidth: { type: Number, default: 1 }, |
| 405 | + disabled: { type: Boolean, default: false }, |
| 406 | + prefix: { type: String, default: '' }, |
| 407 | + suffix: { type: String, default: '' }, |
403 | 408 | }, |
404 | | - setup(props) { |
| 409 | + emits: ['animationEnd'], |
| 410 | + setup(props, { emit }) { |
405 | 411 | const columns = ref([]); |
406 | 412 | const progress = ref(1); |
407 | 413 | let animId; |
|
441 | 447 | } |
442 | 448 |
|
443 | 449 | columns.value = result; |
| 450 | + // disabled 跳过动画 |
| 451 | + if (props.disabled) { |
| 452 | + progress.value = 1; |
| 453 | + const final = result.map(c => applyProgress(c, 1).col).filter(c => c.currentWidth > 0); |
| 454 | + columns.value = final; |
| 455 | + return; |
| 456 | + } |
| 457 | + |
444 | 458 | progress.value = 0; |
445 | 459 | const start = performance.now(); |
446 | 460 | const dur = props.duration; |
447 | | - const easeFn = easingFunctions[props.easing] || easingFunctions.linear; |
| 461 | + // 支持自定义 easing 函数 |
| 462 | + const easeFn = typeof props.easing === 'function' |
| 463 | + ? props.easing |
| 464 | + : easingFunctions[props.easing] || easingFunctions.linear; |
448 | 465 | let lastUpdate = 0; |
449 | 466 |
|
450 | 467 | const animate = (now) => { |
|
460 | 477 | columns.value = final; |
461 | 478 | progress.value = 1; |
462 | 479 | animId = undefined; |
| 480 | + // emit animationEnd |
| 481 | + emit('animationEnd'); |
463 | 482 | } |
464 | 483 | }; |
465 | 484 | animId = requestAnimationFrame(animate); |
466 | | - }, { immediate: true }); |
| 485 | + }, { immediate: false }); // 首次不触发动画 |
| 486 | + |
| 487 | + // 首次渲染:直接显示初始值 |
| 488 | + const initValue = props.value; |
| 489 | + if (initValue) { |
| 490 | + const targetChars = initValue.split(''); |
| 491 | + const result = targetChars.map(char => { |
| 492 | + const col = setTarget(createColumn(), char, lists.value, props.direction); |
| 493 | + return applyProgress(col, 1).col; |
| 494 | + }).filter(c => c.currentWidth > 0); |
| 495 | + columns.value = result; |
| 496 | + progress.value = 1; |
| 497 | + } |
467 | 498 |
|
468 | 499 | onUnmounted(() => { if (animId) cancelAnimationFrame(animId); }); |
469 | 500 |
|
|
473 | 504 | const { charIdx, delta } = applyProgress(col, progress.value); |
474 | 505 | const logicalWidth = col.sourceWidth + (col.targetWidth - col.sourceWidth) * progress.value; |
475 | 506 |
|
476 | | - // Full-width character interpolation logic |
| 507 | + // Full-width character detection (emoji + CJK) |
477 | 508 | const list = col.charList || []; |
478 | | - const isFW = (c) => c && c.length > 0 && c.charCodeAt(0) > 255; |
479 | | - const getW = (c) => isFW(c) ? 1.1 : 0.8; |
| 509 | + const isFW = (c) => { |
| 510 | + if (!c || c.length === 0) return false; |
| 511 | + const code = c.codePointAt(0) || 0; |
| 512 | + return ( |
| 513 | + (code >= 0x3000 && code <= 0x9FFF) || // CJK |
| 514 | + (code >= 0xAC00 && code <= 0xD7AF) || // 韩文 |
| 515 | + (code >= 0xFF00 && code <= 0xFFEF) || // 全角 ASCII |
| 516 | + (code >= 0x1F300 && code <= 0x1FAFF) // Emoji |
| 517 | + ); |
| 518 | + }; |
| 519 | + const getW = (c) => isFW(c) ? 1.25 : 0.8; |
480 | 520 |
|
481 | 521 | const startChar = list[col.startIndex] || ''; |
482 | 522 | const endChar = list[col.endIndex] || ''; |
|
498 | 538 | add(charIdx, 0, 'c'); |
499 | 539 | add(charIdx + 1, -charHeight, 'n'); |
500 | 540 | add(charIdx - 1, charHeight, 'p'); |
501 | | - return { width, chars }; |
| 541 | + |
| 542 | + // Alpha 渐变:新增/删除列 |
| 543 | + const isDeleting = col.targetWidth === 0 && col.sourceWidth > 0; |
| 544 | + const isInserting = col.sourceWidth === 0 && col.targetWidth > 0; |
| 545 | + const opacity = isDeleting ? 1 - progress.value : isInserting ? progress.value : 1; |
| 546 | + |
| 547 | + return { width, chars, opacity }; |
502 | 548 | }).filter(c => c.width > 0); |
503 | 549 | }); |
504 | 550 |
|
|
0 commit comments