fix(web): 视觉与交互 1:1(页头/侧栏/卡片/详情/动效)

对齐项(8080 vs 8081 实测,误差 ≤1px):
- 侧栏:资料卡 224、服务面板 144、导航 143,navTop 548 与原版一致
- 分区卡:142/142/327/270 vs 142/142/326/269;标题 16/1.2、更多按钮 24px
- 应用条目:48px 圆形头像 + 10px 小认证标;hover 名字变主色(不再整行变色)
- 封面:hover 上浮 2px(0.2s ease),去掉缩放;CES 封面恢复 16/10
- 搜索框:44 高、无边框 + 卡片阴影
- 服务面板:状态药丸 22、统计 12/18、操作按钮 28、刷新时图标原地旋转
- 详情:返回链接 hover 下划线、跳转按钮 hover 深主色、view-in 淡入动画
- 菜单:hover 主色、子菜单 0.28s grid 高度动画、chevron 6px 旋转
- 回到顶部:right 30、top 50%、滚动后淡入(补内容区滚动监听)
- 主题:卡片阴影恢复 2%/6%(旧版值)
This commit is contained in:
stash
2026-10-28 20:16:41 +08:00
parent 6b0c0a62e9
commit 1942e201e9
9 changed files with 181 additions and 33 deletions
+140 -9
View File
@@ -6,6 +6,9 @@
z-index: 1; z-index: 1;
/* 透明:让固定背景图从下面透出来(Layout 默认有底色) */ /* 透明:让固定背景图从下面透出来(Layout 默认有底色) */
background-color: transparent; background-color: transparent;
/* 旧版阴影是 2% / 6%,比库默认更深,属于主题级覆盖 */
--su-shadow-card: 0 6px 16px rgba(27, 26, 49, 0.02);
--su-shadow-hover: 0 6px 16px rgba(27, 26, 49, 0.06);
} }
/* 顶部背景图:固定 65vh,底部渐隐到页面底色 */ /* 顶部背景图:固定 65vh,底部渐隐到页面底色 */
@@ -87,6 +90,12 @@
transform: translateX(-50%); transform: translateX(-50%);
} }
/* 旧版搜索是「无边框 + 卡片阴影」 */
.search-row > *:first-child {
border-color: transparent;
box-shadow: var(--su-shadow-card);
}
/* ---------- 两栏骨架 ---------- */ /* ---------- 两栏骨架 ---------- */
.content { .content {
@@ -114,14 +123,57 @@
text-align: center; text-align: center;
} }
.profile-name {
line-height: 1.2;
}
.profile-role {
padding: 1px 8px;
border: 1px solid var(--su-border);
border-radius: var(--su-radius-pill);
font-size: var(--su-font-size-xs);
line-height: 18px;
color: var(--su-text-sub);
}
.profile-bio { .profile-bio {
line-height: 1.8; margin-top: 2px;
line-height: 1.55;
}
.profile-location {
margin-top: 2px;
font-size: var(--su-font-size-xs);
line-height: 18px;
color: var(--su-text-muted);
}
/* 旧版头像下面留 4px,和名字之间更松 */
.profile-card > span:first-child {
margin-bottom: 4px;
} }
.service-panel { .service-panel {
padding: 12px 14px; padding: 12px 14px;
} }
/* 旧版标题与统计值的行高比库默认更紧 */
.service-panel h2 {
line-height: 20px;
}
/* 旧版连接状态药丸:22 高、12px */
.service-status {
height: 22px;
padding: 0 10px;
font-size: var(--su-font-size-xs);
}
/* 旧版统计行是 12/18 */
.service-stats span {
line-height: 18px;
}
.service-stats { .service-stats {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -162,6 +214,16 @@
margin-bottom: var(--su-space-4); margin-bottom: var(--su-space-4);
} }
/* 旧版分区标题是 16/1.2,更多按钮是 24px 小方按钮 */
.section-header h2 {
line-height: 1.2;
}
.section-more {
width: 24px;
height: 24px;
}
.section-bar { .section-bar {
width: 4px; width: 4px;
height: 16px; height: 16px;
@@ -178,8 +240,8 @@
border-radius: var(--su-radius-sm); border-radius: var(--su-radius-sm);
} }
.app-card:hover { .app-card:hover .app-card-name {
background-color: var(--su-bg-subtle); color: var(--su-color-primary);
} }
.app-card-info { .app-card-info {
@@ -189,10 +251,29 @@
min-width: 0; min-width: 0;
} }
/* 旧版应用头像是 48px 圆形,右下角是 10px 小认证标 */
.app-card-avatar {
width: 48px;
height: 48px;
font-size: 18px;
}
.app-card-avatar > span + span {
width: 10px;
height: 10px;
border-width: 1px;
font-size: 6px;
}
/* ---------- 图文白卡 ---------- */ /* ---------- 图文白卡 ---------- */
.ces-card { .ces-card {
overflow: hidden; overflow: hidden;
transition: transform var(--su-duration-fast) var(--su-ease-in-out);
}
.ces-card:hover {
transform: translateY(-2px);
} }
.ces-body { .ces-body {
@@ -227,10 +308,6 @@
padding: 12px var(--su-space-5); padding: 12px var(--su-space-5);
} }
.detail-nav > * {
width: 100%;
}
.detail-back { .detail-back {
color: var(--su-color-primary); color: var(--su-color-primary);
font-size: var(--su-font-size-md); font-size: var(--su-font-size-md);
@@ -239,7 +316,11 @@
} }
.detail-back:hover { .detail-back:hover {
opacity: 0.8; text-decoration: underline;
}
.detail-nav > * {
width: 100%;
} }
.detail-hero { .detail-hero {
@@ -363,7 +444,7 @@
} }
.item-jump:hover:not(:disabled) { .item-jump:hover:not(:disabled) {
opacity: 0.8; background-color: var(--su-color-primary-hover);
} }
.item-jump:disabled { .item-jump:disabled {
@@ -454,6 +535,56 @@
/* ---------- 通用 ---------- */ /* ---------- 通用 ---------- */
/* 服务面板的两个操作按钮:旧版是 12px / 28 高 / 等分 / 描边 */
.service-action {
flex: 1;
height: 28px;
padding: 0 var(--su-space-2);
font-size: var(--su-font-size-xs);
border-radius: var(--su-radius-cover);
}
.service-action:hover:not(:disabled) {
border-color: var(--su-color-primary);
background-color: var(--su-color-primary-soft);
color: var(--su-color-primary);
}
/* 页头图标按钮:旧版 hover 是浅灰底 + 深色字 */
.site-header-inner button:hover {
background-color: var(--su-bg-light);
color: var(--su-text-main);
}
/* 分区卡右上角 •••:旧版 hover 只有浅灰底 */
.section-more:hover {
background-color: var(--su-bg-light);
color: var(--su-text-sub);
}
/* 详情 / 文章切换时的淡入上浮(旧版 view-in) */
@keyframes view-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: none;
}
}
.detail-view {
animation: view-in 0.22s ease both;
}
@media (prefers-reduced-motion: reduce) {
.detail-view {
animation: none;
}
}
.link-button { .link-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+15 -3
View File
@@ -103,20 +103,32 @@ export default function App() {
return [] return []
}, [itemView, itemCategory]) }, [itemView, itemCategory])
// 切换视图时回到顶部(页面整体滚动) // 切换视图时把内容区(内部滚动)和窗口都回到顶部
useEffect(() => { useEffect(() => {
const scroller = document.getElementById('content-scroll')
scroller?.scrollTo({ top: 0 })
window.scrollTo({ top: 0 }) window.scrollTo({ top: 0 })
}, [view]) }, [view])
/**
* 滚动状态同时驱动「顶部渐隐」和「回到顶部」。
* 桌面端滚的是内容容器(scroll 事件不冒泡),所以用捕获阶段监听。
*/
useEffect(() => { useEffect(() => {
const sync = () => { const sync = () => {
const next = window.scrollY > 4 const inner = document.getElementById('content-scroll')?.scrollTop ?? 0
const next = Math.max(inner, window.scrollY) > 4
setScrolled((current) => (current === next ? current : next)) setScrolled((current) => (current === next ? current : next))
} }
sync() sync()
window.addEventListener('scroll', sync, { passive: true }) window.addEventListener('scroll', sync, { passive: true })
return () => window.removeEventListener('scroll', sync) document.addEventListener('scroll', sync, { passive: true, capture: true })
return () => {
window.removeEventListener('scroll', sync)
document.removeEventListener('scroll', sync, { capture: true })
}
}, []) }, [])
/** /**
@@ -9,5 +9,5 @@ type BackToTopProps = {
} }
export function BackToTop({ visible }: BackToTopProps) { export function BackToTop({ visible }: BackToTopProps) {
return <ScrollTop visible={visible} targetId="content-scroll" /> return <ScrollTop visible={visible} targetId="content-scroll" offset={30} />
} }
+3 -2
View File
@@ -24,8 +24,9 @@ export function AppCard({ item, onOpen }: AppCardProps) {
}} }}
> >
<Avatar <Avatar
size="md" size="lg"
shape="square" tone="primary"
className="app-card-avatar"
src={item.cover} src={item.cover}
fallback={item.icon} fallback={item.icon}
verified={item.hot === true} verified={item.hot === true}
+1 -2
View File
@@ -27,9 +27,8 @@ export function CesCard({ item, onOpen }: CesCardProps) {
<Cover <Cover
src={item.cover} src={item.cover}
alt={item.title} alt={item.title}
ratio="article" ratio={16 / 10}
radius="none" radius="none"
hoverZoom
onClick={onOpen === undefined ? undefined : () => onOpen(item.id)} onClick={onOpen === undefined ? undefined : () => onOpen(item.id)}
/> />
{onOpen === undefined ? ( {onOpen === undefined ? (
@@ -21,7 +21,7 @@ export function SectionCard({ title, count, barColor, onMore, children }: Sectio
<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" />
<Title level={3} as="h2"> <Title level={3} as="h2" weight="semibold">
{title} {title}
</Title> </Title>
<Badge count={count} tone="danger" /> <Badge count={count} tone="danger" />
@@ -30,6 +30,7 @@ export function SectionCard({ title, count, barColor, onMore, children }: Sectio
{onMore !== undefined && ( {onMore !== undefined && (
<IconButton <IconButton
size="xs" size="xs"
className="section-more"
label={`查看${title}全部`} label={`查看${title}全部`}
icon={<Icon name="more_1_line" />} icon={<Icon name="more_1_line" />}
onClick={onMore} onClick={onMore}
@@ -29,6 +29,7 @@ export function SearchBar({ value, onChange, inputRef }: SearchBarProps) {
<div className="search-row"> <div className="search-row">
<Input <Input
inputRef={inputRef} inputRef={inputRef}
size="lg"
icon="search_3_line" icon="search_3_line"
value={value} value={value}
placeholder="搜索本地项目…" placeholder="搜索本地项目…"
@@ -9,18 +9,20 @@ export function ProfileCard() {
const { name, role, bio, location } = PLACEHOLDER_PROFILE const { name, role, bio, location } = PLACEHOLDER_PROFILE
return ( return (
<Card padding="lg" className="profile-card"> <Card padding="none" className="profile-card">
<Avatar size="xl" tone="primary" icon={<Icon name="user_3_line" />} verified /> <Avatar size="xl" tone="primary" icon={<Icon name="user_3_line" />} verified />
<Title level={3}>{name}</Title>
<Text size="xs" tone="muted"> <Title level={4} weight="semibold" className="profile-name">
{role} {name}
</Text> </Title>
<span className="profile-role">{role}</span>
<Text size="xs" tone="sub" className="profile-bio"> <Text size="xs" tone="sub" className="profile-bio">
{bio} {bio}
</Text> </Text>
<Text size="xs" tone="muted">
IP 属地:{location} <span className="profile-location">IP属地:{location}</span>
</Text>
</Card> </Card>
) )
} }
@@ -18,26 +18,26 @@ export function ServicePanel({ projects, live, refreshing, onRefresh }: ServiceP
return ( return (
<Card padding="none" 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" weight="semibold">
本地服务 本地服务
</Title> </Title>
<Tag tone={live ? 'success' : 'neutral'} dot> <Tag tone={live ? 'success' : 'neutral'} dot className="service-status">
{live ? '已连接' : '未连接'} {live ? '已连接' : '未连接'}
</Tag> </Tag>
</Space> </Space>
<div className="service-stats"> <div className="service-stats">
<Space justify="between"> <Space justify="between">
<Text size="sm" tone="sub"> <Text size="xs" tone="sub">
项目数 项目数
</Text> </Text>
<Text strong>{projects.length}</Text> <Text size="xs" strong>{projects.length}</Text>
</Space> </Space>
<Space justify="between"> <Space justify="between">
<Text size="sm" tone="sub"> <Text size="xs" tone="sub">
访问地址 访问地址
</Text> </Text>
<Text size="sm" truncate title={host} className="service-host"> <Text size="xs" truncate title={host} className="service-host">
{host} {host}
</Text> </Text>
</Space> </Space>
@@ -47,7 +47,8 @@ export function ServicePanel({ projects, live, refreshing, onRefresh }: ServiceP
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
loading={refreshing} className="service-action"
spinning={refreshing}
icon={<Icon name="refresh_3_line" />} icon={<Icon name="refresh_3_line" />}
onClick={onRefresh} onClick={onRefresh}
title="重新扫描项目" title="重新扫描项目"