继续沉淀工具信息弹窗与个人中心内容骨架
新增PlatformUtilityInfoModal统一工具信息弹窗白底骨架 收口profile副弹层的摘要头列表骨架与内容行 同步更新PlatformUiKit收口计划与共享决策记录
This commit is contained in:
63
src/components/common/PlatformProfileContentRow.tsx
Normal file
63
src/components/common/PlatformProfileContentRow.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PlatformSubpanel } from './PlatformSubpanel';
|
||||
|
||||
type PlatformProfileContentRowProps = {
|
||||
as?: 'div' | 'button';
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
surface?: 'platform' | 'flat' | 'soft';
|
||||
radius?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
padding?: 'none' | 'row' | 'xs' | 'sm' | 'md' | 'lg' | 'tight';
|
||||
interactive?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 个人中心白底 modal 内容行骨架。
|
||||
* 只承接常见的 row 外壳与可点击语义,具体字段布局仍留在业务 modal 内部。
|
||||
*/
|
||||
export function PlatformProfileContentRow({
|
||||
as = 'div',
|
||||
children,
|
||||
className,
|
||||
surface = 'flat',
|
||||
radius = 'xs',
|
||||
padding = 'md',
|
||||
interactive = as === 'button',
|
||||
disabled,
|
||||
type = 'button',
|
||||
onClick,
|
||||
}: PlatformProfileContentRowProps) {
|
||||
if (as === 'button') {
|
||||
return (
|
||||
<PlatformSubpanel
|
||||
as="button"
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
interactive={interactive}
|
||||
surface={surface}
|
||||
radius={radius}
|
||||
padding={padding}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface={surface}
|
||||
radius={radius}
|
||||
padding={padding}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { PlatformProfileContentRow } from './PlatformProfileContentRow';
|
||||
import { PlatformProfileSkeletonList } from './PlatformProfileSkeletonList';
|
||||
import { PlatformProfileSummaryHeader } from './PlatformProfileSummaryHeader';
|
||||
|
||||
test('platform profile summary header renders kicker, title and badge slot', () => {
|
||||
render(
|
||||
<PlatformProfileSummaryHeader
|
||||
kicker="PLAYED"
|
||||
title="玩过"
|
||||
badge={<span>1.5小时</span>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('PLAYED')).toBeTruthy();
|
||||
expect(screen.getByText('玩过')).toBeTruthy();
|
||||
expect(screen.getByText('1.5小时')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('platform profile skeleton list renders requested skeleton rows', () => {
|
||||
const { container } = render(
|
||||
<PlatformProfileSkeletonList
|
||||
count={3}
|
||||
containerClassName="space-y-3"
|
||||
itemClassName="h-16"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll('.animate-pulse')).toHaveLength(3);
|
||||
expect(container.querySelector('.space-y-3')).toBeTruthy();
|
||||
expect(container.querySelector('.h-16')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('platform profile content row preserves interactive button semantics', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformProfileContentRow as="button" onClick={onClick}>
|
||||
点击行
|
||||
</PlatformProfileContentRow>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '点击行' }));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
31
src/components/common/PlatformProfileSkeletonList.tsx
Normal file
31
src/components/common/PlatformProfileSkeletonList.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
type PlatformProfileSkeletonListProps = {
|
||||
count: number;
|
||||
containerClassName?: string;
|
||||
itemClassName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 个人中心白底 modal 列表读取骨架。
|
||||
* 只负责重复 skeleton 行与容器节奏,行高和栅格继续由调用方微调。
|
||||
*/
|
||||
export function PlatformProfileSkeletonList({
|
||||
count,
|
||||
containerClassName,
|
||||
itemClassName,
|
||||
}: PlatformProfileSkeletonListProps) {
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={[
|
||||
'animate-pulse rounded-xl bg-zinc-100',
|
||||
itemClassName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/components/common/PlatformProfileSummaryHeader.tsx
Normal file
39
src/components/common/PlatformProfileSummaryHeader.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type PlatformProfileSummaryHeaderProps = {
|
||||
kicker: ReactNode;
|
||||
title: ReactNode;
|
||||
badge?: ReactNode;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
badgeClassName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 个人中心白底副弹层标题摘要骨架。
|
||||
* 只收口 kicker、标题和摘要 badge 的层次,不承接业务数值拼装。
|
||||
*/
|
||||
export function PlatformProfileSummaryHeader({
|
||||
kicker,
|
||||
title,
|
||||
badge,
|
||||
className,
|
||||
titleClassName,
|
||||
badgeClassName,
|
||||
}: PlatformProfileSummaryHeaderProps) {
|
||||
return (
|
||||
<div className={['pr-10', className].filter(Boolean).join(' ')}>
|
||||
<div className="text-[10px] font-black tracking-[0.22em] text-[#ff4056]">
|
||||
{kicker}
|
||||
</div>
|
||||
<div className={['mt-1 text-2xl font-black', titleClassName].filter(Boolean).join(' ')}>
|
||||
{title}
|
||||
</div>
|
||||
{badge ? (
|
||||
<div className={['mt-3', badgeClassName].filter(Boolean).join(' ')}>
|
||||
{badge}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { CopyFeedbackButton } from './CopyFeedbackButton';
|
||||
import { PlatformInfoBlock } from './PlatformInfoBlock';
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
|
||||
import { useCopyFeedback } from './useCopyFeedback';
|
||||
|
||||
export type PlatformReportDialogField = {
|
||||
@@ -31,8 +31,8 @@ export function PlatformReportDialog({
|
||||
onClose,
|
||||
copyIdleLabel,
|
||||
fields,
|
||||
overlayClassName = 'platform-theme platform-theme--light !items-center',
|
||||
panelClassName = 'platform-remap-surface rounded-[1.5rem]',
|
||||
overlayClassName,
|
||||
panelClassName = 'rounded-[1.5rem]',
|
||||
}: PlatformReportDialogProps) {
|
||||
const { copyState, copyText, resetCopyState } = useCopyFeedback();
|
||||
const reportText = useMemo(() => buildPlatformReportText(fields), [fields]);
|
||||
@@ -50,14 +50,13 @@ export function PlatformReportDialog({
|
||||
};
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
<PlatformUtilityInfoModal
|
||||
open={open}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
overlayClassName={overlayClassName}
|
||||
panelClassName={panelClassName}
|
||||
bodyClassName="space-y-3 px-4 py-4 sm:px-5 sm:py-5"
|
||||
bodyClassName="space-y-3"
|
||||
footerClassName="justify-end px-4 py-4 sm:px-5"
|
||||
footer={
|
||||
<CopyFeedbackButton
|
||||
@@ -82,6 +81,6 @@ export function PlatformReportDialog({
|
||||
</PlatformInfoBlock>
|
||||
))
|
||||
: null}
|
||||
</UnifiedModal>
|
||||
</PlatformUtilityInfoModal>
|
||||
);
|
||||
}
|
||||
|
||||
51
src/components/common/PlatformUtilityInfoModal.test.tsx
Normal file
51
src/components/common/PlatformUtilityInfoModal.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
|
||||
|
||||
test('renders platform utility info modal shell with default platform styling', () => {
|
||||
render(
|
||||
<PlatformUtilityInfoModal
|
||||
open
|
||||
title="工具信息"
|
||||
onClose={() => {}}
|
||||
footer={<button type="button">知道了</button>}
|
||||
panelClassName="rounded-[1.5rem]"
|
||||
>
|
||||
<div>这里是正文</div>
|
||||
</PlatformUtilityInfoModal>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '工具信息' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(dialog.parentElement?.className).toContain('!items-center');
|
||||
expect(dialog.className).toContain('platform-remap-surface');
|
||||
expect(dialog.className).toContain('rounded-[1.5rem]');
|
||||
expect(within(dialog).getByText('这里是正文')).toBeTruthy();
|
||||
expect(within(dialog).getByRole('button', { name: '知道了' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('supports custom theme and spacing overrides', () => {
|
||||
render(
|
||||
<PlatformUtilityInfoModal
|
||||
open
|
||||
title="工具信息"
|
||||
onClose={() => {}}
|
||||
platformTheme="dark"
|
||||
bodyClassName="space-y-3"
|
||||
footerClassName="justify-center pt-0"
|
||||
footer={<button type="button">继续</button>}
|
||||
>
|
||||
<div>这里是正文</div>
|
||||
</PlatformUtilityInfoModal>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '工具信息' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--dark');
|
||||
expect(dialog.querySelector('.space-y-3')).toBeTruthy();
|
||||
expect(dialog.querySelector('.justify-center')).toBeTruthy();
|
||||
expect(dialog.querySelector('.pt-0')).toBeTruthy();
|
||||
expect(within(dialog).getByRole('button', { name: '继续' })).toBeTruthy();
|
||||
});
|
||||
63
src/components/common/PlatformUtilityInfoModal.tsx
Normal file
63
src/components/common/PlatformUtilityInfoModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
|
||||
type PlatformUtilityInfoModalProps = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
platformTheme?: 'light' | 'dark';
|
||||
overlayClassName?: string;
|
||||
panelClassName?: string;
|
||||
bodyClassName?: string;
|
||||
footerClassName?: string;
|
||||
};
|
||||
|
||||
function joinClassNames(...classNames: Array<string | null | undefined | false>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具信息类弹窗统一走平台主题 overlay + 白底 panel 壳层。
|
||||
* 这里只收口通用窗口骨架,字段内容、分享渠道和复制按钮仍由业务组件自己渲染。
|
||||
*/
|
||||
export function PlatformUtilityInfoModal({
|
||||
open,
|
||||
title,
|
||||
onClose,
|
||||
children,
|
||||
footer,
|
||||
platformTheme = 'light',
|
||||
overlayClassName,
|
||||
panelClassName,
|
||||
bodyClassName,
|
||||
footerClassName,
|
||||
}: PlatformUtilityInfoModalProps) {
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={open}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
overlayClassName={joinClassNames(
|
||||
`platform-theme platform-theme--${platformTheme}`,
|
||||
'!items-center',
|
||||
overlayClassName,
|
||||
)}
|
||||
panelClassName={joinClassNames('platform-remap-surface', panelClassName)}
|
||||
bodyClassName={joinClassNames(
|
||||
'space-y-4 px-4 py-4 sm:px-5 sm:py-5',
|
||||
bodyClassName,
|
||||
)}
|
||||
footer={footer}
|
||||
footerClassName={joinClassNames(
|
||||
'px-4 py-4 sm:px-5',
|
||||
footerClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { CopyFeedbackButton } from './CopyFeedbackButton';
|
||||
import { PlatformInfoBlock } from './PlatformInfoBlock';
|
||||
import { PlatformSubpanel } from './PlatformSubpanel';
|
||||
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
|
||||
import {
|
||||
buildPublishShareText,
|
||||
type PublishShareModalPayload,
|
||||
} from './publishShareModalModel';
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
import { useCopyFeedback } from './useCopyFeedback';
|
||||
|
||||
type PublishShareModalProps = {
|
||||
@@ -115,14 +115,12 @@ export function PublishShareModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
<PlatformUtilityInfoModal
|
||||
open={open && Boolean(payload)}
|
||||
title="分享给朋友"
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
overlayClassName={`platform-theme platform-theme--${platformTheme} !items-center`}
|
||||
panelClassName="platform-remap-surface rounded-[1.75rem]"
|
||||
bodyClassName="space-y-4 px-4 py-4 sm:px-5 sm:py-5"
|
||||
platformTheme={platformTheme}
|
||||
panelClassName="rounded-[1.75rem]"
|
||||
footerClassName="justify-center border-t-0 px-4 pb-5 pt-0 sm:px-5"
|
||||
footer={
|
||||
<div className="grid w-full grid-cols-3 gap-3">
|
||||
@@ -168,6 +166,6 @@ export function PublishShareModal({
|
||||
actionFullWidth
|
||||
className="disabled:opacity-55"
|
||||
/>
|
||||
</UnifiedModal>
|
||||
</PlatformUtilityInfoModal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user