fix(web): 恢复与旧版一致的布局与组件参数

- 布局:Layout 使用 contained(1380)+gutter20+siderWidth240+gap20、solid 60px 页头、整屏内部滚动
- 搜索框恢复悬浮居中;侧栏与内容恢复 52/88px 顶部偏移
- 新增并使用 @stash/ui 的 Cover(海报 2:3 / 图文 4:3 / 宽图)替换手写封面
- 头像恢复 64px 品牌底色;正文恢复 14/1.9;详情标题、标签、引用、统计、跳转按原版还原
- 分区卡 padding 20、分区标题 16、网格间距 12/16 对齐原值
This commit is contained in:
stash
2026-10-28 18:12:14 +08:00
parent 64f1fbf906
commit 1bdcca025b
20 changed files with 319 additions and 196 deletions
+210 -105
View File
@@ -1,26 +1,41 @@
/* 页面级样式:只保留「组件库没有」的部分(背景、外壳、封面比例、正文版式)。 /* 页面级样式:只保留「组件库没有」的部分(背景、外壳、封面兜底、正文版式)。
颜色 / 间距 / 字号全部取自 @stash/ui 的令牌,组件内部样式不要再在这里覆盖。 */ 颜色 / 间距 / 字号全部取自 @stash/ui 的令牌。 */
.app { .app {
background-color: var(--su-bg-page); position: relative;
z-index: 1;
/* 透明:让固定背景图从下面透出来(Layout 默认有底色) */
background-color: transparent;
} }
/* 顶部背景图:固定 65vh,底部渐隐到页面底色 */ /* 顶部背景图:固定 65vh,底部渐隐到页面底色 */
.site-background { .site-background {
position: fixed; position: fixed;
inset: 0 0 auto; inset: 0 0 auto;
z-index: -1; z-index: 0;
height: 65vh; height: 65vh;
background: url('./assets/site-bg.jpg') center top / cover no-repeat; background: url('./assets/site-bg.jpg') center top / cover no-repeat;
opacity: 0.16; pointer-events: none;
mask-image: linear-gradient(to bottom, #000 35%, transparent 100%);
} }
.site-background::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 80%;
background: linear-gradient(to bottom, transparent, var(--su-bg-page));
}
/* ---------- 页头 ---------- */
.site-header-inner { .site-header-inner {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--su-space-6); justify-content: space-between;
height: 64px; gap: var(--su-space-4);
height: var(--su-header-height);
} }
.site-brand { .site-brand {
@@ -34,10 +49,10 @@
place-items: center; place-items: center;
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: var(--su-radius-sm); border-radius: var(--su-radius-pill);
background-color: var(--su-color-primary); background-color: var(--su-text-main);
color: var(--su-white); color: var(--su-white);
font-size: var(--su-font-size-md); font-size: var(--su-font-size-base);
font-weight: var(--su-font-weight-strong); font-weight: var(--su-font-weight-strong);
} }
@@ -56,46 +71,50 @@
margin-left: auto; margin-left: auto;
} }
/* ---------- 搜索:悬浮在页头下方居中 ---------- */
.search-row {
position: fixed;
top: 76px;
left: 50%;
z-index: var(--su-z-header);
width: min(480px, calc(100vw - 48px));
height: 44px;
transform: translateX(-50%);
}
/* ---------- 两栏骨架 ---------- */
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--su-space-4); gap: var(--su-space-5);
padding-top: var(--su-space-2); padding-top: 88px;
} padding-bottom: var(--su-space-10);
/* 搜索框吸在页头下方,滚动时内容从它下面穿过 */
.search-row {
position: sticky;
top: 64px;
z-index: var(--su-z-header);
padding: var(--su-space-2) 0;
background-color: color-mix(in srgb, var(--su-bg-page) 88%, transparent);
backdrop-filter: blur(8px);
} }
.sidebar { .sidebar {
position: sticky;
top: 84px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--su-space-3); gap: var(--su-space-4);
padding: var(--su-space-4) var(--su-space-4) var(--su-space-4) 0; padding-top: 88px;
} }
.profile-card, /* ---------- 侧栏卡片 ---------- */
.service-panel,
.nav-card {
flex-shrink: 0;
}
.profile-card { .profile-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: var(--su-space-2); gap: 6px;
padding: 18px 14px 14px;
text-align: center; text-align: center;
} }
.service-panel {
padding: 12px 14px;
}
.service-stats { .service-stats {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -107,10 +126,16 @@
max-width: 140px; max-width: 140px;
} }
.nav-card {
padding: var(--su-space-2);
}
/* ---------- 首页分区 ---------- */
.home-view { .home-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--su-space-4); gap: var(--su-space-5);
} }
.section-header { .section-header {
@@ -131,10 +156,9 @@
.app-card { .app-card {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--su-space-3); gap: 15px;
padding: var(--su-space-2); padding: 6px 0;
border-radius: var(--su-radius-sm); border-radius: var(--su-radius-sm);
transition: background-color var(--su-duration-fast) var(--su-ease-in-out);
} }
.app-card:hover { .app-card:hover {
@@ -148,50 +172,7 @@
min-width: 0; min-width: 0;
} }
/* ---------- 封面卡(海报 / 图文) ---------- */ /* ---------- 图文白卡 ---------- */
.cover-card {
position: relative;
display: block;
width: 100%;
overflow: hidden;
border-radius: var(--su-radius-sm);
background-color: var(--su-bg-light);
text-align: left;
}
.cover {
display: block;
width: 100%;
object-fit: cover;
transition: transform var(--su-duration-base) var(--su-ease-in-out);
}
.cover--poster {
aspect-ratio: 2 / 3;
}
.cover--article {
aspect-ratio: 4 / 3;
}
.cover-card:hover .cover {
transform: scale(1.04);
}
.cover-caption {
position: absolute;
inset: auto 0 0;
display: -webkit-box;
overflow: hidden;
padding: 24px var(--su-space-2) var(--su-space-2);
background: linear-gradient(transparent, rgb(0 0 0 / 62%));
color: var(--su-white);
font-size: var(--su-font-size-sm);
line-height: var(--su-line-height-sm);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.ces-card { .ces-card {
overflow: hidden; overflow: hidden;
@@ -200,6 +181,7 @@
.ces-body { .ces-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--su-space-1);
padding: var(--su-space-3); padding: var(--su-space-3);
text-align: left; text-align: left;
} }
@@ -218,23 +200,63 @@
.detail-view { .detail-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--su-space-4); gap: var(--su-space-5);
}
.detail-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px var(--su-space-5);
}
.detail-back {
color: var(--su-color-primary);
font-size: var(--su-font-size-md);
font-weight: var(--su-font-weight-bold);
cursor: pointer;
}
.detail-back:hover {
opacity: 0.8;
} }
.detail-hero { .detail-hero {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: var(--su-space-3); padding: 12px 0 4px;
text-align: center;
} }
.detail-hero-icon { .detail-hero-icon {
font-size: 24px; font-size: 40px;
line-height: 1; line-height: 1;
} }
.item-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 28px;
}
.item-title {
justify-content: center;
gap: 6px;
}
.item-edit { .item-edit {
margin-left: var(--su-space-2); margin-left: var(--su-space-1);
font-size: var(--su-font-size-sm);
color: var(--su-color-primary);
vertical-align: middle; vertical-align: middle;
cursor: pointer;
}
.item-edit:disabled {
cursor: not-allowed;
opacity: 0.6;
} }
.item-cover { .item-cover {
@@ -268,14 +290,64 @@
} }
.item-quote { .item-quote {
padding: var(--su-space-3) var(--su-space-4); display: flex;
border-left: 2px solid var(--su-color-primary); gap: 6px;
border-radius: 0 var(--su-radius-sm) var(--su-radius-sm) 0; width: 100%;
background-color: var(--su-bg-subtle); margin-top: 18px;
text-align: left;
}
.item-quote-mark {
font-size: var(--su-font-size-lg);
line-height: 1;
color: var(--su-color-primary);
}
.item-quote-text {
font-size: var(--su-font-size-md);
line-height: 1.7;
color: var(--su-text-sub);
} }
.item-actions { .item-actions {
margin-top: var(--su-space-4); display: flex;
align-items: center;
gap: var(--su-space-2);
width: 100%;
margin-top: 18px;
}
/* 统计小药丸:浏览 / 点赞的底 */
.item-stat {
min-width: 56px;
height: 36px;
padding: 0 var(--su-space-3);
border-radius: var(--su-radius-cover);
background-color: var(--su-bg-page);
}
/* 跳转按钮占满剩余宽度 */
.item-jump {
display: inline-flex;
flex: 1;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
border-radius: var(--su-radius-cover);
background-color: var(--su-color-primary);
color: var(--su-white);
font-size: var(--su-font-size-md);
transition: opacity var(--su-duration-fast) var(--su-ease-in-out);
}
.item-jump:hover:not(:disabled) {
opacity: 0.8;
}
.item-jump:disabled {
cursor: not-allowed;
opacity: 0.5;
} }
.item-banner { .item-banner {
@@ -292,16 +364,28 @@
gap: var(--su-space-2); gap: var(--su-space-2);
} }
.article-body { .article-heading {
display: flex; max-width: 760px;
flex-direction: column; font-size: var(--su-font-size-xl);
gap: var(--su-space-3); font-weight: var(--su-font-weight-strong);
line-height: 1.4;
} }
.article-meta-dot { .article-meta-dot {
color: var(--su-text-muted); color: var(--su-text-muted);
} }
.article-body-card {
overflow: hidden;
}
.article-body {
display: flex;
flex-direction: column;
gap: var(--su-space-3);
padding: 32px 40px 40px;
}
.article-lead { .article-lead {
margin-bottom: var(--su-space-2); margin-bottom: var(--su-space-2);
} }
@@ -314,12 +398,27 @@
/* ---------- 推荐阅读 ---------- */ /* ---------- 推荐阅读 ---------- */
.related-item { .related-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 24px;
margin-top: var(--su-space-4);
}
.related-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--su-space-2); gap: var(--su-space-2);
padding: var(--su-space-2); margin-bottom: var(--su-space-4);
border-radius: var(--su-radius-sm); }
.related-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 4px;
border-radius: var(--su-radius-cover);
text-align: left; text-align: left;
transition: background-color var(--su-duration-fast) var(--su-ease-in-out); transition: background-color var(--su-duration-fast) var(--su-ease-in-out);
} }
@@ -340,6 +439,7 @@
gap: var(--su-space-1); gap: var(--su-space-1);
height: 32px; height: 32px;
padding: 0 var(--su-space-3); padding: 0 var(--su-space-3);
border: 1px solid var(--su-border);
border-radius: var(--su-radius-sm); border-radius: var(--su-radius-sm);
font-size: var(--su-font-size-sm); font-size: var(--su-font-size-sm);
color: var(--su-text-sub); color: var(--su-text-sub);
@@ -364,11 +464,10 @@
@media (max-width: 1100px) { @media (max-width: 1100px) {
.sidebar { .sidebar {
position: static;
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--su-space-3); gap: var(--su-space-3);
padding: var(--su-space-4); padding: 0 var(--su-space-4) var(--su-space-4);
} }
.profile-card { .profile-card {
@@ -382,6 +481,17 @@
.header-info { .header-info {
display: none; display: none;
} }
.search-row {
position: static;
width: 100%;
height: 36px;
transform: none;
}
.content {
padding-top: var(--su-space-4);
}
} }
@media (max-width: 820px) { @media (max-width: 820px) {
@@ -389,17 +499,12 @@
display: none; display: none;
} }
.search-row {
top: 60px;
}
.site-header-inner { .site-header-inner {
height: 60px; height: 56px;
} }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.cover,
.app-card, .app-card,
.related-item, .related-item,
.link-button { .link-button {
+10 -1
View File
@@ -241,7 +241,16 @@ export default function App() {
<Layout <Layout
contained contained
maxWidth={1380} maxWidth={1380}
siderWidth={260} gutter={20}
siderWidth={240}
gap={20}
headerHeight={60}
headerVariant="solid"
siderBordered={false}
contentPadding="none"
height="screen"
scrollable
contentId="content-scroll"
className="app" className="app"
header={ header={
<SiteHeader <SiteHeader
@@ -1,8 +1,7 @@
import { ScrollTop } from '@stash/ui' import { ScrollTop } from '@stash/ui'
/** /**
* 回到顶部:直接用组件库的 ScrollTop。 * 回到顶部:直接用组件库的 ScrollTop,滚动容器是应用的内容区。
* 页面整体滚动(没有内部滚动容器),所以不传 targetId。
*/ */
type BackToTopProps = { type BackToTopProps = {
/** 页面是否已滚动(由 App 传入) */ /** 页面是否已滚动(由 App 传入) */
@@ -10,5 +9,5 @@ type BackToTopProps = {
} }
export function BackToTop({ visible }: BackToTopProps) { export function BackToTop({ visible }: BackToTopProps) {
return <ScrollTop visible={visible} /> return <ScrollTop visible={visible} targetId="content-scroll" />
} }
+7 -2
View File
@@ -2,7 +2,7 @@ import { Grid } from '@stash/ui'
import type { AppItem } from '../../data/types' import type { AppItem } from '../../data/types'
import { AppCard } from './AppCard' import { AppCard } from './AppCard'
/** 应用网格:默认 4 列,平板 2 列,手机 2 列(组件库 Grid 负责断点) */ /** 应用网格:桌面 4 列,平板 / 手机 2 列(断点由组件库 Grid 负责) */
type AppGridProps = { type AppGridProps = {
items: AppItem[] items: AppItem[]
/** 明细页使用更宽松的间距 */ /** 明细页使用更宽松的间距 */
@@ -13,7 +13,12 @@ type AppGridProps = {
export function AppGrid({ items, variant = 'home', onOpen }: AppGridProps) { export function AppGrid({ items, variant = 'home', onOpen }: AppGridProps) {
return ( return (
<Grid columns={4} tabletColumns={2} mobileColumns={2} gap={variant === 'detail' ? 'md' : 'sm'}> <Grid
columns={4}
tabletColumns={2}
mobileColumns={2}
gap={variant === 'detail' ? 'lg' : 'md'}
>
{items.map((item) => ( {items.map((item) => (
<AppCard key={item.id} item={item} onOpen={onOpen} /> <AppCard key={item.id} item={item} onOpen={onOpen} />
))} ))}
@@ -123,7 +123,7 @@ export function ArticleBody({ markdown }: ArticleBodyProps) {
} }
return ( return (
<Paragraph tone="main" key={index}> <Paragraph tone="sub" size="md" leading="relaxed" key={index}>
{renderInline(block.text)} {renderInline(block.text)}
</Paragraph> </Paragraph>
) )
@@ -1,3 +1,4 @@
import { Cover } from '@stash/ui'
import type { ArticleItem } from '../../data/types' import type { ArticleItem } from '../../data/types'
/** /**
@@ -10,20 +11,13 @@ type ArticleCardProps = {
} }
export function ArticleCard({ item, onOpen }: ArticleCardProps) { export function ArticleCard({ item, onOpen }: ArticleCardProps) {
const content = (
<>
<img className="cover cover--article" src={item.cover} alt={item.title} loading="lazy" />
<span className="cover-caption">{item.title}</span>
</>
)
if (onOpen === undefined) {
return <figure className="cover-card">{content}</figure>
}
return ( return (
<button type="button" className="cover-card" title={item.title} onClick={() => onOpen(item.id)}> <Cover
{content} src={item.cover}
</button> alt={item.title}
caption={item.title}
ratio="article"
onClick={onOpen === undefined ? undefined : () => onOpen(item.id)}
/>
) )
} }
@@ -1,4 +1,4 @@
import { Button, Card, Paragraph, Space, Text, Title } from '@stash/ui' import { Card, Paragraph, Space, Text, Title } from '@stash/ui'
import type { ArticleItem } from '../../data/types' import type { ArticleItem } from '../../data/types'
import { ArticleBody } from './ArticleBody' import { ArticleBody } from './ArticleBody'
@@ -20,23 +20,23 @@ export function ArticleDetailView({ article, categoryTitle, onBack }: ArticleDet
return ( return (
<div className="detail-view article-view"> <div className="detail-view article-view">
<Card padding="sm"> <Card padding="none" className="detail-nav">
<Space justify="between" wrap> <Space justify="between" wrap>
<Button variant="ghost" size="sm" onClick={onBack}> <button type="button" className="detail-back" onClick={onBack}>
返回 返回
</Button> </button>
<Text size="sm" tone="sub"> <Text size="xs" tone="muted">
{categoryTitle} / 文章 {categoryTitle} / 文章
</Text> </Text>
</Space> </Space>
</Card> </Card>
<header className="article-head"> <header className="article-head">
<Title level={2}> <Title level={1} as="h2" className="article-heading">
{article.title} {article.title}
<Button variant="ghost" size="sm" disabled className="item-edit"> <button type="button" className="item-edit" disabled title="编辑功能待接入">
编辑 [编辑]
</Button> </button>
</Title> </Title>
<Space gap="sm" wrap> <Space gap="sm" wrap>
@@ -55,9 +55,9 @@ export function ArticleDetailView({ article, categoryTitle, onBack }: ArticleDet
</Space> </Space>
</header> </header>
<Card padding="lg"> <Card padding="none" className="article-body-card">
<div className="article-body"> <div className="article-body">
<Paragraph tone="main" className="article-lead"> <Paragraph tone="main" size="md" leading="relaxed" className="article-lead">
{article.desc} {article.desc}
</Paragraph> </Paragraph>
<ArticleBody markdown={article.body} /> <ArticleBody markdown={article.body} />
@@ -11,7 +11,7 @@ type ArticleGridProps = {
export function ArticleGrid({ items, onOpen }: ArticleGridProps) { export function ArticleGrid({ items, onOpen }: ArticleGridProps) {
return ( return (
<Grid columns={4} tabletColumns={2} mobileColumns={1} gap="sm"> <Grid columns={4} tabletColumns={2} mobileColumns={1} gap="md">
{items.map((item) => ( {items.map((item) => (
<ArticleCard key={item.id} item={item} onOpen={onOpen} /> <ArticleCard key={item.id} item={item} onOpen={onOpen} />
))} ))}
+9 -2
View File
@@ -1,4 +1,4 @@
import { Card, Space, Text } from '@stash/ui' import { Card, Cover, Space, Text } from '@stash/ui'
import type { ArticleItem } from '../../data/types' import type { ArticleItem } from '../../data/types'
/** /**
@@ -24,7 +24,14 @@ export function CesCard({ item, onOpen }: CesCardProps) {
return ( return (
<Card padding="none" hoverable className="ces-card"> <Card padding="none" hoverable className="ces-card">
<img className="cover cover--article" src={item.cover} alt={item.title} loading="lazy" /> <Cover
src={item.cover}
alt={item.title}
ratio="article"
radius="none"
hoverZoom
onClick={onOpen === undefined ? undefined : () => onOpen(item.id)}
/>
{onOpen === undefined ? ( {onOpen === undefined ? (
<div className="ces-body">{body}</div> <div className="ces-body">{body}</div>
) : ( ) : (
@@ -11,7 +11,7 @@ type CesCardGridProps = {
export function CesCardGrid({ items, onOpen }: CesCardGridProps) { export function CesCardGrid({ items, onOpen }: CesCardGridProps) {
return ( return (
<Grid columns={4} tabletColumns={2} mobileColumns={1} gap="sm"> <Grid columns={4} tabletColumns={2} mobileColumns={1} gap="lg">
{items.map((item) => ( {items.map((item) => (
<CesCard key={item.id} item={item} onOpen={onOpen} /> <CesCard key={item.id} item={item} onOpen={onOpen} />
))} ))}
@@ -1,4 +1,4 @@
import { Badge, Breadcrumb, Button, Card, Empty, Space, Title } from '@stash/ui' import { Badge, Breadcrumb, Card, Empty, Space, Title } from '@stash/ui'
import { categoryCount, type Category } from '../../data/types' import { categoryCount, type Category } from '../../data/types'
import { AppGrid } from './AppGrid' import { AppGrid } from './AppGrid'
import { CesCardGrid } from './CesCardGrid' import { CesCardGrid } from './CesCardGrid'
@@ -70,11 +70,11 @@ export function DetailView({ category, groupId, onBack, onOpenItem }: DetailView
return ( return (
<div className="detail-view"> <div className="detail-view">
<Card padding="sm"> <Card padding="none" className="detail-nav">
<Space justify="between" wrap> <Space justify="between" wrap>
<Button variant="ghost" size="sm" onClick={onBack}> <button type="button" className="detail-back" onClick={onBack}>
返回首页 返回首页
</Button> </button>
<Breadcrumb items={crumbs} /> <Breadcrumb items={crumbs} />
</Space> </Space>
</Card> </Card>
@@ -1,4 +1,4 @@
import { Button, Card, Icon, IconButton, Paragraph, Space, Tag, Text, Title } from '@stash/ui' import { Card, Icon, IconButton, Paragraph, Space, Tag, Text, Title } from '@stash/ui'
import type { ItemDetailModel } from '../../data/detail' import type { ItemDetailModel } from '../../data/detail'
import type { RelatedItem } from '../../data/types' import type { RelatedItem } from '../../data/types'
import { RelatedList } from './RelatedList' import { RelatedList } from './RelatedList'
@@ -27,23 +27,25 @@ export function ItemDetailView({
}: ItemDetailViewProps) { }: ItemDetailViewProps) {
return ( return (
<div className="detail-view item-view"> <div className="detail-view item-view">
<Card padding="sm"> <Card padding="none" className="detail-nav">
<Space justify="between" wrap> <Space justify="between" wrap>
<Button variant="ghost" size="sm" onClick={onBack}> <button type="button" className="detail-back" onClick={onBack}>
返回 返回
</Button> </button>
<Text size="sm" tone="sub"> <span className="detail-breadcrumb">
{breadcrumb} <Text size="xs" tone="muted">
</Text> {breadcrumb}
</Text>
</span>
</Space> </Space>
</Card> </Card>
<Card padding="lg"> <Card padding="none" className="item-card">
<Title level={2}> <Title level={2} className="item-title">
{model.title} {model.title}
<Button variant="ghost" size="sm" disabled className="item-edit"> <button type="button" className="item-edit" disabled title="编辑功能待接入">
编辑 [编辑]
</Button> </button>
</Title> </Title>
<div className={`item-cover item-cover--${model.shape}`}> <div className={`item-cover item-cover--${model.shape}`}>
@@ -57,7 +59,7 @@ export function ItemDetailView({
{model.tags.length > 0 && ( {model.tags.length > 0 && (
<Space gap="sm" wrap> <Space gap="sm" wrap>
{model.tags.map((tag) => ( {model.tags.map((tag) => (
<Tag key={tag} shape="rounded"> <Tag key={tag} tone="primary">
{tag} {tag}
</Tag> </Tag>
))} ))}
@@ -65,9 +67,12 @@ export function ItemDetailView({
)} )}
{model.comment !== undefined && ( {model.comment !== undefined && (
<Paragraph tone="secondary" className="item-quote"> <p className="item-quote">
{model.comment} <span className="item-quote-mark" aria-hidden="true">
</Paragraph> “
</span>
<span className="item-quote-text">{model.comment}</span>
</p>
)} )}
{model.paragraphs.map((paragraph, index) => ( {model.paragraphs.map((paragraph, index) => (
@@ -76,36 +81,36 @@ export function ItemDetailView({
</Paragraph> </Paragraph>
))} ))}
<Space justify="between" wrap className="item-actions"> <div className="item-actions">
<Space gap="lg"> <Space gap="sm">
<Space gap="xs" className="item-stat"> <Space gap="xs" className="item-stat">
<Icon name="eye_line" /> <Icon name="eye_line" />
<Text size="sm" tone="sub"> <Text size="xs" tone="sub">
{model.views} {model.views}
</Text> </Text>
</Space> </Space>
<Space gap="xs" className="item-stat"> <Space gap="xs" className="item-stat">
<Icon name="thumb_up_2_line" /> <Icon name="thumb_up_2_line" />
<Text size="sm" tone="sub"> <Text size="xs" tone="sub">
{model.likes} {model.likes}
</Text> </Text>
</Space> </Space>
<IconButton label="分享(待接入)" size="xs" icon={<Icon name="share_3_line" />} /> <IconButton label="分享(待接入)" size="sm" icon={<Icon name="share_3_line" />} />
</Space> </Space>
{/* 占位数据没有真实地址时,按钮置灰而不是打开空白页 */} {/* 占位数据没有真实地址时,按钮置灰而不是打开空白页 */}
{model.href === '#' ? ( {model.href === '#' ? (
<Button size="sm" disabled> <button type="button" className="item-jump" disabled title="暂无跳转地址">
跳转到 跳转到
<Icon name="a-external_link_line1" /> <Icon name="a-external_link_line1" />
</Button> </button>
) : ( ) : (
<a className="link-button" href={model.href} target="_blank" rel="noreferrer"> <a className="item-jump" href={model.href} target="_blank" rel="noreferrer">
跳转到 跳转到
<Icon name="a-external_link_line1" /> <Icon name="a-external_link_line1" />
</a> </a>
)} )}
</Space> </div>
{model.banner !== undefined && ( {model.banner !== undefined && (
<img className="item-banner" src={model.banner} alt="" loading="lazy" /> <img className="item-banner" src={model.banner} alt="" loading="lazy" />
@@ -1,3 +1,4 @@
import { Cover } from '@stash/ui'
import type { PosterItem } from '../../data/types' import type { PosterItem } from '../../data/types'
/** /**
@@ -10,20 +11,13 @@ type PosterCardProps = {
} }
export function PosterCard({ item, onOpen }: PosterCardProps) { export function PosterCard({ item, onOpen }: PosterCardProps) {
const content = (
<>
<img className="cover cover--poster" src={item.cover} alt={item.title} loading="lazy" />
<span className="cover-caption">{item.title}</span>
</>
)
if (onOpen === undefined) {
return <figure className="cover-card">{content}</figure>
}
return ( return (
<button type="button" className="cover-card" title={item.title} onClick={() => onOpen(item.id)}> <Cover
{content} src={item.cover}
</button> alt={item.title}
caption={item.title}
ratio="poster"
onClick={onOpen === undefined ? undefined : () => onOpen(item.id)}
/>
) )
} }
@@ -12,7 +12,7 @@ type PosterGridProps = {
export function PosterGrid({ items, onOpen }: PosterGridProps) { export function PosterGrid({ items, onOpen }: PosterGridProps) {
return ( return (
<Grid columns={6} tabletColumns={4} mobileColumns={3} gap="sm"> <Grid columns={6} tabletColumns={4} mobileColumns={3} gap="md">
{items.map((item) => ( {items.map((item) => (
<PosterCard key={item.id} item={item} onOpen={onOpen} /> <PosterCard key={item.id} item={item} onOpen={onOpen} />
))} ))}
@@ -14,12 +14,13 @@ export function RelatedList({ items, onOpen }: RelatedListProps) {
if (items.length === 0) return null if (items.length === 0) return null
return ( return (
<Card padding="lg" className="related-card"> <Card padding="md" className="related-card">
<Title level={4} as="h3"> <Title level={3} as="h3" className="related-title">
<span aria-hidden="true">📚</span>
推荐阅读 推荐阅读
</Title> </Title>
<Grid columns={2} tabletColumns={2} mobileColumns={1} gap="sm"> <Grid className="related-grid" columns={2} tabletColumns={2} mobileColumns={1} gap="sm">
{items.map((item) => ( {items.map((item) => (
<button <button
key={item.id} key={item.id}
@@ -27,7 +28,12 @@ export function RelatedList({ items, onOpen }: RelatedListProps) {
className="related-item" className="related-item"
onClick={() => onOpen(item.id)} onClick={() => onOpen(item.id)}
> >
<Avatar size="sm" shape="square" src={item.cover} fallback={item.icon ?? '•'} /> <Avatar
size="md"
tone="primary"
src={item.cover}
fallback={item.icon ?? '•'}
/>
<Space direction="column" gap="xs" align="start" className="related-info"> <Space direction="column" gap="xs" align="start" className="related-info">
<Text size="sm" strong truncate> <Text size="sm" strong truncate>
{item.name} {item.name}
@@ -17,7 +17,7 @@ type SectionCardProps = {
export function SectionCard({ title, count, barColor, onMore, children }: SectionCardProps) { export function SectionCard({ title, count, barColor, onMore, children }: SectionCardProps) {
return ( return (
<Card padding="lg" className="section-card"> <Card padding="md" className="section-card">
<div className="section-header"> <div className="section-header">
<Space gap="sm"> <Space gap="sm">
<span className="section-bar" style={{ backgroundColor: barColor }} aria-hidden="true" /> <span className="section-bar" style={{ backgroundColor: barColor }} aria-hidden="true" />
@@ -35,7 +35,6 @@ export function SearchBar({ value, onChange, inputRef }: SearchBarProps) {
aria-label="搜索本地项目" aria-label="搜索本地项目"
clearable clearable
onClear={() => onChange('')} onClear={() => onChange('')}
shortcut="Ctrl K"
spellCheck={false} spellCheck={false}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
@@ -65,7 +65,7 @@ export function CategoryNav({
} }
return ( return (
<Card padding="sm" className="nav-card"> <Card padding="none" className="nav-card">
<Menu items={items} activeKey={activeKey} onSelect={handleSelect} /> <Menu items={items} activeKey={activeKey} onSelect={handleSelect} />
</Card> </Card>
) )
@@ -10,8 +10,8 @@ export function ProfileCard() {
return ( return (
<Card padding="lg" className="profile-card"> <Card padding="lg" className="profile-card">
<Avatar size="lg" fallback="S" verified /> <Avatar size="xl" tone="primary" fallback="S" verified />
<Title level={4}>{name}</Title> <Title level={3}>{name}</Title>
{!name.includes(role) && <Tag shape="rounded">{role}</Tag>} {!name.includes(role) && <Tag shape="rounded">{role}</Tag>}
<Paragraph>{bio}</Paragraph> <Paragraph>{bio}</Paragraph>
<Text size="xs" tone="muted"> <Text size="xs" tone="muted">
@@ -16,7 +16,7 @@ export function ServicePanel({ projects, live, refreshing, onRefresh }: ServiceP
const host = typeof window === 'undefined' ? '-' : window.location.host const host = typeof window === 'undefined' ? '-' : window.location.host
return ( return (
<Card padding="md" className="service-panel"> <Card padding="none" className="service-panel">
<Space justify="between"> <Space justify="between">
<Title level={4} as="h2"> <Title level={4} as="h2">
本地服务 本地服务
@@ -48,7 +48,7 @@ export function ServicePanel({ projects, live, refreshing, onRefresh }: ServiceP
<Space gap="sm" wrap> <Space gap="sm" wrap>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="secondary"
loading={refreshing} loading={refreshing}
icon={<Icon name="refresh_3_line" />} icon={<Icon name="refresh_3_line" />}
onClick={onRefresh} onClick={onRefresh}