feat: 新增首页和图片资源

This commit is contained in:
JiaJun
2026-04-24 18:02:42 +08:00
parent bd92f10b83
commit 9127a06d4a
179 changed files with 3424 additions and 101 deletions

View File

@@ -0,0 +1,66 @@
import { SmartImage } from '@/components/smart-image'
const cx = (...classes: Array<string | false | null | undefined>) =>
classes.filter(Boolean).join(' ')
const animalModules = import.meta.glob('../../../../assets/animal/*.webp', {
eager: true,
import: 'default',
}) as Record<string, string>
const animalImageList = Object.entries(animalModules)
.map(([path, url]) => {
const match = path.match(/\/(\d+)\.webp$/)
return {
id: Number(match?.[1] ?? 0),
url,
}
})
.filter((item) => item.id > 0)
.sort((left, right) => left.id - right.id)
interface DesktopAnimalProps {
activeId?: number | null
className?: string
itemClassName?: string
imageClassName?: string
onSelect?: (animalId: number) => void
}
export function DesktopAnimal({
activeId,
className,
itemClassName,
imageClassName,
onSelect,
}: DesktopAnimalProps) {
return (
<section className={cx('grid grid-cols-6', className)}>
{animalImageList.map((item) => {
const isActive = item.id === activeId
return (
<button
key={item.id}
type="button"
onClick={() => onSelect?.(item.id)}
className={cx(
'flex flex-col items-center transition',
'cursor-pointer',
isActive &&
'border-[rgba(255,151,15,0.95)] shadow-[inset_0_0_16px_rgba(255,151,15,0.55)]',
itemClassName,
)}
>
<SmartImage
src={item.url}
alt={`animal-${item.id}`}
className={cx('h-[110px] w-[220px]', imageClassName)}
/>
</button>
)
})}
</section>
)
}

View File

