diff --git a/frontend/index.html b/frontend/index.html index 59c6a4a6..e6603f32 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - + { const accountMenu = await screen.findByRole('button', { name: '打开账号菜单' }) expect(accountMenu.textContent).toContain('reader@example.com') - expect(within(accountMenu).getByTestId('default-account-avatar').getAttribute('src')).toBe( - '/windup-mark.svg', - ) + expect( + within(accountMenu).getByTestId('default-account-avatar').getAttribute('aria-hidden'), + ).toBe('true') }) it('账号菜单在点击外部或按 Escape 时关闭', async () => { diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index dfe556d2..e6e9a93e 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { useEffect, useRef, useState } from 'react' import { Link, useLocation, useNavigate } from 'react-router' @@ -227,7 +228,7 @@ export function AppHeader({
@@ -240,7 +241,7 @@ export function AppHeader({ wave.entry === 'brand' ? 'app-header-text-wave' : '' }`} > - + @@ -430,10 +431,9 @@ export function AppHeader({ accountMenu.expanded ? 'scale-110' : 'scale-100' }`} > - @@ -550,7 +550,7 @@ function MarketingHeaderView({ session, wave, onWave }: MarketingHeaderViewProps wave.entry === 'brand' ? 'app-header-text-wave' : '' }`} > - + diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 083ee886..b1a40bc4 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { useEffect, useId, @@ -668,7 +669,7 @@ function AccountPanelDialog({ className="auth-screen-brand fixed z-[2] flex items-center gap-[0.7rem]" aria-label="Windup" > - + Windup
diff --git a/frontend/src/pages/account/index.test.tsx b/frontend/src/pages/account/index.test.tsx index 3dc1d755..995d4109 100644 --- a/frontend/src/pages/account/index.test.tsx +++ b/frontend/src/pages/account/index.test.tsx @@ -137,8 +137,8 @@ describe('AccountPage', () => { ) expect(screen.getByRole('heading', { name: '个人资料' })).toBeTruthy() expect(screen.getByLabelText('昵称').getAttribute('placeholder')).toBe('输入昵称') - expect(screen.getByTestId('account-profile-default-avatar').getAttribute('src')).toBe( - '/windup-mark.svg', + expect(screen.getByTestId('account-profile-default-avatar').getAttribute('aria-hidden')).toBe( + 'true', ) expect(screen.queryByLabelText('邮箱验证码')).toBeNull() diff --git a/frontend/src/pages/account/index.tsx b/frontend/src/pages/account/index.tsx index e1a3a5ee..1a50baff 100644 --- a/frontend/src/pages/account/index.tsx +++ b/frontend/src/pages/account/index.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { Gift, X } from '@phosphor-icons/react' import { useEffect, @@ -734,10 +735,9 @@ export function AccountPage() {
- diff --git a/frontend/src/pages/landing/index.tsx b/frontend/src/pages/landing/index.tsx index 15e88e73..8d9cc095 100644 --- a/frontend/src/pages/landing/index.tsx +++ b/frontend/src/pages/landing/index.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { useEffect, useRef, useState } from 'react' import type { CSSProperties } from 'react' import { Link } from 'react-router' @@ -749,7 +750,7 @@ export function LandingPage() { aria-label="返回 Windup 宣传页" className="inline-flex items-center gap-3 rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-app-accent" > - + Windup

diff --git a/frontend/src/shared/ui/asset-preview-card.test.tsx b/frontend/src/shared/ui/asset-preview-card.test.tsx index 6bda0c94..08dfe7c8 100644 --- a/frontend/src/shared/ui/asset-preview-card.test.tsx +++ b/frontend/src/shared/ui/asset-preview-card.test.tsx @@ -3,7 +3,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { MemoryRouter } from 'react-router' -import { AssetPreviewCard, AssetThumbnailImage } from './asset-preview-card' +import { AssetPreviewCard, AssetPreviewSurface, AssetThumbnailImage } from './asset-preview-card' afterEach(cleanup) @@ -203,3 +203,40 @@ describe('AssetThumbnailImage', () => { ) }) }) + +describe('unavailable preview images', () => { + it('replaces a failed source with a status and retries a different URL', () => { + const { rerender } = render( + , + ) + fireEvent.error(screen.getByRole('img', { name: '角色预览' })) + expect(screen.queryByRole('img')).toBeNull() + expect(screen.getByRole('status').textContent).toContain('无法加载') + rerender( + , + ) + expect(screen.getByRole('img').getAttribute('src')).toBe('https://storage.windup.test/new.png') + expect(screen.queryByRole('status')).toBeNull() + }) + + it('tries the original after thumbnail failure before showing unavailable', () => { + render( + , + ) + fireEvent.error(screen.getByRole('img')) + expect(screen.queryByRole('status')).toBeNull() + expect(screen.getByRole('img').getAttribute('src')).toBe( + 'https://media.windup.test/media/outfit-preview/old.source.png', + ) + fireEvent.error(screen.getByRole('img')) + expect(screen.queryByRole('img')).toBeNull() + expect(screen.getByRole('status')).toBeTruthy() + }) +}) diff --git a/frontend/src/shared/ui/asset-preview-card.tsx b/frontend/src/shared/ui/asset-preview-card.tsx index 77dedbd2..6d862300 100644 --- a/frontend/src/shared/ui/asset-preview-card.tsx +++ b/frontend/src/shared/ui/asset-preview-card.tsx @@ -73,14 +73,22 @@ export function AssetPreviewSurface({ className = '', thumbnail = false, }: AssetPreviewSurfaceProps) { + const [failedUrl, setFailedUrl] = useState(null) + const unavailable = !!previewUrl && failedUrl === previewUrl + const handleError: ImgHTMLAttributes['onError'] = (event) => { + // 缩略图失败仍要尝试原图;只有原图也失败才显示缺图状态。 + if (event.currentTarget.getAttribute('src') === previewUrl) setFailedUrl(previewUrl) + } + return (

- {previewUrl ? ( + {previewUrl && !unavailable ? ( thumbnail ? ( ) ) : ( -
+
)} diff --git a/frontend/src/shared/ui/windup-mark.tsx b/frontend/src/shared/ui/windup-mark.tsx new file mode 100644 index 00000000..7bbaff42 --- /dev/null +++ b/frontend/src/shared/ui/windup-mark.tsx @@ -0,0 +1,44 @@ +import { useId, type SVGProps } from 'react' + +/** 内联标志跟随文字颜色;独立遮罩避免同页多个实例互相引用。 */ +export function WindupMark(props: SVGProps) { + const maskId = useId() + return ( + + ) +}