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:
+210
-105
@@ -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
@@ -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" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user