首页自适应优化:背景视频铺满整屏
- index.css: 新增 .home-bg-cover(absolute inset-0) - home-page.tsx: home-canvas 前插入背景视频层(成对插入,不破坏JSX) - 效果:背景视频独立铺满真实屏,540画布内保留原视频(前景对齐美术构图), 画布外区域由铺满背景自然延续,消除瘦长屏的深蓝色块边界,无边界沉浸 - DOM验证: home-bg-cover + home-canvas 均生效
This commit is contained in:
@@ -79,6 +79,7 @@ select {
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
.reference-fit-frame {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'
|
||||
import { getDaysLeft, getElapsedDays } from '../lib/anniversary-rules'
|
||||
import { defaultHomeLayout } from '../lib/home-layout'
|
||||
import { shouldUseStaticBackgroundMedia } from '../lib/platform'
|
||||
import type {
|
||||
Anniversary,
|
||||
AppRoute,
|
||||
HomeBackgroundMode,
|
||||
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'
|
||||
import { getDaysLeft, getElapsedDays } from '../lib/anniversary-rules'
|
||||
import { defaultHomeLayout } from '../lib/home-layout'
|
||||
import { shouldUseStaticBackgroundMedia } from '../lib/platform'
|
||||
import type {
|
||||
Anniversary,
|
||||
AppRoute,
|
||||
HomeBackgroundMode,
|
||||
HomeCoverId,
|
||||
HomeLayout,
|
||||
HomeLayoutElementId,
|
||||
@@ -15,20 +15,20 @@ import type {
|
||||
import homeBirthdayBackground from '../assets/desktop-assets/20260601-151705.mp4'
|
||||
import activityBanner1 from '../assets/desktop-assets/activity-image-1.png'
|
||||
import activityBanner2 from '../assets/desktop-assets/activity-image-2.png'
|
||||
import activityIcon from '../assets/desktop-assets/home-icons/activity.png'
|
||||
import albumIcon from '../assets/desktop-assets/home-icons/album.png'
|
||||
import anniversaryIcon from '../assets/desktop-assets/home-icons/anniversary.png'
|
||||
import avatarFrameImage from '../assets/desktop-assets/home-icons/avatar-frame.png'
|
||||
import bottomIconBar from '../assets/desktop-assets/home-icons/bottom-icon-bar.png'
|
||||
import activityIcon from '../assets/desktop-assets/home-icons/activity.png'
|
||||
import albumIcon from '../assets/desktop-assets/home-icons/album.png'
|
||||
import anniversaryIcon from '../assets/desktop-assets/home-icons/anniversary.png'
|
||||
import avatarFrameImage from '../assets/desktop-assets/home-icons/avatar-frame.png'
|
||||
import bottomIconBar from '../assets/desktop-assets/home-icons/bottom-icon-bar.png'
|
||||
import momentsIcon from '../assets/desktop-assets/home-icons/moments.png'
|
||||
import petIcon from '../assets/desktop-assets/home-icons/pet.png'
|
||||
import profilePlateImage from '../assets/desktop-assets/home-icons/profile-plate.png'
|
||||
import shopIcon from '../assets/desktop-assets/home-icons/shop.png'
|
||||
import supportIcon from '../assets/desktop-assets/home-icons/support.png'
|
||||
import voteIcon from '../assets/desktop-assets/home-icons/vote.png'
|
||||
import homeChoiceBoxImage from '../assets/desktop-assets/home-dialog/choice-box.png'
|
||||
import homeDialogBoxImage from '../assets/desktop-assets/home-dialog/dialog-box.png'
|
||||
import homeAnniversaryImage from '../assets/desktop-assets/home-anniversary/home-anniversary.png'
|
||||
import voteIcon from '../assets/desktop-assets/home-icons/vote.png'
|
||||
import homeChoiceBoxImage from '../assets/desktop-assets/home-dialog/choice-box.png'
|
||||
import homeDialogBoxImage from '../assets/desktop-assets/home-dialog/dialog-box.png'
|
||||
import homeAnniversaryImage from '../assets/desktop-assets/home-anniversary/home-anniversary.png'
|
||||
import homeDayBackground from '../assets/desktop-assets/日常首页-白天.mp4'
|
||||
import homeNightBackground from '../assets/desktop-assets/日常首页-夜晚.mp4'
|
||||
import coverChiyingXingge from '../assets/desktop-assets/home-covers/cover-chiying-xingge.mp4'
|
||||
@@ -137,19 +137,19 @@ const homeCoverBackgrounds: Record<HomeCoverId, string> = {
|
||||
'xingye-xiehou': coverXingyeXiehou,
|
||||
}
|
||||
|
||||
const homeAnniversaryAssetRatio = 2992 / 1392
|
||||
|
||||
interface HomeAnniversaryReminder {
|
||||
days: number
|
||||
statusLabel: '已经' | '还剩'
|
||||
title: string
|
||||
}
|
||||
|
||||
const defaultHomeAnniversaryReminder: HomeAnniversaryReminder = {
|
||||
days: 0,
|
||||
statusLabel: '已经',
|
||||
title: '加入 DL.L9 粉丝团',
|
||||
}
|
||||
const homeAnniversaryAssetRatio = 2992 / 1392
|
||||
|
||||
interface HomeAnniversaryReminder {
|
||||
days: number
|
||||
statusLabel: '已经' | '还剩'
|
||||
title: string
|
||||
}
|
||||
|
||||
const defaultHomeAnniversaryReminder: HomeAnniversaryReminder = {
|
||||
days: 0,
|
||||
statusLabel: '已经',
|
||||
title: '加入 DL.L9 粉丝团',
|
||||
}
|
||||
|
||||
const enlargedSideIconLayoutIds = new Set<HomeLayoutElementId>([
|
||||
'homeIconAnniversary',
|
||||
@@ -273,10 +273,10 @@ const HOME_IDLE_DIALOG_DELAY_MS = 12_000
|
||||
const HOME_IDLE_DIALOG_VISIBLE_MS = 30_000
|
||||
const isLegacyMomentsBadgeVisible = false
|
||||
|
||||
interface HomePageProps {
|
||||
anniversaries: Anniversary[]
|
||||
homeBackgroundMode: HomeBackgroundMode
|
||||
homeCoverId: HomeCoverId | null
|
||||
interface HomePageProps {
|
||||
anniversaries: Anniversary[]
|
||||
homeBackgroundMode: HomeBackgroundMode
|
||||
homeCoverId: HomeCoverId | null
|
||||
homeLayout: HomeLayout
|
||||
isHomeLayoutEditing: boolean
|
||||
onHomeLayoutChange: (elementId: HomeLayoutElementId, rect: HomeLayoutRect) => void
|
||||
@@ -285,9 +285,9 @@ interface HomePageProps {
|
||||
user: UserProfile
|
||||
}
|
||||
|
||||
export function HomePage({
|
||||
anniversaries,
|
||||
homeBackgroundMode,
|
||||
export function HomePage({
|
||||
anniversaries,
|
||||
homeBackgroundMode,
|
||||
homeCoverId,
|
||||
homeLayout,
|
||||
isHomeLayoutEditing,
|
||||
@@ -309,9 +309,9 @@ export function HomePage({
|
||||
day: homeDayBackground,
|
||||
night: homeNightBackground,
|
||||
}[backgroundMode]
|
||||
const selectedCoverBackground = homeCoverId ? homeCoverBackgrounds[homeCoverId] : null
|
||||
const activeHomeBackground = selectedCoverBackground ?? homeBackground
|
||||
const homeAnniversaryReminder = getHomeAnniversaryReminder(anniversaries)
|
||||
const selectedCoverBackground = homeCoverId ? homeCoverBackgrounds[homeCoverId] : null
|
||||
const activeHomeBackground = selectedCoverBackground ?? homeBackground
|
||||
const homeAnniversaryReminder = getHomeAnniversaryReminder(anniversaries)
|
||||
|
||||
useEffect(() => {
|
||||
const updateMode = () => setBackgroundMode(getResolvedHomeBackgroundMode(user.birthday, homeBackgroundMode))
|
||||
@@ -454,7 +454,7 @@ export function HomePage({
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden bg-[#061124]">
|
||||
<div className="reference-fit-frame overflow-hidden" onPointerDownCapture={resetHomeIdleTimer}>
|
||||
<div className="home-canvas overflow-hidden" onPointerDownCapture={resetHomeIdleTimer}>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 bg-[radial-gradient(circle_at_50%_18%,rgba(83,169,255,0.34),transparent_30%),linear-gradient(180deg,#18345f_0%,#071a33_48%,#030a17_100%)]"
|
||||
@@ -479,16 +479,16 @@ export function HomePage({
|
||||
onClick={handleBackgroundTalk}
|
||||
type="button"
|
||||
/>
|
||||
{isHomeLayoutEditing ? (
|
||||
<button
|
||||
className="absolute left-1/2 top-[10.5%] z-50 min-h-9 -translate-x-1/2 rounded-full border border-cyan-100/80 bg-sky-950/70 px-4 text-[12px] font-semibold text-white shadow-[0_8px_18px_rgba(0,0,0,0.28)] backdrop-blur-sm active:scale-[0.98]"
|
||||
onClick={() => onNavigate('my')}
|
||||
type="button"
|
||||
>
|
||||
返回我的保存
|
||||
</button>
|
||||
) : null}
|
||||
{activeDialog ? (
|
||||
{isHomeLayoutEditing ? (
|
||||
<button
|
||||
className="absolute left-1/2 top-[10.5%] z-50 min-h-9 -translate-x-1/2 rounded-full border border-cyan-100/80 bg-sky-950/70 px-4 text-[12px] font-semibold text-white shadow-[0_8px_18px_rgba(0,0,0,0.28)] backdrop-blur-sm active:scale-[0.98]"
|
||||
onClick={() => onNavigate('my')}
|
||||
type="button"
|
||||
>
|
||||
返回我的保存
|
||||
</button>
|
||||
) : null}
|
||||
{activeDialog ? (
|
||||
<div
|
||||
className={`absolute inset-x-[4%] bottom-[15.8%] z-40 ${
|
||||
activeDialog.kind === 'idle'
|
||||
@@ -516,13 +516,13 @@ export function HomePage({
|
||||
: 'animate-[home-dialog-float_2500ms_ease-out_both]'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full select-none object-fill opacity-75"
|
||||
draggable={false}
|
||||
src={homeDialogBoxImage}
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full select-none object-fill opacity-75"
|
||||
draggable={false}
|
||||
src={homeDialogBoxImage}
|
||||
/>
|
||||
<span
|
||||
className={`z-10 block text-[14px] font-normal leading-[1.75] tracking-[0.04em] text-white [font-family:'Source_Han_Serif_SC','Noto_Serif_CJK_SC','思源宋体',serif] [text-shadow:1px_1.5px_0_rgba(0,0,0,0.72),0_0_1px_rgba(0,0,0,0.38)] ${
|
||||
activeDialog.kind === 'idle'
|
||||
@@ -548,7 +548,7 @@ export function HomePage({
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full select-none object-fill opacity-85 transition group-hover:opacity-85"
|
||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full select-none object-fill opacity-85 transition group-hover:opacity-85"
|
||||
draggable={false}
|
||||
src={homeChoiceBoxImage}
|
||||
/>
|
||||
@@ -572,21 +572,21 @@ export function HomePage({
|
||||
onChange={onHomeLayoutChange}
|
||||
onClick={() => onNavigate('my')}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none block h-full w-full select-none object-fill drop-shadow-[0_8px_18px_rgba(16,38,74,0.35)]"
|
||||
draggable={false}
|
||||
src={profilePlateImage}
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-[4%] top-[11.7%] z-10 aspect-square w-[29.5%] select-none object-contain drop-shadow-[0_0_8px_rgba(174,220,255,0.55)]"
|
||||
draggable={false}
|
||||
src={avatarFrameImage}
|
||||
/>
|
||||
<span className="absolute left-[36%] top-[33%] max-w-[42%] truncate text-[clamp(8px,2.35vw,12px)] font-light leading-none tracking-[0.04em] text-[#eef6ff] drop-shadow-[0_1px_3px_rgba(7,18,42,0.8)]">
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none block h-full w-full select-none object-fill drop-shadow-[0_8px_18px_rgba(16,38,74,0.35)]"
|
||||
draggable={false}
|
||||
src={profilePlateImage}
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-[4%] top-[11.7%] z-10 aspect-square w-[29.5%] select-none object-contain drop-shadow-[0_0_8px_rgba(174,220,255,0.55)]"
|
||||
draggable={false}
|
||||
src={avatarFrameImage}
|
||||
/>
|
||||
<span className="absolute left-[36%] top-[33%] max-w-[42%] truncate text-[clamp(8px,2.35vw,12px)] font-light leading-none tracking-[0.04em] text-[#eef6ff] drop-shadow-[0_1px_3px_rgba(7,18,42,0.8)]">
|
||||
{user.name}
|
||||
</span>
|
||||
</HomeLayoutBox>
|
||||
@@ -599,14 +599,14 @@ export function HomePage({
|
||||
layout={homeLayout}
|
||||
onChange={onHomeLayoutChange}
|
||||
>
|
||||
<HomeAnniversaryAssetButton
|
||||
reminder={homeAnniversaryReminder}
|
||||
image={homeAnniversaryImage}
|
||||
isEditing={isHomeLayoutEditing}
|
||||
layout={homeLayout}
|
||||
onLayoutChange={onHomeLayoutChange}
|
||||
onOpen={() => onNavigate('anniversary')}
|
||||
/>
|
||||
<HomeAnniversaryAssetButton
|
||||
reminder={homeAnniversaryReminder}
|
||||
image={homeAnniversaryImage}
|
||||
isEditing={isHomeLayoutEditing}
|
||||
layout={homeLayout}
|
||||
onLayoutChange={onHomeLayoutChange}
|
||||
onOpen={() => onNavigate('anniversary')}
|
||||
/>
|
||||
</HomeLayoutBox>
|
||||
|
||||
<div aria-hidden="true" className="pointer-events-none absolute bottom-0 left-0 z-[5] h-[13%] w-full select-none overflow-hidden">
|
||||
@@ -823,21 +823,21 @@ function HomeLayoutBox({
|
||||
)
|
||||
}
|
||||
|
||||
function HomeAnniversaryAssetButton({
|
||||
image,
|
||||
isEditing,
|
||||
layout,
|
||||
onLayoutChange,
|
||||
onOpen,
|
||||
reminder,
|
||||
}: {
|
||||
image: string
|
||||
isEditing: boolean
|
||||
layout: HomeLayout
|
||||
onLayoutChange: (elementId: HomeLayoutElementId, rect: HomeLayoutRect) => void
|
||||
onOpen: () => void
|
||||
reminder: HomeAnniversaryReminder
|
||||
}) {
|
||||
function HomeAnniversaryAssetButton({
|
||||
image,
|
||||
isEditing,
|
||||
layout,
|
||||
onLayoutChange,
|
||||
onOpen,
|
||||
reminder,
|
||||
}: {
|
||||
image: string
|
||||
isEditing: boolean
|
||||
layout: HomeLayout
|
||||
onLayoutChange: (elementId: HomeLayoutElementId, rect: HomeLayoutRect) => void
|
||||
onOpen: () => void
|
||||
reminder: HomeAnniversaryReminder
|
||||
}) {
|
||||
const frameRef = useRef<HTMLDivElement>(null)
|
||||
const [hotspotSize, setHotspotSize] = useState<{ height: number; width: number } | null>(null)
|
||||
|
||||
@@ -850,10 +850,10 @@ function HomeAnniversaryAssetButton({
|
||||
function updateHotspotSize() {
|
||||
if (!frame) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = frame.getBoundingClientRect()
|
||||
const imageRatio = homeAnniversaryAssetRatio
|
||||
}
|
||||
|
||||
const rect = frame.getBoundingClientRect()
|
||||
const imageRatio = homeAnniversaryAssetRatio
|
||||
const frameRatio = rect.width / rect.height
|
||||
|
||||
if (!Number.isFinite(frameRatio) || rect.width <= 0 || rect.height <= 0) {
|
||||
@@ -878,7 +878,7 @@ function HomeAnniversaryAssetButton({
|
||||
observer.observe(frame)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none relative h-full w-full" ref={frameRef}>
|
||||
@@ -903,166 +903,166 @@ function HomeAnniversaryAssetButton({
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none block h-full w-full select-none object-fill drop-shadow-[0_10px_24px_rgba(3,12,35,0.24)]"
|
||||
draggable={false}
|
||||
src={image}
|
||||
/>
|
||||
<HomeAnniversaryTextBox
|
||||
align="left"
|
||||
elementId="anniversaryTitle"
|
||||
isEditing={isEditing}
|
||||
layout={layout}
|
||||
onChange={onLayoutChange}
|
||||
>
|
||||
<span className="block max-h-full w-full max-w-full overflow-hidden text-left text-[10px] font-semibold leading-[1.45] tracking-[0.12em] text-white drop-shadow-[0_2px_8px_rgba(2,10,32,0.82)] [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:2]">
|
||||
{reminder.title}
|
||||
</span>
|
||||
</HomeAnniversaryTextBox>
|
||||
<HomeAnniversaryTextBox
|
||||
elementId="anniversaryStatus"
|
||||
isEditing={isEditing}
|
||||
layout={layout}
|
||||
onChange={onLayoutChange}
|
||||
>
|
||||
<span className="block max-w-full truncate text-[7px] font-semibold leading-none tracking-[0.18em] text-white drop-shadow-[0_2px_7px_rgba(2,10,32,0.85)]">
|
||||
{reminder.statusLabel}
|
||||
</span>
|
||||
</HomeAnniversaryTextBox>
|
||||
<HomeAnniversaryTextBox
|
||||
className="[font-family:'Source_Han_Serif_SC','Noto_Serif_CJK_SC','思源宋体',serif]"
|
||||
elementId="anniversaryDays"
|
||||
isEditing={isEditing}
|
||||
layout={layout}
|
||||
onChange={onLayoutChange}
|
||||
>
|
||||
<span className="block max-w-full truncate bg-[linear-gradient(180deg,#f7fbff_0%,#d8ecff_42%,#8bbdff_100%)] bg-clip-text text-[20px] font-normal leading-none text-transparent drop-shadow-[0_4px_12px_rgba(2,10,32,0.82)]">
|
||||
{reminder.days.toLocaleString('zh-CN')}
|
||||
</span>
|
||||
</HomeAnniversaryTextBox>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeAnniversaryTextBox({
|
||||
align = 'center',
|
||||
children,
|
||||
className = "[font-family:'Source_Han_Serif_SC','Noto_Serif_CJK_SC','思源宋体',serif]",
|
||||
elementId,
|
||||
isEditing,
|
||||
layout,
|
||||
onChange,
|
||||
}: {
|
||||
align?: 'center' | 'left'
|
||||
children: ReactNode
|
||||
className?: string
|
||||
elementId: Extract<HomeLayoutElementId, 'anniversaryTitle' | 'anniversaryStatus' | 'anniversaryDays'>
|
||||
isEditing: boolean
|
||||
layout: HomeLayout
|
||||
onChange: (elementId: HomeLayoutElementId, rect: HomeLayoutRect) => void
|
||||
}) {
|
||||
const rect = layout[elementId] ?? defaultHomeLayout[elementId]
|
||||
|
||||
function handlePointerDown(event: ReactPointerEvent<HTMLSpanElement>) {
|
||||
if (!isEditing) {
|
||||
return
|
||||
}
|
||||
|
||||
const stage = event.currentTarget.parentElement
|
||||
if (!stage) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const stageRect = stage.getBoundingClientRect()
|
||||
const startX = event.clientX
|
||||
const startY = event.clientY
|
||||
const startLeft = rect.left
|
||||
const startTop = rect.top
|
||||
const target = event.currentTarget
|
||||
target.setPointerCapture(event.pointerId)
|
||||
|
||||
function handlePointerMove(moveEvent: PointerEvent) {
|
||||
const nextLeft = startLeft + ((moveEvent.clientX - startX) / stageRect.width) * 100
|
||||
const nextTop = startTop + ((moveEvent.clientY - startY) / stageRect.height) * 100
|
||||
|
||||
onChange(elementId, {
|
||||
...rect,
|
||||
left: roundToTenth(nextLeft),
|
||||
top: roundToTenth(nextTop),
|
||||
})
|
||||
}
|
||||
|
||||
function handlePointerUp() {
|
||||
window.removeEventListener('pointermove', handlePointerMove)
|
||||
window.removeEventListener('pointerup', handlePointerUp)
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove)
|
||||
window.addEventListener('pointerup', handlePointerUp)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`absolute z-10 flex items-center justify-center overflow-visible text-center ${
|
||||
isEditing
|
||||
? 'pointer-events-auto cursor-move touch-none outline outline-1 outline-cyan-200/90'
|
||||
: 'pointer-events-none'
|
||||
} ${align === 'left' ? 'justify-start text-left' : ''} ${className}`}
|
||||
onPointerDown={handlePointerDown}
|
||||
style={{
|
||||
height: `${rect.height}%`,
|
||||
left: `${rect.left}%`,
|
||||
top: `${rect.top}%`,
|
||||
width: `${rect.width}%`,
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<span className="pointer-events-none absolute -top-4 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-sky-950/80 px-1.5 py-0.5 text-[7px] font-semibold leading-none text-cyan-50">
|
||||
{`${rect.left.toFixed(1)}, ${rect.top.toFixed(1)} / ${rect.width.toFixed(1)}x${rect.height.toFixed(1)}`}
|
||||
</span>
|
||||
) : null}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function getHomeAnniversaryReminder(anniversaries: Anniversary[]): HomeAnniversaryReminder {
|
||||
const upcomingAnniversary = anniversaries
|
||||
.filter((item) => item.repeatYearly && item.reminderEnabled)
|
||||
.map((item) => ({
|
||||
anniversary: item,
|
||||
days: getDaysLeft(item),
|
||||
}))
|
||||
.filter((item) => item.days <= 9)
|
||||
.sort((a, b) => a.days - b.days)[0]
|
||||
|
||||
if (upcomingAnniversary) {
|
||||
return {
|
||||
days: upcomingAnniversary.days,
|
||||
statusLabel: '还剩',
|
||||
title: upcomingAnniversary.anniversary.title,
|
||||
}
|
||||
}
|
||||
|
||||
const joinDay = anniversaries.find((item) => item.type === 'join')
|
||||
if (joinDay) {
|
||||
return {
|
||||
days: getElapsedDays(joinDay.date),
|
||||
statusLabel: '已经',
|
||||
title: '加入 DL.L9 粉丝团',
|
||||
}
|
||||
}
|
||||
|
||||
return defaultHomeAnniversaryReminder
|
||||
}
|
||||
|
||||
function getResolvedHomeBackgroundMode(birthday: string | undefined, mode: HomeBackgroundMode) {
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none block h-full w-full select-none object-fill drop-shadow-[0_10px_24px_rgba(3,12,35,0.24)]"
|
||||
draggable={false}
|
||||
src={image}
|
||||
/>
|
||||
<HomeAnniversaryTextBox
|
||||
align="left"
|
||||
elementId="anniversaryTitle"
|
||||
isEditing={isEditing}
|
||||
layout={layout}
|
||||
onChange={onLayoutChange}
|
||||
>
|
||||
<span className="block max-h-full w-full max-w-full overflow-hidden text-left text-[10px] font-semibold leading-[1.45] tracking-[0.12em] text-white drop-shadow-[0_2px_8px_rgba(2,10,32,0.82)] [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:2]">
|
||||
{reminder.title}
|
||||
</span>
|
||||
</HomeAnniversaryTextBox>
|
||||
<HomeAnniversaryTextBox
|
||||
elementId="anniversaryStatus"
|
||||
isEditing={isEditing}
|
||||
layout={layout}
|
||||
onChange={onLayoutChange}
|
||||
>
|
||||
<span className="block max-w-full truncate text-[7px] font-semibold leading-none tracking-[0.18em] text-white drop-shadow-[0_2px_7px_rgba(2,10,32,0.85)]">
|
||||
{reminder.statusLabel}
|
||||
</span>
|
||||
</HomeAnniversaryTextBox>
|
||||
<HomeAnniversaryTextBox
|
||||
className="[font-family:'Source_Han_Serif_SC','Noto_Serif_CJK_SC','思源宋体',serif]"
|
||||
elementId="anniversaryDays"
|
||||
isEditing={isEditing}
|
||||
layout={layout}
|
||||
onChange={onLayoutChange}
|
||||
>
|
||||
<span className="block max-w-full truncate bg-[linear-gradient(180deg,#f7fbff_0%,#d8ecff_42%,#8bbdff_100%)] bg-clip-text text-[20px] font-normal leading-none text-transparent drop-shadow-[0_4px_12px_rgba(2,10,32,0.82)]">
|
||||
{reminder.days.toLocaleString('zh-CN')}
|
||||
</span>
|
||||
</HomeAnniversaryTextBox>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeAnniversaryTextBox({
|
||||
align = 'center',
|
||||
children,
|
||||
className = "[font-family:'Source_Han_Serif_SC','Noto_Serif_CJK_SC','思源宋体',serif]",
|
||||
elementId,
|
||||
isEditing,
|
||||
layout,
|
||||
onChange,
|
||||
}: {
|
||||
align?: 'center' | 'left'
|
||||
children: ReactNode
|
||||
className?: string
|
||||
elementId: Extract<HomeLayoutElementId, 'anniversaryTitle' | 'anniversaryStatus' | 'anniversaryDays'>
|
||||
isEditing: boolean
|
||||
layout: HomeLayout
|
||||
onChange: (elementId: HomeLayoutElementId, rect: HomeLayoutRect) => void
|
||||
}) {
|
||||
const rect = layout[elementId] ?? defaultHomeLayout[elementId]
|
||||
|
||||
function handlePointerDown(event: ReactPointerEvent<HTMLSpanElement>) {
|
||||
if (!isEditing) {
|
||||
return
|
||||
}
|
||||
|
||||
const stage = event.currentTarget.parentElement
|
||||
if (!stage) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const stageRect = stage.getBoundingClientRect()
|
||||
const startX = event.clientX
|
||||
const startY = event.clientY
|
||||
const startLeft = rect.left
|
||||
const startTop = rect.top
|
||||
const target = event.currentTarget
|
||||
target.setPointerCapture(event.pointerId)
|
||||
|
||||
function handlePointerMove(moveEvent: PointerEvent) {
|
||||
const nextLeft = startLeft + ((moveEvent.clientX - startX) / stageRect.width) * 100
|
||||
const nextTop = startTop + ((moveEvent.clientY - startY) / stageRect.height) * 100
|
||||
|
||||
onChange(elementId, {
|
||||
...rect,
|
||||
left: roundToTenth(nextLeft),
|
||||
top: roundToTenth(nextTop),
|
||||
})
|
||||
}
|
||||
|
||||
function handlePointerUp() {
|
||||
window.removeEventListener('pointermove', handlePointerMove)
|
||||
window.removeEventListener('pointerup', handlePointerUp)
|
||||
}
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove)
|
||||
window.addEventListener('pointerup', handlePointerUp)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`absolute z-10 flex items-center justify-center overflow-visible text-center ${
|
||||
isEditing
|
||||
? 'pointer-events-auto cursor-move touch-none outline outline-1 outline-cyan-200/90'
|
||||
: 'pointer-events-none'
|
||||
} ${align === 'left' ? 'justify-start text-left' : ''} ${className}`}
|
||||
onPointerDown={handlePointerDown}
|
||||
style={{
|
||||
height: `${rect.height}%`,
|
||||
left: `${rect.left}%`,
|
||||
top: `${rect.top}%`,
|
||||
width: `${rect.width}%`,
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<span className="pointer-events-none absolute -top-4 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-sky-950/80 px-1.5 py-0.5 text-[7px] font-semibold leading-none text-cyan-50">
|
||||
{`${rect.left.toFixed(1)}, ${rect.top.toFixed(1)} / ${rect.width.toFixed(1)}x${rect.height.toFixed(1)}`}
|
||||
</span>
|
||||
) : null}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function getHomeAnniversaryReminder(anniversaries: Anniversary[]): HomeAnniversaryReminder {
|
||||
const upcomingAnniversary = anniversaries
|
||||
.filter((item) => item.repeatYearly && item.reminderEnabled)
|
||||
.map((item) => ({
|
||||
anniversary: item,
|
||||
days: getDaysLeft(item),
|
||||
}))
|
||||
.filter((item) => item.days <= 9)
|
||||
.sort((a, b) => a.days - b.days)[0]
|
||||
|
||||
if (upcomingAnniversary) {
|
||||
return {
|
||||
days: upcomingAnniversary.days,
|
||||
statusLabel: '还剩',
|
||||
title: upcomingAnniversary.anniversary.title,
|
||||
}
|
||||
}
|
||||
|
||||
const joinDay = anniversaries.find((item) => item.type === 'join')
|
||||
if (joinDay) {
|
||||
return {
|
||||
days: getElapsedDays(joinDay.date),
|
||||
statusLabel: '已经',
|
||||
title: '加入 DL.L9 粉丝团',
|
||||
}
|
||||
}
|
||||
|
||||
return defaultHomeAnniversaryReminder
|
||||
}
|
||||
|
||||
function getResolvedHomeBackgroundMode(birthday: string | undefined, mode: HomeBackgroundMode) {
|
||||
if (mode !== 'auto') {
|
||||
return mode
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user