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:
+140
-9
@@ -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
@@ -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} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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="重新扫描项目"
|
||||||
|
|||||||
Reference in New Issue
Block a user