Compare commits

...

2 Commits

Author SHA1 Message Date
admin 4312f376b8 chore(favicon): 使用预发布环境的图标
Docker Image / Quality and Bundle Budgets (push) Successful in 3s
Docker Image / Build and Push Docker Image (push) Successful in 1m52s
2026-07-21 15:36:01 +08:00
admin 0381090867 feat(tip): improve gift layout and background 2026-07-21 15:35:57 +08:00
6 changed files with 165 additions and 112 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 35 KiB

@@ -209,6 +209,9 @@ describe("TipScreen checkout", () => {
.querySelector("main") .querySelector("main")
?.getAttribute("style"), ?.getAttribute("style"),
).toContain('--tip-cover-image: url("/images/cover/maya.png")'); ).toContain('--tip-cover-image: url("/images/cover/maya.png")');
expect(
container.querySelectorAll('main > div[aria-hidden="true"]'),
).toHaveLength(3);
}); });
it.each([ it.each([
+72 -28
View File
@@ -14,42 +14,87 @@
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
background: background:
radial-gradient(circle at 88% 8%, rgba(255, 116, 151, 0.14), transparent 28%), radial-gradient(
linear-gradient(180deg, #fff2e8 0%, #fff9f4 58%, #ffffff 100%); circle at 18% 12%,
rgba(255, 181, 105, 0.34),
transparent 32%
),
radial-gradient(
circle at 86% 24%,
rgba(255, 85, 139, 0.2),
transparent 30%
),
linear-gradient(180deg, #fff2e8 0%, #fff8f2 46%, #ffffff 100%);
color: #25191b; color: #25191b;
isolation: isolate; isolation: isolate;
} }
.bgImage { .bgImage,
.bgGlowOne,
.bgGlowTwo {
position: absolute; position: absolute;
z-index: 0; z-index: 0;
pointer-events: none;
}
.bgImage {
inset: 0 0 auto; inset: 0 0 auto;
height: clamp(230px, 42vh, 330px); height: clamp(230px, 42vh, 330px);
background: background:
linear-gradient( linear-gradient(
180deg, 180deg,
rgba(255, 242, 232, 0.18) 0%, rgba(255, 242, 232, 0.3) 0%,
rgba(255, 246, 239, 0.5) 62%, rgba(255, 242, 232, 0.64) 64%,
#fff9f4 100% #fff2e8 96%
), ),
var(--tip-cover-image) center 20% / cover no-repeat; var(--tip-cover-image) center 18% / cover no-repeat;
filter: saturate(0.9); filter: saturate(0.95) blur(0.2px);
opacity: 0.48; opacity: 0.36;
pointer-events: none; }
.bgGlowOne,
.bgGlowTwo {
border-radius: 999px;
filter: blur(10px);
}
.bgGlowOne {
top: 192px;
right: -86px;
width: 190px;
height: 190px;
background: rgba(255, 92, 142, 0.16);
}
.bgGlowTwo {
bottom: 108px;
left: -98px;
width: 210px;
height: 210px;
background: rgba(255, 181, 104, 0.18);
} }
.header { .header {
position: relative; position: relative;
z-index: 1; z-index: 1;
display: grid; display: flex;
min-height: 46px; min-height: 46px;
grid-template-columns: 42px minmax(0, 1fr) 42px;
align-items: center; align-items: center;
gap: 8px; }
.contentFlow {
position: relative;
z-index: 1;
display: flex;
min-height: 0;
flex: 0 1 auto;
flex-direction: column;
margin-top: auto;
} }
.headerIdentity { .headerIdentity {
display: flex; display: flex;
width: 100%;
min-width: 0; min-width: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -82,12 +127,6 @@
white-space: nowrap; white-space: nowrap;
} }
.headerBalance {
display: block;
width: 42px;
height: 1px;
}
.supportPrompt { .supportPrompt {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -119,7 +158,7 @@
z-index: 1; z-index: 1;
display: flex; display: flex;
min-height: 0; min-height: 0;
flex: 1 1 auto; flex: 0 1 auto;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
margin-top: 12px; margin-top: 12px;
@@ -128,7 +167,7 @@
.productCard { .productCard {
display: grid; display: grid;
flex: 0 1 auto; flex: 0 1 auto;
grid-template-columns: clamp(98px, 28vw, 118px) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px; padding: 12px;
@@ -143,9 +182,10 @@
.coffeeStage { .coffeeStage {
position: relative; position: relative;
width: 100%; width: clamp(136px, 40vw, 176px);
aspect-ratio: 1; aspect-ratio: 1;
align-self: center; align-self: center;
justify-self: center;
overflow: hidden; overflow: hidden;
border-radius: 20px; border-radius: 20px;
background: #c59b7d; background: #c59b7d;
@@ -341,6 +381,7 @@
.productSkeleton { .productSkeleton {
min-height: 174px; min-height: 174px;
margin-top: 0;
} }
.skeletonImage, .skeletonImage,
@@ -356,8 +397,9 @@
} }
.skeletonImage { .skeletonImage {
width: 100%; width: clamp(136px, 40vw, 176px);
aspect-ratio: 1; aspect-ratio: 1;
justify-self: center;
border-radius: 20px; border-radius: 20px;
} }
@@ -378,7 +420,6 @@
.checkoutSlot { .checkoutSlot {
flex: 0 0 auto; flex: 0 0 auto;
margin-top: auto;
} }
.checkoutButton { .checkoutButton {
@@ -428,8 +469,7 @@
} }
.header { .header {
grid-template-columns: 40px minmax(0, 1fr) 40px; min-height: 40px;
gap: 5px;
} }
.headerIdentity { .headerIdentity {
@@ -442,7 +482,6 @@
} }
.productCard { .productCard {
grid-template-columns: 82px minmax(0, 1fr);
gap: 7px; gap: 7px;
padding: 9px; padding: 9px;
} }
@@ -497,10 +536,15 @@
} }
.productCard { .productCard {
grid-template-columns: 92px minmax(0, 1fr);
padding: 9px; padding: 9px;
} }
.coffeeStage,
.skeletonImage {
width: 128px;
justify-self: center;
}
.tierList { .tierList {
max-height: 148px; max-height: 148px;
gap: 5px; gap: 5px;
+90 -84
View File
@@ -210,6 +210,8 @@ export function TipScreen({
} }
> >
<div className={styles.bgImage} aria-hidden="true" /> <div className={styles.bgImage} aria-hidden="true" />
<div className={styles.bgGlowOne} aria-hidden="true" />
<div className={styles.bgGlowTwo} aria-hidden="true" />
<header className={styles.header}> <header className={styles.header}>
<BackButton <BackButton
@@ -218,6 +220,9 @@ export function TipScreen({
ariaLabel="Back to character home" ariaLabel="Back to character home"
analyticsKey="tip.back_to_splash" analyticsKey="tip.back_to_splash"
/> />
</header>
<div className={styles.contentFlow}>
<div className={styles.headerIdentity}> <div className={styles.headerIdentity}>
<div className={styles.headerAvatar}> <div className={styles.headerAvatar}>
<CharacterAvatar <CharacterAvatar
@@ -232,97 +237,98 @@ export function TipScreen({
{character.copy.tipTitle} {character.copy.tipTitle}
</h1> </h1>
</div> </div>
<span className={styles.headerBalance} aria-hidden="true" />
</header>
<p <p
className={`${styles.supportPrompt} ${ className={`${styles.supportPrompt} ${
supportPrompt.isReady ? styles.supportPromptReady : "" supportPrompt.isReady ? styles.supportPromptReady : ""
}`} }`}
> >
{supportPrompt.prompt} {supportPrompt.prompt}
</p> </p>
<div className={styles.purchaseFlow}> <div className={styles.purchaseFlow}>
{payment.isLoadingPlans ? ( {payment.isLoadingPlans ? (
<section
className={`${styles.productCard} ${styles.productSkeleton}`}
aria-label="Loading gifts"
aria-busy="true"
>
<div className={styles.skeletonImage} />
<div className={styles.skeletonList}>
<span />
<span />
<span />
</div>
</section>
) : selectedProduct ? (
<>
<section <section
className={styles.productCard} className={`${styles.productCard} ${styles.productSkeleton}`}
aria-label="Gift products" aria-label="Loading gifts"
aria-busy="true"
> >
<div className={styles.coffeeStage}> <div className={styles.skeletonImage} />
<TipProductImage <div className={styles.skeletonList}>
key={selectedProduct.planId} <span />
sources={getGiftImageSources( <span />
selectedProduct, <span />
selectedCategory, </div>
)} </section>
alt={selectedProduct.planName} ) : selectedProduct ? (
className={styles.coffeeImage} <>
priority <section
className={styles.productCard}
aria-label="Gift products"
>
<div className={styles.coffeeStage}>
<TipProductImage
key={selectedProduct.planId}
sources={getGiftImageSources(
selectedProduct,
selectedCategory,
)}
alt={selectedProduct.planName}
className={styles.coffeeImage}
priority
/>
</div>
<TipGiftProductSelector
disabled={isSelectionDisabled}
products={visibleProducts}
onChange={handleProductChange}
selectedPlanId={payment.selectedPlanId}
/>
</section>
<PaymentMethodSelector
config={paymentMethodConfig}
value={payment.payChannel}
density="compact"
disabled={isPaymentBusy}
className={styles.paymentMethodSlot}
analyticsKey="tip.payment_method"
onChange={handlePaymentMethodChange}
/>
<div className={styles.checkoutSlot}>
<TipCheckoutButton
giftCategory={payment.selectedGiftCategory}
giftPlanId={payment.selectedPlanId || null}
disabled={!canCreateOrder}
onOrder={handleOrder}
returnPath={returnPath}
/> />
</div> </div>
</>
<TipGiftProductSelector ) : showCatalogError || showEmptyCatalog ? (
disabled={isSelectionDisabled} <section className={styles.catalogStatus} role="status">
products={visibleProducts} <p>
onChange={handleProductChange} {showCatalogError
selectedPlanId={payment.selectedPlanId} ? "We could not load gifts for this character."
/> : "This character does not have any gifts available yet."}
</p>
{showCatalogError ? (
<button
type="button"
onClick={() =>
paymentDispatch({
type: "PaymentCatalogRetryRequested",
})
}
>
Try again
</button>
) : null}
</section> </section>
) : null}
<PaymentMethodSelector </div>
config={paymentMethodConfig}
value={payment.payChannel}
density="compact"
disabled={isPaymentBusy}
className={styles.paymentMethodSlot}
analyticsKey="tip.payment_method"
onChange={handlePaymentMethodChange}
/>
<div className={styles.checkoutSlot}>
<TipCheckoutButton
giftCategory={payment.selectedGiftCategory}
giftPlanId={payment.selectedPlanId || null}
disabled={!canCreateOrder}
onOrder={handleOrder}
returnPath={returnPath}
/>
</div>
</>
) : showCatalogError || showEmptyCatalog ? (
<section className={styles.catalogStatus} role="status">
<p>
{showCatalogError
? "We could not load gifts for this character."
: "This character does not have any gifts available yet."}
</p>
{showCatalogError ? (
<button
type="button"
onClick={() =>
paymentDispatch({ type: "PaymentCatalogRetryRequested" })
}
>
Try again
</button>
) : null}
</section>
) : null}
</div> </div>
</main> </main>
</MobileShell> </MobileShell>