refactor(web): 前端改用 @stash/ui 组件库
- 新增本地依赖 @stash/ui(file:../../stash-ui),引入其 style.css 与设计令牌 - 页面骨架改用 Layout(contained + header/sider),搜索框改为吸顶 - 卡片/菜单/按钮/标签/头像/分页/空态/提示全部换成组件库组件 - 删除本地 iconfont 与 1400 行手写组件样式,App.css 只保留页面级样式 - 数据层、路由、过滤逻辑未改动;33 个 Rust 测试通过
This commit is contained in:
+88
-116
@@ -1,3 +1,4 @@
|
||||
import { Empty, Layout, Notice } from '@stash/ui'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { BackToTop } from './components/common/BackToTop'
|
||||
import { ArticleDetailView } from './components/content/ArticleDetailView'
|
||||
@@ -39,7 +40,6 @@ export default function App() {
|
||||
const { view, go, back } = useViewRoute()
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const categories = useMemo(
|
||||
() => withLocalCategory(projects, catalog.categories),
|
||||
@@ -103,20 +103,18 @@ export default function App() {
|
||||
return []
|
||||
}, [itemView, itemCategory])
|
||||
|
||||
// 切换视图时把内容区滚回顶部
|
||||
// 切换视图时回到顶部(页面整体滚动)
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: 0 })
|
||||
window.scrollTo({ top: 0 })
|
||||
}, [view])
|
||||
|
||||
// 桌面端滚动作在内容容器里,窄屏滚的是窗口:
|
||||
// 两处都要驱动「已滚动」状态(顶部渐隐层 + 返回顶部按钮共用)
|
||||
useEffect(() => {
|
||||
const sync = () => {
|
||||
const inner = scrollRef.current?.scrollTop ?? 0
|
||||
const next = Math.max(inner, window.scrollY) > 4
|
||||
const next = window.scrollY > 4
|
||||
setScrolled((current) => (current === next ? current : next))
|
||||
}
|
||||
|
||||
sync()
|
||||
window.addEventListener('scroll', sync, { passive: true })
|
||||
return () => window.removeEventListener('scroll', sync)
|
||||
}, [])
|
||||
@@ -125,7 +123,6 @@ export default function App() {
|
||||
* 「打开这个分区」应该落到哪个视图。
|
||||
*
|
||||
* 应用类分区配了首页展示块时,直接落到那个分组;其余情况打开整个分区。
|
||||
* 这样卡片上的「···」和分区标题都通向卡片里实际展示的那块内容。
|
||||
*/
|
||||
const detailViewFor = (categoryId: string): View => {
|
||||
const category = categories.find((item) => item.id === categoryId)
|
||||
@@ -203,21 +200,13 @@ export default function App() {
|
||||
function renderItemView() {
|
||||
if (itemView === undefined) return null
|
||||
if (itemCategory === undefined) {
|
||||
return (
|
||||
<p className="section-empty">
|
||||
{dataPending ? '正在读取内容库…' : '分类不存在'}
|
||||
</p>
|
||||
)
|
||||
return <Empty text={dataPending ? '正在读取内容库…' : '分类不存在'} />
|
||||
}
|
||||
|
||||
if (itemCategory.kind === 'articles') {
|
||||
const article = findArticleItem(itemCategory, itemView.itemId)
|
||||
if (article === undefined) {
|
||||
return (
|
||||
<p className="section-empty">
|
||||
{dataPending ? '正在读取内容库…' : '条目不存在'}
|
||||
</p>
|
||||
)
|
||||
return <Empty text={dataPending ? '正在读取内容库…' : '条目不存在'} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -230,11 +219,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
if (detailModel === undefined) {
|
||||
return (
|
||||
<p className="section-empty">
|
||||
{dataPending ? '正在读取内容库…' : '条目不存在'}
|
||||
</p>
|
||||
)
|
||||
return <Empty text={dataPending ? '正在读取内容库…' : '条目不存在'} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -249,111 +234,98 @@ export default function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<>
|
||||
{/* 主题的顶部背景图:固定 65vh,底部渐隐到页面底色 */}
|
||||
<div className="site-background" aria-hidden="true" />
|
||||
|
||||
<SiteHeader
|
||||
projectCount={projects.length}
|
||||
sampleProject={projects[0]?.name}
|
||||
live={live}
|
||||
onFocusSearch={() => searchRef.current?.focus()}
|
||||
/>
|
||||
<Layout
|
||||
contained
|
||||
maxWidth={1380}
|
||||
siderWidth={260}
|
||||
className="app"
|
||||
header={
|
||||
<SiteHeader
|
||||
projectCount={projects.length}
|
||||
sampleProject={projects[0]?.name}
|
||||
live={live}
|
||||
onFocusSearch={() => searchRef.current?.focus()}
|
||||
/>
|
||||
}
|
||||
sider={
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
categories={sidebarCategories}
|
||||
live={live}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
activeCategoryId={viewCategoryId}
|
||||
activeGroupId={detailView?.groupId}
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="content">
|
||||
<SearchBar value={keyword} onChange={setKeyword} inputRef={searchRef} />
|
||||
|
||||
<div className="search-row">
|
||||
<SearchBar value={keyword} onChange={setKeyword} inputRef={searchRef} />
|
||||
</div>
|
||||
<h1 className="sr-only">Stash 本地应用工作区</h1>
|
||||
|
||||
<div className="layout">
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
categories={sidebarCategories}
|
||||
live={live}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
activeCategoryId={viewCategoryId}
|
||||
activeGroupId={detailView?.groupId}
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
{status === 'error' && (
|
||||
<Notice
|
||||
tone="error"
|
||||
title="连不上 Stash 服务"
|
||||
action={{ label: '重试', onClick: refresh }}
|
||||
>
|
||||
{error}
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
<main className="content">
|
||||
{/* 滚动后才淡入的顶部遮罩,避免内容在容器顶边被硬切 */}
|
||||
<div className={`content-veil${scrolled ? ' is-visible' : ''}`} aria-hidden="true" />
|
||||
{/* 内容库读取失败通常是用户改坏了 data 列里的 JSON,错误信息会带条目 id */}
|
||||
{catalog.status === 'error' && (
|
||||
<Notice
|
||||
tone="error"
|
||||
title="内容库读取失败"
|
||||
action={{ label: '重试', onClick: catalog.reload }}
|
||||
>
|
||||
{catalog.error}
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="content-scroll"
|
||||
id="content-scroll"
|
||||
ref={scrollRef}
|
||||
onScroll={(event) => {
|
||||
const next = event.currentTarget.scrollTop > 4
|
||||
setScrolled((current) => (current === next ? current : next))
|
||||
}}
|
||||
>
|
||||
<h1 className="sr-only">Stash 本地应用工作区</h1>
|
||||
{detailView === undefined && itemView === undefined && (
|
||||
<>
|
||||
{catalog.status === 'loading' && <Empty text="正在读取内容库…" />}
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="notice notice-error" role="alert">
|
||||
<strong>连不上 Stash 服务</strong>
|
||||
<p>{error}</p>
|
||||
<button type="button" className="notice-action" onClick={refresh}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 内容库读取失败通常是用户改坏了 data 列里的 JSON,错误信息会带条目 id */}
|
||||
{catalog.status === 'error' && (
|
||||
<div className="notice notice-error" role="alert">
|
||||
<strong>内容库读取失败</strong>
|
||||
<p>{catalog.error}</p>
|
||||
<button type="button" className="notice-action" onClick={catalog.reload}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{detailView === undefined && itemView === undefined && (
|
||||
<>
|
||||
{catalog.status === 'loading' && (
|
||||
<p className="section-empty">正在读取内容库…</p>
|
||||
)}
|
||||
|
||||
<HomeView
|
||||
categories={visibleCategories}
|
||||
onOpenDetail={openDetail}
|
||||
onOpenItem={openItem}
|
||||
/>
|
||||
|
||||
{searching && visibleCategories.length === 0 && (
|
||||
<div className="notice">
|
||||
<strong>没有匹配「{keyword.trim()}」的内容</strong>
|
||||
<p>换个关键字试试,或清空搜索查看全部。</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{detailView !== undefined && detailCategory !== undefined && (
|
||||
<DetailView
|
||||
category={detailCategory}
|
||||
groupId={detailView.groupId}
|
||||
onBack={goHome}
|
||||
onOpenItem={(itemId) => openItem(detailCategory.id, itemId)}
|
||||
<HomeView
|
||||
categories={visibleCategories}
|
||||
onOpenDetail={openDetail}
|
||||
onOpenItem={openItem}
|
||||
/>
|
||||
)}
|
||||
|
||||
{detailView !== undefined && detailCategory === undefined && (
|
||||
<p className="section-empty">
|
||||
{dataPending ? '正在读取内容库…' : '分类不存在'}
|
||||
</p>
|
||||
)}
|
||||
{searching && visibleCategories.length === 0 && (
|
||||
<Notice title={`没有匹配「${keyword.trim()}」的内容`}>
|
||||
换个关键字试试,或清空搜索查看全部。
|
||||
</Notice>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{itemView !== undefined && renderItemView()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{detailView !== undefined && detailCategory !== undefined && (
|
||||
<DetailView
|
||||
category={detailCategory}
|
||||
groupId={detailView.groupId}
|
||||
onBack={goHome}
|
||||
onOpenItem={(itemId) => openItem(detailCategory.id, itemId)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{detailView !== undefined && detailCategory === undefined && (
|
||||
<Empty text={dataPending ? '正在读取内容库…' : '分类不存在'} />
|
||||
)}
|
||||
|
||||
{itemView !== undefined && renderItemView()}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<BackToTop visible={scrolled} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user