@@ -0,0 +1,97 @@
import { CircleAlert, Mail, Plus, Volume2 } from 'lucide-react'
import avatar from '@/assets/system/avatar.webp'
import diamond from '@/assets/system/diamond.webp'
import logo from '@/assets/system/logo.webp'
import wifi from '@/assets/system/wifi.webp'
import { SmartImage } from '@/components/smart-image.tsx'
export function DesktopHeader() {
return (
<header className="sticky top-0 z-30 border-b border-white/8 bg-slate-950/70 backdrop-blur-xl">
<div className="h-[70px] w-full flex items-center px-[12px]">
<div className="w-[400px] flex justify-center items-center h-full border-r border-[rgba(128,223,231,0.65)]">
<SmartImage
src={logo}
alt="logo"
priority
className="w-[320px] h-[40px]"
/>
</div>
<div className="w-[148px] flex justify-center items-center gap-[10px] h-full px-[20px] border-r border-[rgba(128,223,231,0.65)]">
<SmartImage
src={wifi}
alt="wifi"
priority
className="w-[28px] h-[20px]"
/>
<div className={'text-[#74FF69] text-[20px]'}>
24 <span className={'text-[16px]'}>ms</span>
</div>
</div>
<div className="w-[175px] flex flex-col justify-center items-center gap-[5px] h-full px-[20px] border-r border-[rgba(128,223,231,0.65)]">
<div>System Time</div>
<div>20:05:12 GMT+08</div>
</div>
<div className="flex-1 flex items-center justify-around gap-[10px] h-full px-[20px] text-[#D5FBFF] border-r border-[rgba(128,223,231,0.65)]">
<div className={'flex gap-[10px] common-neon-inset'}>
<CircleAlert color={'#57B8BF'} />
<div>Rules & Ddds</div>
</div>
<div className={'flex gap-[10px] common-neon-inset'}>
<Mail color={'#57B8BF'} />
<div>Pesan</div>
</div>
<div className={'flex gap-[10px] common-neon-inset'}>
<Volume2 color={'#57B8BF'} />
<div>BGM</div>
</div>
<div className={'flex gap-[10px] common-neon-inset'}>
<CircleAlert color={'#57B8BF'} />
<div>ID</div>
</div>
</div>
<div className="flex-1 flex items-center h-full text-[#D5FBFF]">
<div className={'relative flex items-center justify-center'}>
<SmartImage
src={avatar}
alt="avatar"
priority
className="absolute left-[20px] top-0 z-20 w-[50px] h-[50px]"
/>
<div
className={
'common-neon-inset !py-[20px] flex items-center justify-center box-border w-[175px] h-[36px]'
}
>
Biomond Balance
</div>
</div>
<div className={'relative flex items-center justify-center h-full'}>
<SmartImage
src={diamond}
alt="diamond"
priority
className="absolute left-[30px] top-0 z-20 w-[50px] h-[50px]"
/>
<div
className={
'common-neon-inset !py-[20px] flex items-center justify-end gap-[10px] w-[175px] h-[36px]'
}
>
<div>5994469974</div>
<div className={'bg-[#2D4559] rounded-xs cursor-pointer p-[5px]'}>
<Plus size={16} />
</div>
</div>
</div>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,12 @@
import { Megaphone } from 'lucide-react'
export function DesktopTitle() {
return (
<section className="common-neon-inset flex items-center text-[#FF970F] flex gap-[10px] !px-[20px] h-[50px]">
<Megaphone color={'#57B8BF'} />
<div>
Selamat kepada pemain Wu Yanzu yang telah memenangkan hadiah utama
sebesar 5.000 yuan sebanyak lima kali berturut-turut!🎉🎉🎉
</div>
</section>
)
}

View File

@@ -0,0 +1,2 @@
export { DesktopHeader } from '@/features/game/components/desktop/desktop-header'
export { GameAnnouncementModal } from '@/features/game/components/shared/game-announcement-modal'

View File

@@ -0,0 +1,115 @@
import type { ReactNode } from 'react'
const cx = (...classes: Array<string | false | null | undefined>) =>
classes.filter(Boolean).join(' ')
type GameTone = 'neutral' | 'brand' | 'success' | 'warning' | 'danger'
interface GameOverlayAction {
label: string
onClick: () => void
tone?: GameTone
}
interface GameAnnouncementModalProps {
open: boolean
title: string
description?: ReactNode
eyebrow?: string
tone?: GameTone
primaryAction?: GameOverlayAction
secondaryAction?: GameOverlayAction
children?: ReactNode
}
const toneClasses: Record<GameTone, string> = {
neutral: 'border-white/10 bg-slate-950/92',
brand: 'border-cyan-300/25 bg-slate-950/94',
success: 'border-emerald-300/25 bg-slate-950/94',
warning: 'border-amber-300/25 bg-slate-950/94',
danger: 'border-rose-300/25 bg-slate-950/94',
}
const actionToneClasses: Record<GameTone, string> = {
neutral: 'border-white/10 bg-white/[0.06] text-white hover:bg-white/[0.1]',
brand: 'border-cyan-300/25 bg-cyan-300/14 text-cyan-50 hover:bg-cyan-300/22',
success:
'border-emerald-300/25 bg-emerald-300/14 text-emerald-50 hover:bg-emerald-300/22',
warning:
'border-amber-300/25 bg-amber-300/14 text-amber-50 hover:bg-amber-300/22',
danger: 'border-rose-300/25 bg-rose-300/14 text-rose-50 hover:bg-rose-300/22',
}
function ModalAction({ label, onClick, tone = 'brand' }: GameOverlayAction) {
return (
<button
type="button"
onClick={onClick}
className={cx(
'inline-flex min-h-12 items-center justify-center rounded-full border px-5 text-sm font-semibold tracking-[0.18em] uppercase transition duration-200',
actionToneClasses[tone],
)}
>
{label}
</button>
)
}
export function GameAnnouncementModal({
open,
title,
description,
eyebrow = '',
tone = 'brand',
primaryAction,
secondaryAction,
children,
}: GameAnnouncementModalProps) {
if (!open) {
return null
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/82 px-4 py-8 backdrop-blur-md">
<div
role="dialog"
aria-modal="true"
aria-labelledby="game-announcement-title"
className={cx(
'w-full max-w-xl rounded-[32px] border p-6 shadow-[0_40px_120px_-40px_rgba(15,23,42,0.95)] sm:p-7',
toneClasses[tone],
)}
>
<div className="space-y-4">
<div className="space-y-2">
<span className="inline-flex rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[0.68rem] font-semibold tracking-[0.24em] text-slate-300 uppercase">
{eyebrow}
</span>
<h2
id="game-announcement-title"
className="text-2xl font-semibold tracking-tight text-white sm:text-[2rem]"
>
{title}
</h2>
{description ? (
<div className="text-sm leading-7 text-slate-300">
{description}
</div>
) : null}
</div>
{children ? (
<div className="rounded-[24px] border border-white/8 bg-white/[0.03] p-4">
{children}
</div>
) : null}
{secondaryAction || primaryAction ? (
<div className="flex flex-wrap gap-3 pt-2">
{secondaryAction ? <ModalAction {...secondaryAction} /> : null}
{primaryAction ? <ModalAction {...primaryAction} /> : null}
</div>
) : null}
</div>
</div>
</div>
)
}