继续扩展共享异步状态壳

将 PlatformAsyncStatePanel 扩展到公共素材选择网格

将 PlatformAsyncStatePanel 扩展到视觉小说存档面板与账号安全子区块

将兑换码弹窗提交动作改为使用标准 profile modal footer

补充对应测试并更新 PlatformUiKit 收口计划与共享决策记录
This commit is contained in:
2026-06-11 04:11:32 +08:00
parent 7431b1b9a4
commit 7f8400fd3a
10 changed files with 220 additions and 115 deletions

View File

@@ -187,6 +187,26 @@ test('renders selectable asset grid cards with shared error chrome', () => {
);
});
test('asset grid keeps error banner while loading state remains mutually exclusive with empty state', () => {
render(
<PlatformAssetPickerGrid
items={[]}
isLoading
error="历史素材读取失败。"
loadingLabel="读取中..."
emptyLabel="暂无历史素材"
getKey={(item: { id: string }) => item.id}
getImageSrc={(item) => item.id}
getImageAlt={() => ''}
onSelect={() => {}}
/>,
);
expect(screen.getByText('历史素材读取失败。')).toBeTruthy();
expect(screen.getByText('读取中...')).toBeTruthy();
expect(screen.queryByText('暂无历史素材')).toBeNull();
});
test('supports dark editor surface with an in-card select affordance', () => {
const onSelect = vi.fn();

View File

@@ -1,6 +1,7 @@
import type { ButtonHTMLAttributes, Key, ReactNode } from 'react';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { PlatformAsyncStatePanel } from './PlatformAsyncStatePanel';
import { PlatformEmptyState } from './PlatformEmptyState';
import { PlatformStatusMessage } from './PlatformStatusMessage';
@@ -250,6 +251,13 @@ export function PlatformAssetPickerGrid<TItem>({
imageClassName,
bodyClassName,
}: PlatformAssetPickerGridProps<TItem>) {
const sharedEmptyStateClassName = [
PLATFORM_ASSET_PICKER_GRID_EMPTY_CLASS[surface],
emptyClassName,
]
.filter(Boolean)
.join(' ');
return (
<>
{error ? (
@@ -262,61 +270,53 @@ export function PlatformAssetPickerGrid<TItem>({
{error}
</PlatformStatusMessage>
) : null}
{isLoading ? (
<PlatformEmptyState
surface="dashed"
size="panel"
className={[
PLATFORM_ASSET_PICKER_GRID_EMPTY_CLASS[surface],
emptyClassName,
]
.filter(Boolean)
.join(' ')}
>
{loadingLabel}
</PlatformEmptyState>
) : null}
{!isLoading && !error && items.length <= 0 ? (
<PlatformEmptyState
surface="dashed"
size="panel"
className={[
PLATFORM_ASSET_PICKER_GRID_EMPTY_CLASS[surface],
emptyClassName,
]
.filter(Boolean)
.join(' ')}
>
{emptyLabel}
</PlatformEmptyState>
) : null}
{!isLoading && items.length > 0 ? (
<div className={gridClassName}>
{items.map((item) => (
<PlatformAssetPickerCard
key={getKey(item)}
disabled={disabled}
aria-label={getAriaLabel?.(item)}
onClick={() => onSelect(item)}
imageSrc={getImageSrc(item)}
imageAlt={getImageAlt(item)}
assetTitle={getTitle?.(item)}
subtitle={getSubtitle?.(item)}
surface={surface}
selectLabel={selectLabel}
selected={isSelected?.(item) ?? false}
className={cardClassName}
cardRadiusClassName={cardRadiusClassName}
imageShellClassName={imageShellClassName}
imageClassName={imageClassName}
bodyClassName={bodyClassName}
/>
))}
</div>
) : null}
<PlatformAsyncStatePanel
isLoading={isLoading}
loadingState={
<PlatformEmptyState
surface="dashed"
size="panel"
className={sharedEmptyStateClassName}
>
{loadingLabel}
</PlatformEmptyState>
}
isEmpty={items.length <= 0}
emptyState={
<PlatformEmptyState
surface="dashed"
size="panel"
className={sharedEmptyStateClassName}
>
{emptyLabel}
</PlatformEmptyState>
}
>
{items.length > 0 ? (
<div className={gridClassName}>
{items.map((item) => (
<PlatformAssetPickerCard
key={getKey(item)}
disabled={disabled}
aria-label={getAriaLabel?.(item)}
onClick={() => onSelect(item)}
imageSrc={getImageSrc(item)}
imageAlt={getImageAlt(item)}
assetTitle={getTitle?.(item)}
subtitle={getSubtitle?.(item)}
surface={surface}
selectLabel={selectLabel}
selected={isSelected?.(item) ?? false}
className={cardClassName}
cardRadiusClassName={cardRadiusClassName}
imageShellClassName={imageShellClassName}
imageClassName={imageClassName}
bodyClassName={bodyClassName}
/>
))}
</div>
) : null}
</PlatformAsyncStatePanel>
</>
);
}