feat(stage-ui): add hologram promo banner (#1514)
--------- Co-authored-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
co-authored by
Neko Ayaka
parent
b85808c0af
commit
f9ebe6522b
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Save as New Profile
|
||||
new-profile-name: Profile name
|
||||
manage: Manage Profiles
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: Moon Gift
|
||||
title: 7-Day Sign-In
|
||||
eventName: Sign in to claim stardust, avatar frames, and launch gifts.
|
||||
date: 03/28 - 04/10
|
||||
reward: Up to 120 Stardust
|
||||
cta: Sign In
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: Build Bonus
|
||||
title: Supply Box Bonus
|
||||
eventName: Finish construction tasks to earn bonus vouchers and double materials.
|
||||
date: 03/30 - 04/12
|
||||
reward: 5 Supply Vouchers + Upgrade Materials
|
||||
cta: View Tasks
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: Spring Draw
|
||||
title: Capsule Carnival
|
||||
eventName: Join the spring draw to unlock bubbles, charms, and themed furniture.
|
||||
date: 04/01 - 04/18
|
||||
reward: Half-Price First Pull + Rare Charm
|
||||
cta: Draw Now
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: Coupon
|
||||
title: Top-Up Coupon
|
||||
eventName: Finish chat interactions to claim coupons for expansion and voice packs.
|
||||
date: 04/02 - 04/20
|
||||
reward: Save 6 on 30 / Save 15 on 68
|
||||
cta: Claim Coupon
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: Room Theme
|
||||
title: Spring Theme Update
|
||||
eventName: Unlock window, desk, and night-lamp themes to refresh your room.
|
||||
date: 04/05 - 04/22
|
||||
reward: Free Theme Draw Ticket on Login
|
||||
cta: View Theme
|
||||
waiting: Waiting
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Guardar como Nuevo Perfil
|
||||
new-profile-name: Nombre del perfil
|
||||
manage: Administrar Perfiles
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: Regalo lunar
|
||||
title: Recompensa de 7 días
|
||||
eventName: Inicia sesión para conseguir polvo estelar, marcos de avatar y regalos especiales.
|
||||
date: 03/28 - 04/10
|
||||
reward: Hasta 120 de polvo estelar
|
||||
cta: Iniciar sesión
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: Bono de obra
|
||||
title: Caja de suministro extra
|
||||
eventName: Completa tareas de construcción para obtener cupones y materiales al doble.
|
||||
date: 03/30 - 04/12
|
||||
reward: 5 cupones + materiales de mejora
|
||||
cta: Ver tareas
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: Sorteo primaveral
|
||||
title: Carnaval cápsula
|
||||
eventName: Participa en el sorteo de primavera y desbloquea burbujas, dijes y muebles temáticos.
|
||||
date: 04/01 - 04/18
|
||||
reward: Primer intento a mitad + dije raro
|
||||
cta: Ir al sorteo
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: Cupón
|
||||
title: Cupón de recarga
|
||||
eventName: Completa interacciones para recibir cupones para paquetes de diálogo y voces.
|
||||
date: 04/02 - 04/20
|
||||
reward: Ahorra 6 en 30 / 15 en 68
|
||||
cta: Recibir cupón
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: Tema del cuarto
|
||||
title: Decoración de primavera
|
||||
eventName: Desbloquea temas de ventana, escritorio y lámpara para renovar tu espacio.
|
||||
date: 04/05 - 04/22
|
||||
reward: Ticket de tema gratis al iniciar sesión
|
||||
cta: Ver tema
|
||||
waiting: Esperando...
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Enregistrer comme Nouveau Profil
|
||||
new-profile-name: Nom du profil
|
||||
manage: Gérer les Profils
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: Cadeau lunaire
|
||||
title: Cadeau de connexion 7 jours
|
||||
eventName: Connectez-vous pour obtenir poussière d’étoile, cadre d’avatar et cadeaux spéciaux.
|
||||
date: 03/28 - 04/10
|
||||
reward: Jusqu’à 120 poussières d’étoile
|
||||
cta: Se connecter
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: Bonus chantier
|
||||
title: Caisse bonus
|
||||
eventName: Terminez les missions de construction pour obtenir des coupons et des matériaux doublés.
|
||||
date: 03/30 - 04/12
|
||||
reward: 5 coupons + matériaux d’amélioration
|
||||
cta: Voir les missions
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: Tirage printanier
|
||||
title: Carnaval capsule
|
||||
eventName: Participez au tirage du printemps pour débloquer bulles, charms et meubles thématiques.
|
||||
date: 04/01 - 04/18
|
||||
reward: Premier tirage à moitié prix + charm rare
|
||||
cta: Aller au tirage
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: Coupon
|
||||
title: Coupon de recharge
|
||||
eventName: Terminez des interactions pour obtenir des coupons pour packs de dialogue et de voix.
|
||||
date: 04/02 - 04/20
|
||||
reward: -6 sur 30 / -15 sur 68
|
||||
cta: Récupérer le coupon
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: Theme de chambre
|
||||
title: Décor de printemps
|
||||
eventName: Débloquez des thèmes de fenêtre, bureau et veilleuse pour renouveler votre espace.
|
||||
date: 04/05 - 04/22
|
||||
reward: Ticket de thème offert à la connexion
|
||||
cta: Voir le thème
|
||||
waiting: En attente
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: 新規プロファイルとして保存
|
||||
new-profile-name: Profile name
|
||||
manage: プロファイルを管理
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: 月の贈り物
|
||||
title: 7日間ログイン特典
|
||||
eventName: ログインで星砂、アイコンフレーム、登場記念ギフトを獲得できます。
|
||||
date: 03/28 - 04/10
|
||||
reward: 最大 120 星砂
|
||||
cta: 今すぐ受け取る
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: 建造ボーナス
|
||||
title: 補給箱ボーナス
|
||||
eventName: 建造任務を完了すると補給券と2倍素材報酬を獲得できます。
|
||||
date: 03/30 - 04/12
|
||||
reward: 補給券 x5 + 強化素材
|
||||
cta: 任務を見る
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: 春の抽選
|
||||
title: カプセルカーニバル
|
||||
eventName: 春のガチャに参加して吹き出し、チャーム、テーマ家具を解放しましょう。
|
||||
date: 04/01 - 04/18
|
||||
reward: 初回半額 + レアチャーム
|
||||
cta: 引きに行く
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: クーポン
|
||||
title: チャージクーポン
|
||||
eventName: 会話ミッションを完了すると拡張パックや音声パック用クーポンを受け取れます。
|
||||
date: 04/02 - 04/20
|
||||
reward: 30で6割引 / 68で15割引
|
||||
cta: クーポンを受け取る
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: ルームテーマ
|
||||
title: 春のルーム更新
|
||||
eventName: 窓辺、デスク、ナイトライトのテーマを開放して部屋を模様替えできます。
|
||||
date: 04/05 - 04/22
|
||||
reward: ログインでテーマ抽選券
|
||||
cta: テーマを見る
|
||||
waiting: 待機中
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Save as New Profile
|
||||
new-profile-name: Profile name
|
||||
manage: Manage Profiles
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: 달빛 선물
|
||||
title: 7일 출석 보상
|
||||
eventName: 출석만 해도 스타더스트, 아바타 프레임, 기념 보상을 받을 수 있어요.
|
||||
date: 03/28 - 04/10
|
||||
reward: 최대 120 스타더스트
|
||||
cta: 지금 출석하기
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: 건설 보너스
|
||||
title: 보급 상자 추가 지급
|
||||
eventName: 건설 임무를 완료하면 보급 쿠폰과 2배 재료 보상을 획득합니다.
|
||||
date: 03/30 - 04/12
|
||||
reward: 보급 쿠폰 x5 + 강화 재료
|
||||
cta: 임무 보기
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: 봄 추첨
|
||||
title: 캡슐 카니발
|
||||
eventName: 봄 뽑기에 참여해 말풍선, 장식, 테마 가구를 잠금 해제하세요.
|
||||
date: 04/01 - 04/18
|
||||
reward: 첫 뽑기 반값 + 희귀 장식
|
||||
cta: 뽑기 하러 가기
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: 쿠폰
|
||||
title: 충전 쿠폰
|
||||
eventName: 대화 미션을 완료하면 확장팩과 음성팩에 사용할 쿠폰을 받을 수 있어요.
|
||||
date: 04/02 - 04/20
|
||||
reward: 30 이상 6 할인 / 68 이상 15 할인
|
||||
cta: 쿠폰 받기
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: 룸 테마
|
||||
title: 봄 배경 업데이트
|
||||
eventName: 창가, 책상, 무드등 테마를 열어 홈 화면을 새롭게 꾸며보세요.
|
||||
date: 04/05 - 04/22
|
||||
reward: 로그인 시 테마 추첨권 지급
|
||||
cta: 테마 보기
|
||||
waiting: 대기 중
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Сохранить как Новый Профиль
|
||||
new-profile-name: Profile name
|
||||
manage: Управление профилями
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: Лунный подарок
|
||||
title: Подарок за 7 дней входа
|
||||
eventName: За вход можно получить звёздную пыль, рамку аватара и памятные награды.
|
||||
date: 03/28 - 04/10
|
||||
reward: До 120 звёздной пыли
|
||||
cta: Войти сейчас
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: Бонус стройки
|
||||
title: Дополнительный ящик снабжения
|
||||
eventName: Выполняйте строительные задания и получайте купоны и двойные материалы.
|
||||
date: 03/30 - 04/12
|
||||
reward: 5 купонов + материалы усиления
|
||||
cta: Открыть задания
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: Весенний розыгрыш
|
||||
title: Весенний карнавал
|
||||
eventName: Участвуйте в розыгрыше и открывайте пузырьки, подвески и тематическую мебель.
|
||||
date: 04/01 - 04/18
|
||||
reward: Первый розыгрыш со скидкой + редкая подвеска
|
||||
cta: Перейти к розыгрышу
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: Купон
|
||||
title: Купон на пополнение
|
||||
eventName: Завершите диалоговые задания и получите купоны на наборы диалогов и голосов.
|
||||
date: 04/02 - 04/20
|
||||
reward: Скидка 6 от 30 / 15 от 68
|
||||
cta: Получить купон
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: Тема комнаты
|
||||
title: Весеннее оформление
|
||||
eventName: Откройте темы окна, стола и ночника, чтобы обновить домашнее пространство.
|
||||
date: 04/05 - 04/22
|
||||
reward: Билет на тему за вход
|
||||
cta: Открыть тему
|
||||
waiting: Ожидание
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Save as New Profile
|
||||
new-profile-name: Profile name
|
||||
manage: Manage Profiles
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: Qua trang
|
||||
title: Quà đăng nhập 7 ngày
|
||||
eventName: Đăng nhập để nhận bụi sao, khung avatar và quà ra mắt đặc biệt.
|
||||
date: 03/28 - 04/10
|
||||
reward: Tối đa 120 bụi sao
|
||||
cta: Điểm danh ngay
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: Thuong xay dung
|
||||
title: Thùng tiếp tế thêm quà
|
||||
eventName: Hoàn thành nhiệm vụ xây dựng để nhận phiếu tiếp tế và nguyên liệu nhân đôi.
|
||||
date: 03/30 - 04/12
|
||||
reward: 5 phiếu tiếp tế + nguyên liệu nâng cấp
|
||||
cta: Xem nhiệm vụ
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: Rut thuong xuan
|
||||
title: Lễ hội capsule
|
||||
eventName: Tham gia vòng quay mùa xuân để mở khóa bong bóng chat, charm và nội thất chủ đề.
|
||||
date: 04/01 - 04/18
|
||||
reward: Lượt đầu giảm nửa giá + charm hiếm
|
||||
cta: Đến quay ngay
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: Phieu giam gia
|
||||
title: Phiếu ưu đãi nạp
|
||||
eventName: Hoàn thành tương tác hội thoại để nhận phiếu cho gói mở rộng và gói giọng nói.
|
||||
date: 04/02 - 04/20
|
||||
reward: Giảm 6 cho 30 / giảm 15 cho 68
|
||||
cta: Nhận phiếu
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: Chu de phong
|
||||
title: Cập nhật chủ đề xuân
|
||||
eventName: Mở khóa chủ đề cửa sổ, bàn và đèn ngủ để làm mới không gian trang chủ.
|
||||
date: 04/05 - 04/22
|
||||
reward: Tặng vé quay chủ đề khi đăng nhập
|
||||
cta: Xem chủ đề
|
||||
waiting: Đang chờ
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: 存为新角色
|
||||
new-profile-name: 角色卡名称
|
||||
manage: 管理角色卡
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: 月光礼盒
|
||||
title: 七日签到礼
|
||||
eventName: 完成签到即可领取星砂、头像框与登场纪念礼
|
||||
date: 03/28 - 04/10
|
||||
reward: 累计可得 120 星砂
|
||||
cta: 立即签到
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: 建造加成
|
||||
title: 补给箱加送
|
||||
eventName: 完成建造任务可额外掉落补给券,素材收益双倍结算
|
||||
date: 03/30 - 04/12
|
||||
reward: 补给券 x5 + 强化素材
|
||||
cta: 查看任务
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: 春日抽奖
|
||||
title: 扭蛋嘉年华
|
||||
eventName: 参与春日池抽奖,解锁聊天气泡、挂件与主题家具
|
||||
date: 04/01 - 04/18
|
||||
reward: 首抽半价 + 稀有挂件
|
||||
cta: 前往抽取
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: 优惠券
|
||||
title: 充值优惠券
|
||||
eventName: 完成对话互动即可领取充值券,可用于对话扩展包与语音包
|
||||
date: 04/02 - 04/20
|
||||
reward: 满 30 减 6 / 满 68 减 15
|
||||
cta: 领取优惠券
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: 房间主题
|
||||
title: 春季布景更新
|
||||
eventName: 解锁窗台、桌摆与晚安灯饰三套主题,快速焕新主页空间
|
||||
date: 04/05 - 04/22
|
||||
reward: 登录即送布景抽取券
|
||||
cta: 查看主题
|
||||
waiting: 等待中
|
||||
|
||||
@@ -41,4 +41,46 @@ profile-switcher:
|
||||
save-as-new: Save as New Profile
|
||||
new-profile-name: Profile name
|
||||
manage: Manage Profiles
|
||||
promo-banner:
|
||||
items:
|
||||
signin:
|
||||
watermark: Moon
|
||||
fallbackLabel: 月光贈禮
|
||||
title: 七日簽到禮
|
||||
eventName: 完成簽到即可領取星砂、頭像框與登場紀念禮
|
||||
date: 03/28 - 04/10
|
||||
reward: 累計可得 120 星砂
|
||||
cta: 立即簽到
|
||||
build:
|
||||
watermark: Build
|
||||
fallbackLabel: 建造加成
|
||||
title: 補給箱加送
|
||||
eventName: 完成建造任務可額外掉落補給券,素材收益雙倍結算
|
||||
date: 03/30 - 04/12
|
||||
reward: 補給券 x5 + 強化素材
|
||||
cta: 查看任務
|
||||
spring:
|
||||
watermark: Spring
|
||||
fallbackLabel: 春日抽獎
|
||||
title: 扭蛋嘉年華
|
||||
eventName: 參與春日池抽獎,解鎖聊天氣泡、掛件與主題家具
|
||||
date: 04/01 - 04/18
|
||||
reward: 首抽半價 + 稀有掛件
|
||||
cta: 前往抽取
|
||||
coupon:
|
||||
watermark: Talk
|
||||
fallbackLabel: 優惠券
|
||||
title: 充值優惠券
|
||||
eventName: 完成對話互動即可領取充值券,可用於對話擴展包與語音包
|
||||
date: 04/02 - 04/20
|
||||
reward: 滿 30 減 6 / 滿 68 減 15
|
||||
cta: 領取優惠券
|
||||
home:
|
||||
watermark: Home
|
||||
fallbackLabel: 房間主題
|
||||
title: 春季佈景更新
|
||||
eventName: 解鎖窗台、桌擺與晚安燈飾三套主題,快速煥新主頁空間
|
||||
date: 04/05 - 04/22
|
||||
reward: 登錄即送佈景抽取券
|
||||
cta: 查看主題
|
||||
waiting: 等待中
|
||||
|
||||
@@ -103,6 +103,8 @@
|
||||
"d3": "catalog:",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.3",
|
||||
"embla-carousel-autoplay": "9.0.0-rc01",
|
||||
"embla-carousel-vue": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"gpuu": "^1.0.6",
|
||||
"hono": "catalog:",
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<script setup lang="ts">
|
||||
import type { PromoBannerAction, PromoBannerItem, PromoBannerItemKey, PromoBannerVisual } from './promo-banner'
|
||||
|
||||
import useEmblaCarousel from 'embla-carousel-vue'
|
||||
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAuthStore } from '../../../stores/auth'
|
||||
import { getPromoBannerFallbackLabelKey, promoBannerVisuals } from './promo-banner'
|
||||
import { usePromoBannerLayout } from './use-promo-banner-layout'
|
||||
|
||||
const router = useRouter()
|
||||
const { locale, t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const isVisible = ref(false)
|
||||
|
||||
function translateBannerItem(key: PromoBannerItemKey): PromoBannerItem {
|
||||
const prefix = `stage.promo-banner.items.${key}`
|
||||
|
||||
return {
|
||||
watermark: t(`${prefix}.watermark`),
|
||||
title: t(`${prefix}.title`),
|
||||
eventName: t(`${prefix}.eventName`),
|
||||
date: t(`${prefix}.date`),
|
||||
reward: t(`${prefix}.reward`),
|
||||
cta: t(`${prefix}.cta`),
|
||||
}
|
||||
}
|
||||
|
||||
const items = computed<(PromoBannerItem & PromoBannerVisual)[]>(() =>
|
||||
promoBannerVisuals.map(item => ({
|
||||
...item,
|
||||
...translateBannerItem(item.key),
|
||||
})),
|
||||
)
|
||||
|
||||
const currentIndex = ref(0)
|
||||
let autoplayTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const [_emblaRef, emblaApi] = useEmblaCarousel({ loop: true })
|
||||
|
||||
function stopAutoplay() {
|
||||
if (autoplayTimer !== undefined) {
|
||||
clearInterval(autoplayTimer)
|
||||
autoplayTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function startAutoplay() {
|
||||
stopAutoplay()
|
||||
autoplayTimer = setInterval(() => {
|
||||
emblaApi.value?.goToNext()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
watch(emblaApi, (api, _, onCleanup) => {
|
||||
if (!api) {
|
||||
return
|
||||
}
|
||||
|
||||
const syncIndex = () => {
|
||||
currentIndex.value = api.selectedSnap()
|
||||
}
|
||||
|
||||
syncIndex()
|
||||
api.on('select', syncIndex)
|
||||
|
||||
const rootNode = api.rootNode()
|
||||
rootNode.addEventListener('mouseenter', stopAutoplay)
|
||||
rootNode.addEventListener('mouseleave', startAutoplay)
|
||||
|
||||
startAutoplay()
|
||||
|
||||
onCleanup(() => {
|
||||
api.off?.('select', syncIndex)
|
||||
rootNode.removeEventListener('mouseenter', stopAutoplay)
|
||||
rootNode.removeEventListener('mouseleave', startAutoplay)
|
||||
stopAutoplay()
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
function scrollTo(index: number) {
|
||||
if (emblaApi.value) {
|
||||
emblaApi.value.goTo(index)
|
||||
startAutoplay()
|
||||
}
|
||||
}
|
||||
|
||||
function handlePromoBannerAction(action: PromoBannerAction) {
|
||||
close()
|
||||
|
||||
if (action.type === 'login') {
|
||||
authStore.needsLogin = true
|
||||
return
|
||||
}
|
||||
|
||||
void router.push(action.to)
|
||||
}
|
||||
|
||||
function close() {
|
||||
isVisible.value = false
|
||||
stopAutoplay()
|
||||
}
|
||||
|
||||
function open() {
|
||||
isVisible.value = true
|
||||
startAutoplay()
|
||||
}
|
||||
|
||||
const activeItem = computed(() => items.value[currentIndex.value] ?? items.value[0])
|
||||
const {
|
||||
buttonClass,
|
||||
descriptionClass,
|
||||
metaClass,
|
||||
titleClass,
|
||||
watermarkClass,
|
||||
} = usePromoBannerLayout(locale)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAutoplay()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed bottom-10 left-6 z-50 <md:hidden">
|
||||
<button
|
||||
v-if="!isVisible"
|
||||
:class="[
|
||||
'pointer-events-auto max-h-[10lh] min-h-[1lh]',
|
||||
'bg-neutral-100 dark:bg-neutral-800',
|
||||
'text-lg text-neutral-500 dark:text-neutral-400',
|
||||
'flex items-center justify-center rounded-md p-2 outline-none',
|
||||
'transition-colors transition-transform active:scale-95',
|
||||
]"
|
||||
title="Open promo banner"
|
||||
type="button"
|
||||
@click="open"
|
||||
>
|
||||
<div class="i-solar:gift-bold-duotone" />
|
||||
</button>
|
||||
|
||||
<div v-else class="pointer-events-auto relative flex flex-col items-start">
|
||||
<div :class="['relative h-60 w-108 overflow-hidden rounded-3xl border border-white/8 bg-neutral-900/86 shadow-2xl backdrop-blur-xl', 'ring-1 ring-black/10']">
|
||||
<button
|
||||
:class="[
|
||||
'absolute right-3 top-3 z-30 h-8 w-8 flex items-center justify-center rounded-full',
|
||||
'text-white/55 transition-colors hover:bg-white/10 hover:text-white',
|
||||
]"
|
||||
type="button"
|
||||
@click="close"
|
||||
>
|
||||
<div class="i-lucide-x h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'pointer-events-none absolute inset-0 bg-gradient-to-br',
|
||||
activeItem.accentClass,
|
||||
]"
|
||||
/>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-28 w-px bg-white/8" />
|
||||
|
||||
<div ref="_emblaRef" class="embla h-full w-full">
|
||||
<div class="embla__container h-full">
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="embla__slide relative h-full min-w-0"
|
||||
>
|
||||
<div class="absolute inset-y-0 left-0 w-78 px-4 py-4">
|
||||
<div class="absolute left-5 top-4 z-0 select-none">
|
||||
<span :class="['block text-white/8 leading-none uppercase', watermarkClass]">
|
||||
{{ item.watermark }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 h-full flex flex-col pr-2">
|
||||
<div :class="['flex items-center gap-1.5 text-white/72', metaClass]">
|
||||
<div class="i-solar:calendar-mark-bold-duotone text-[13px] text-primary-200" />
|
||||
<span>{{ item.date }}</span>
|
||||
</div>
|
||||
|
||||
<div :class="['mt-3 text-white break-words', titleClass]">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
|
||||
<div :class="['mt-2 text-white/68 break-words', descriptionClass]">
|
||||
{{ item.eventName }}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1.5">
|
||||
<div :class="['flex items-center gap-1.5 text-white/72', metaClass]">
|
||||
<div class="i-solar:gift-bold-duotone text-[13px] text-amber-300" />
|
||||
<span>{{ item.reward }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:class="[
|
||||
'mt-auto mb-1 w-fit rounded-full px-4 py-2 text-neutral-950 transition-transform active:scale-95',
|
||||
buttonClass,
|
||||
'bg-white shadow-[0_8px_24px_rgba(255,255,255,0.16)] hover:translate-y-[-1px]',
|
||||
]"
|
||||
type="button"
|
||||
@click="handlePromoBannerAction(item.action)"
|
||||
>
|
||||
{{ item.cta }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-y-3 right-3 w-23 overflow-hidden border border-white/10 rounded-2xl bg-white/5">
|
||||
<img
|
||||
v-if="item.image"
|
||||
:src="item.image"
|
||||
:alt="item.title"
|
||||
class="h-full w-full object-cover"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
'relative h-full w-full overflow-hidden bg-gradient-to-br',
|
||||
item.fallbackClass,
|
||||
]"
|
||||
>
|
||||
<div class="absolute left-4 top-4 h-10 w-10 rounded-2xl bg-white/12 blur-sm" />
|
||||
<div class="absolute right-3 top-3 h-14 w-14 rounded-full bg-amber-300/22 blur-md" />
|
||||
<div class="absolute inset-x-4 bottom-4 h-18 border border-white/12 rounded-[1.4rem] bg-black/18 backdrop-blur-sm" />
|
||||
<div :class="[item.fallbackIcon, item.fallbackIconClass, 'absolute left-5 top-5 text-2xl']" />
|
||||
<div :class="[item.fallbackIcon, item.fallbackIconClass, 'absolute bottom-7 right-4 text-4xl opacity-90']" />
|
||||
<div class="absolute bottom-5 left-4 text-[10px] text-white/70 font-700 tracking-[0.3em] uppercase">
|
||||
{{ t(getPromoBannerFallbackLabelKey(item.key)) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pointer-events-none absolute inset-0 from-neutral-950/55 via-transparent to-white/8 bg-gradient-to-t" />
|
||||
<div class="pointer-events-none absolute bottom-0 left-0 right-0 h-18 from-neutral-950/88 to-transparent bg-gradient-to-t" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-3.5 right-4 z-30 flex items-center gap-2">
|
||||
<span class="text-[11px] text-white/42 font-600">{{ currentIndex + 1 }}/{{ items.length }}</span>
|
||||
<div class="flex gap-1.5">
|
||||
<button
|
||||
v-for="(_, index) in items"
|
||||
:key="`dot-${index}`"
|
||||
:class="[
|
||||
'h-2.5 rounded-full transition-all duration-300',
|
||||
currentIndex === index ? 'w-5 bg-white' : 'w-2.5 bg-white/30 hover:bg-white/50',
|
||||
]"
|
||||
type="button"
|
||||
@click="scrollTo(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.embla {
|
||||
overflow: hidden;
|
||||
}
|
||||
.embla__container {
|
||||
display: flex;
|
||||
}
|
||||
.embla__slide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as HoloCoupon } from './holo-coupon.vue'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
getPromoBannerFallbackLabelKey,
|
||||
promoBannerVisuals,
|
||||
} from './promo-banner'
|
||||
|
||||
describe('promo banner visuals', () => {
|
||||
it('defines concrete actions for every promo banner item', () => {
|
||||
expect(promoBannerVisuals).toMatchObject([
|
||||
{
|
||||
key: 'signin',
|
||||
action: { type: 'login' },
|
||||
},
|
||||
{
|
||||
key: 'build',
|
||||
action: { type: 'route', to: '/settings/modules/consciousness' },
|
||||
},
|
||||
{
|
||||
key: 'spring',
|
||||
action: { type: 'route', to: '/settings/flux' },
|
||||
},
|
||||
{
|
||||
key: 'coupon',
|
||||
action: { type: 'route', to: '/settings/flux' },
|
||||
},
|
||||
{
|
||||
key: 'home',
|
||||
action: { type: 'route', to: '/settings/scene' },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('resolves fallback labels through locale keys instead of hard-coded English strings', () => {
|
||||
expect(getPromoBannerFallbackLabelKey('signin')).toBe('stage.promo-banner.items.signin.fallbackLabel')
|
||||
expect(getPromoBannerFallbackLabelKey('build')).toBe('stage.promo-banner.items.build.fallbackLabel')
|
||||
expect(getPromoBannerFallbackLabelKey('spring')).toBe('stage.promo-banner.items.spring.fallbackLabel')
|
||||
expect(getPromoBannerFallbackLabelKey('coupon')).toBe('stage.promo-banner.items.coupon.fallbackLabel')
|
||||
expect(getPromoBannerFallbackLabelKey('home')).toBe('stage.promo-banner.items.home.fallbackLabel')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import factorioPoster from '../../../assets/factorio-simple.png'
|
||||
import onboardingPoster from '../../../assets/onboarding.avif'
|
||||
|
||||
export interface PromoBannerItem {
|
||||
watermark: string
|
||||
title: string
|
||||
eventName: string
|
||||
date: string
|
||||
reward: string
|
||||
cta: string
|
||||
}
|
||||
|
||||
export type PromoBannerItemKey = 'signin' | 'build' | 'spring' | 'coupon' | 'home'
|
||||
|
||||
export type PromoBannerAction
|
||||
= | { type: 'login' }
|
||||
| { type: 'route', to: string }
|
||||
|
||||
export interface PromoBannerVisual {
|
||||
key: PromoBannerItemKey
|
||||
image: string
|
||||
action: PromoBannerAction
|
||||
accentClass: string
|
||||
fallbackIcon: string
|
||||
fallbackIconClass: string
|
||||
fallbackClass: string
|
||||
}
|
||||
|
||||
export function getPromoBannerFallbackLabelKey(key: PromoBannerItemKey) {
|
||||
return `stage.promo-banner.items.${key}.fallbackLabel`
|
||||
}
|
||||
|
||||
export const promoBannerVisuals: PromoBannerVisual[] = [
|
||||
{
|
||||
key: 'signin',
|
||||
image: onboardingPoster,
|
||||
action: { type: 'login' },
|
||||
accentClass: 'from-fuchsia-500/30 via-rose-400/18 to-transparent',
|
||||
fallbackIcon: 'i-solar:stars-line-duotone',
|
||||
fallbackIconClass: 'text-amber-100',
|
||||
fallbackClass: 'from-fuchsia-300/25 via-rose-300/14 to-violet-400/20',
|
||||
},
|
||||
{
|
||||
key: 'build',
|
||||
image: factorioPoster,
|
||||
action: { type: 'route', to: '/settings/modules/consciousness' },
|
||||
accentClass: 'from-cyan-500/30 via-sky-400/18 to-transparent',
|
||||
fallbackIcon: 'i-solar:box-bold-duotone',
|
||||
fallbackIconClass: 'text-cyan-100',
|
||||
fallbackClass: 'from-cyan-300/25 via-sky-300/14 to-blue-400/20',
|
||||
},
|
||||
{
|
||||
key: 'spring',
|
||||
image: '',
|
||||
action: { type: 'route', to: '/settings/flux' },
|
||||
accentClass: 'from-amber-400/30 via-orange-300/18 to-transparent',
|
||||
fallbackIcon: 'i-solar:gift-bold-duotone',
|
||||
fallbackIconClass: 'text-white/88',
|
||||
fallbackClass: 'from-amber-300/25 via-rose-300/14 to-fuchsia-400/20',
|
||||
},
|
||||
{
|
||||
key: 'coupon',
|
||||
image: '',
|
||||
action: { type: 'route', to: '/settings/flux' },
|
||||
accentClass: 'from-emerald-400/28 via-teal-300/16 to-transparent',
|
||||
fallbackIcon: 'i-solar:ticket-sale-bold-duotone',
|
||||
fallbackIconClass: 'text-emerald-100',
|
||||
fallbackClass: 'from-emerald-300/24 via-cyan-300/12 to-teal-400/18',
|
||||
},
|
||||
{
|
||||
key: 'home',
|
||||
image: '',
|
||||
action: { type: 'route', to: '/settings/scene' },
|
||||
accentClass: 'from-sky-400/28 via-indigo-300/14 to-transparent',
|
||||
fallbackIcon: 'i-solar:home-angle-bold-duotone',
|
||||
fallbackIconClass: 'text-sky-100',
|
||||
fallbackClass: 'from-sky-300/25 via-indigo-300/14 to-violet-400/18',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
const cjkLocalePrefixes = ['zh', 'ja', 'ko']
|
||||
|
||||
export function usePromoBannerLayout(locale: Ref<string>) {
|
||||
const isCjkLocale = computed(() =>
|
||||
cjkLocalePrefixes.some(prefix => locale.value.startsWith(prefix)),
|
||||
)
|
||||
|
||||
const titleClass = computed(() =>
|
||||
isCjkLocale.value
|
||||
? 'text-[28px] leading-none font-700'
|
||||
: 'font-sans text-[22px] leading-tight font-600 tracking-normal',
|
||||
)
|
||||
|
||||
const descriptionClass = computed(() =>
|
||||
isCjkLocale.value
|
||||
? 'max-w-58 text-[13px] leading-5'
|
||||
: 'max-w-60 font-sans text-[12px] leading-4.5 font-500',
|
||||
)
|
||||
|
||||
const metaClass = computed(() =>
|
||||
isCjkLocale.value
|
||||
? 'text-[11px]'
|
||||
: 'font-sans text-[11px] font-500',
|
||||
)
|
||||
|
||||
const buttonClass = computed(() =>
|
||||
isCjkLocale.value
|
||||
? 'text-xs font-700'
|
||||
: 'font-sans text-[12px] font-600 tracking-normal',
|
||||
)
|
||||
|
||||
const watermarkClass = computed(() =>
|
||||
isCjkLocale.value
|
||||
? 'text-5xl font-600'
|
||||
: 'font-sans text-[44px] font-500 tracking-[0.08em]',
|
||||
)
|
||||
|
||||
return {
|
||||
buttonClass,
|
||||
descriptionClass,
|
||||
isCjkLocale,
|
||||
metaClass,
|
||||
titleClass,
|
||||
watermarkClass,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './about'
|
||||
export * from './chat'
|
||||
export * from './dialogs'
|
||||
export * from './hologram'
|
||||
export * from './providers'
|
||||
export * from './settings'
|
||||
export * from './toasters'
|
||||
|
||||
Reference in New Issue
Block a user