feat(ui): 新增 EntryRow / EntryTile,收掉应用里两套手写内容卡

- EntryRow:头像 + 标题(可带角标)+ 单行描述,整行可点;
  点击语义内置(无修饰键接管,按住 Ctrl/Cmd/Shift/Alt 保留浏览器默认)
- EntryTile:上图下文白卡,由 Card padding=none hoverable + Cover 拼成
- 展示页 #cards 补两个示例与 props,COMPONENTS.md 补契约与 changelog

兼容性:纯新增;应用侧的旧类名(.app-card / .ces-card / .ces-body)随之删除
This commit is contained in:
2026-11-01 16:08:39 +08:00
parent 50056a7154
commit 17a09f22b6
7 changed files with 247 additions and 1 deletions
+3
View File
@@ -35,6 +35,8 @@
| `Ticker` | L3 | ✅ | 无限滚动的信息条 | `children` `speed` `paused` `label` |
| `Layout` | L3 | ✅ | 页面骨架:页头 / 侧栏 / 内容 / 页脚 | `header` `sider` `aside` `maxWidth` `gap` `height` |
| `Card` | L3 | ✅ | 白色容器(1px 极浅描边) | `as` `padding` `hoverable` |
| `EntryRow` | L2 | ✅ | 条目行:头像 + 标题 + 描述,整行可点 | `title` `description` `href` `avatar` `badge` `onOpen` |
| `EntryTile` | L3 | ✅ | 条目磁贴:上图下文白卡 | `title` `description` `cover` `ratio` `onOpen` |
| `Cover` | L3 | ✅ | 比例封面卡(2:3 / 4:3 / 16:9 + 标题遮罩) | `ratio` `src` `caption` `lift` `onClick` |
| `Image` | L3 | ✅ | 带比例 / 圆角 / 失败降级的图片 | `ratio` `fit` `radius` `fallback` |
| `FormField` | L3 | ✅ | 表单字段排版(标签 + 控件 + 提示/错误) | `label` `hint` `error` `required` |
@@ -767,3 +769,4 @@
| 2026-10-30 | **修复**:`PasswordInput` 的眼睛按钮移入输入框内部(`Input` 新增 `suffix` 插槽);`Input` 隐藏 Edge 原生 `::-ms-reveal` / `::-ms-clear`,避免与自带显隐按钮叠成两个眼睛。兼容性:`Input` 新增可选 prop,无破坏性;`PasswordInput` API 不变、仅外观变化 |
| 2026-10-31 | `Layout` 新增 `toolbar` 插槽:页头与页身之间的一条真实占位带(搜索 / 筛选 / 操作),宽度跟随容器(`contained` 时与侧栏 + 内容同宽),在 `height="screen"` 下不参与内部滚动。用它替代「搜索框 fixed + `contentOffset` 让位」的写法,页面不再需要用 JS 改 DOM 尺寸。兼容性:纯新增可选 prop,`contentOffset` 行为不变 |
| 2026-11-01 | 新增三个「站点外壳」组件,把应用里各自手写的一套收进库:`Brand`(logo / 首字标记 + 名称,`href` 可选)、`Ticker`(无限滚动信息条,按实测宽度位移,循环不跳帧)、`SearchField`(语义版搜索框,`variant` 切普通 / 悬浮条)。同时新增展示级令牌 `--su-font-size-2xl` / `--su-line-height-2xl`(32 / 40)。兼容性:纯新增,无破坏 |
| 2026-11-01 | 新增 `EntryRow` / `EntryTile`:把应用里两套手写内容卡(`.app-card` / `.ces-card`)收进库。两者共用同一套点击语义(无修饰键接管、按住修饰键保留默认)与悬停反馈;`EntryTile` 由 `Card padding="none" hoverable` + `Cover` 拼成。兼容性:纯新增 |
+39 -1
View File
@@ -1,4 +1,4 @@
import { Badge, Card, Icon, IconButton, Space } from '../../src'
import { Badge, Card, EntryRow, EntryTile, Icon, IconButton, ItemBadge, Space } from '../../src'
import { Demo } from '../components/Demo'
import { PropsTable } from '../components/PropsTable'
import { Section } from '../components/Section'
@@ -94,12 +94,50 @@ export function Cards() {
</Card>
</Demo>
<Demo
title="条目行与条目磁贴"
desc="同一份内容数据的两种版式:EntryRow 是横向一行(头像 + 标题 + 描述),EntryTile 是上图下文的白卡。点击语义一致——不带修饰键时接管点击,按住 Ctrl/Cmd 时保留浏览器默认行为。"
layout="column"
code={`<EntryRow title="GitHub" description="代码托管与开源社区" fallback="G" badge={<ItemBadge tone="hot">HOT</ItemBadge>} />
<EntryTile title="可塑性极强的玻璃" description="前沿科技" ratio={16 / 10} />`}
>
<EntryRow
title="GitHub"
description="代码托管与开源社区"
fallback="G"
badge={<ItemBadge tone="hot">HOT</ItemBadge>}
onOpen={() => undefined}
/>
<EntryRow title="知乎" description="深度内容问答社区" fallback="知" onOpen={() => undefined} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 'var(--su-space-3)' }}>
<EntryTile
title="可塑性极强的玻璃,在设计师手里能变成什么"
description="前沿科技"
ratio={16 / 10}
onOpen={() => undefined}
/>
<EntryTile title="废旧共享单车如何变成微型共享…" description="环保设计" ratio={16 / 10} />
</div>
</Demo>
<PropsTable
rows={[
{ name: 'as', type: "'div' | 'section' | 'article' | 'aside'", defaultValue: "'div'", desc: '渲染标签,按语义选' },
{ name: 'padding', type: "'none' | 'sm' | 'md' | 'lg'", defaultValue: "'md'", desc: '内边距档位' },
{ name: 'hoverable', type: 'boolean', defaultValue: 'false', desc: '悬停加深阴影' },
{ name: 'className / style', type: 'string / CSSProperties', desc: '布局微调,例如绝对定位角标' },
{ name: 'EntryRow.title', type: 'string', required: true, desc: '标题' },
{ name: 'EntryRow.description', type: 'string', desc: '单行描述' },
{ name: 'EntryRow.href', type: 'string', desc: '整行链接地址' },
{ name: 'EntryRow.avatar / fallback', type: 'string', desc: '头像图片 / 首字占位' },
{ name: 'EntryRow.verified', type: 'boolean', desc: '头像右下角认证勾' },
{ name: 'EntryRow.badge', type: 'ReactNode', desc: '标题右侧角标(如 HOT)' },
{ name: 'EntryRow.onOpen', type: '() => void', desc: '接管点击;不传保持链接行为' },
{ name: 'EntryTile.title', type: 'string', required: true, desc: '标题' },
{ name: 'EntryTile.description', type: 'string', desc: '单行描述' },
{ name: 'EntryTile.cover', type: 'string', desc: '封面地址' },
{ name: 'EntryTile.ratio', type: 'number', defaultValue: '16 / 10', desc: '封面宽高比' },
{ name: 'EntryTile.onOpen', type: '() => void', desc: '点击整张卡' },
]}
/>
</Section>
+33
View File
@@ -0,0 +1,33 @@
.root {
display: flex;
align-items: center;
gap: var(--su-space-4);
padding: var(--su-space-2) 0;
border-radius: var(--su-radius-sm);
color: inherit;
text-decoration: none;
}
.root:hover .title {
color: var(--su-color-primary);
}
.info {
display: flex;
flex-direction: column;
gap: var(--su-space-1);
min-width: 0;
}
.titleRow {
display: inline-flex;
align-items: center;
gap: var(--su-space-2);
min-width: 0;
}
@media (prefers-reduced-motion: reduce) {
.root .title {
transition: none;
}
}
+69
View File
@@ -0,0 +1,69 @@
import type { ReactNode } from 'react'
import { Avatar } from './Avatar'
import { Text } from './Typography'
import styles from './EntryRow.module.css'
/**
* 条目行:头像 + 标题(可带角标)+ 单行描述,整行可点。
*
* 点击语义跟站点里的其它卡片一致:不带修饰键时接管点击(走 SPA 路由),
* 按住 Ctrl/Cmd/Shift/Alt 时保留浏览器默认行为(新标签打开原始链接)。
*/
export type EntryRowProps = {
title: string
description?: string
/** 不带修饰键点击时跳转的地址 */
href?: string
/** 头像图片;不传时用 fallback 首字 */
avatar?: string
fallback?: string
/** 头像右下角的认证勾 */
verified?: boolean
/** 标题右侧的角标,例如 `<ItemBadge tone="hot">HOT</ItemBadge>` */
badge?: ReactNode
/** 接管点击;不传则保持普通链接行为 */
onOpen?: () => void
className?: string
}
export function EntryRow({
title,
description,
href = '#',
avatar,
fallback,
verified = false,
badge,
onOpen,
className,
}: EntryRowProps) {
return (
<a
className={[styles.root, className].filter(Boolean).join(' ')}
href={href}
title={title}
onClick={(event) => {
if (onOpen === undefined) return
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
event.preventDefault()
onOpen()
}}
>
<Avatar size="lg" tone="primary" src={avatar} fallback={fallback} verified={verified} />
<span className={styles.info}>
<span className={styles.titleRow}>
<Text strong truncate className={styles.title}>
{title}
</Text>
{badge}
</span>
{description !== undefined && description !== '' && (
<Text size="xs" tone="muted" truncate>
{description}
</Text>
)}
</span>
</a>
)
}
+40
View File
@@ -0,0 +1,40 @@
.root {
overflow: hidden;
transition: transform var(--su-duration-fast) var(--su-ease-in-out);
}
/* 悬停时整张卡浮起 2px,与 Cover 的 lift 是同一套手感 */
.root:hover {
transform: translateY(-2px);
}
.body {
display: flex;
flex-direction: column;
gap: var(--su-space-1);
padding: var(--su-space-3);
text-align: left;
}
.bodyButton {
width: 100%;
border: none;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
}
.bodyButton:hover {
background-color: var(--su-bg-subtle);
}
@media (prefers-reduced-motion: reduce) {
.root {
transition: none;
}
.root:hover {
transform: none;
}
}
+57
View File
@@ -0,0 +1,57 @@
import { Card } from './Card'
import { Cover } from './Cover'
import { Text } from './Typography'
import styles from './EntryTile.module.css'
/**
* 条目磁贴:上图下文的白卡,整张卡可点。
*
* 与 `EntryRow` 的区别是版式(横向一行 / 纵向一卡),交互语义一致:
* 不带修饰键时接管点击,按住修饰键保留浏览器默认行为。
*/
export type EntryTileProps = {
title: string
description?: string
cover?: string
/** 封面宽高比,默认 16 / 10 */
ratio?: number
/** 点击整张卡;不传则只是展示 */
onOpen?: () => void
className?: string
}
export function EntryTile({
title,
description,
cover,
ratio = 16 / 10,
onOpen,
className,
}: EntryTileProps) {
const body = (
<>
<Text strong truncate>
{title}
</Text>
{description !== undefined && description !== '' && (
<Text size="xs" tone="muted" truncate>
{description}
</Text>
)}
</>
)
return (
<Card padding="none" hoverable className={[styles.root, className].filter(Boolean).join(' ')}>
<Cover src={cover} alt={title} ratio={ratio} radius="none" onClick={onOpen} />
{onOpen === undefined ? (
<div className={styles.body}>{body}</div>
) : (
<button type="button" className={`${styles.body} ${styles.bodyButton}`} title={title} onClick={onOpen}>
{body}
</button>
)}
</Card>
)
}
+6
View File
@@ -37,6 +37,12 @@ export type { SearchFieldProps } from './components/SearchField'
export { Ticker } from './components/Ticker'
export type { TickerProps } from './components/Ticker'
export { EntryRow } from './components/EntryRow'
export type { EntryRowProps } from './components/EntryRow'
export { EntryTile } from './components/EntryTile'
export type { EntryTileProps } from './components/EntryTile'
export { Cover } from './components/Cover'
export type { CoverProps, CoverRatio } from './components/Cover'