From 17a09f22b6af77d6f8864d0f80b6ab041724a3b8 Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 1 Nov 2026 16:08:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=20EntryRow=20/?= =?UTF-8?q?=20EntryTile=EF=BC=8C=E6=94=B6=E6=8E=89=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E9=87=8C=E4=B8=A4=E5=A5=97=E6=89=8B=E5=86=99=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EntryRow:头像 + 标题(可带角标)+ 单行描述,整行可点; 点击语义内置(无修饰键接管,按住 Ctrl/Cmd/Shift/Alt 保留浏览器默认) - EntryTile:上图下文白卡,由 Card padding=none hoverable + Cover 拼成 - 展示页 #cards 补两个示例与 props,COMPONENTS.md 补契约与 changelog 兼容性:纯新增;应用侧的旧类名(.app-card / .ces-card / .ces-body)随之删除 --- docs/COMPONENTS.md | 3 ++ site/sections/Cards.tsx | 40 ++++++++++++++++- src/components/EntryRow.module.css | 33 ++++++++++++++ src/components/EntryRow.tsx | 69 +++++++++++++++++++++++++++++ src/components/EntryTile.module.css | 40 +++++++++++++++++ src/components/EntryTile.tsx | 57 ++++++++++++++++++++++++ src/index.ts | 6 +++ 7 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/components/EntryRow.module.css create mode 100644 src/components/EntryRow.tsx create mode 100644 src/components/EntryTile.module.css create mode 100644 src/components/EntryTile.tsx diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index dc9e6c0..e1b981b 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -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` 拼成。兼容性:纯新增 | diff --git a/site/sections/Cards.tsx b/site/sections/Cards.tsx index 4e0cd13..64e230d 100644 --- a/site/sections/Cards.tsx +++ b/site/sections/Cards.tsx @@ -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() { + HOT} /> +`} + > + HOT} + onOpen={() => undefined} + /> + undefined} /> +
+ undefined} + /> + +
+
+ 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: '点击整张卡' }, ]} /> diff --git a/src/components/EntryRow.module.css b/src/components/EntryRow.module.css new file mode 100644 index 0000000..49c693e --- /dev/null +++ b/src/components/EntryRow.module.css @@ -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; + } +} diff --git a/src/components/EntryRow.tsx b/src/components/EntryRow.tsx new file mode 100644 index 0000000..277b3a2 --- /dev/null +++ b/src/components/EntryRow.tsx @@ -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 + /** 标题右侧的角标,例如 `HOT` */ + badge?: ReactNode + /** 接管点击;不传则保持普通链接行为 */ + onOpen?: () => void + className?: string +} + +export function EntryRow({ + title, + description, + href = '#', + avatar, + fallback, + verified = false, + badge, + onOpen, + className, +}: EntryRowProps) { + return ( + { + if (onOpen === undefined) return + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return + event.preventDefault() + onOpen() + }} + > + + + + + + {title} + + {badge} + + {description !== undefined && description !== '' && ( + + {description} + + )} + + + ) +} diff --git a/src/components/EntryTile.module.css b/src/components/EntryTile.module.css new file mode 100644 index 0000000..0e48f03 --- /dev/null +++ b/src/components/EntryTile.module.css @@ -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; + } +} diff --git a/src/components/EntryTile.tsx b/src/components/EntryTile.tsx new file mode 100644 index 0000000..14d866d --- /dev/null +++ b/src/components/EntryTile.tsx @@ -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 = ( + <> + + {title} + + {description !== undefined && description !== '' && ( + + {description} + + )} + + ) + + return ( + + + + {onOpen === undefined ? ( +
{body}
+ ) : ( + + )} +
+ ) +} diff --git a/src/index.ts b/src/index.ts index 1206933..ff1298a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'