refactor: 合并为单 crate,前端改为内嵌 React 控制台
合并 stash-cli / stash-core 为根目录单 crate(lib + bin),新增 React + Vite + TS 控制台替换 Askama 模板。 build.rs 用 walkdir 把 stash-web/dist 同步到 $OUT_DIR/web,由 include_dir! 编译期内嵌,产出单文件二进制。 路由改为 / 与 /assets/** 走内嵌前端,其余交给 ServeDir,缺失路径返回内置 404。同步更新依赖、.gitignore 与 README。
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { BackToTop } from './components/BackToTop'
|
||||
import { SearchBar } from './components/SearchBar'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { SiteCard } from './components/SiteCard'
|
||||
import { TopBar } from './components/TopBar'
|
||||
import { MOCK_SECTIONS, type CatalogCard, type CatalogSection } from './catalog'
|
||||
import { useProjects } from './useProjects'
|
||||
import './App.css'
|
||||
|
||||
function initialOf(name: string): string {
|
||||
const [first] = Array.from(name.trim())
|
||||
return first ? first.toUpperCase() : '#'
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { status, projects, error, live, refreshing, refresh } = useProjects()
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// 真实项目放最前面,占位分类跟在后面
|
||||
const sections = useMemo<CatalogSection[]>(() => {
|
||||
const localCards: CatalogCard[] = projects.map((project) => ({
|
||||
id: project.route_path,
|
||||
title: project.name,
|
||||
desc: project.route_path,
|
||||
href: project.route_path,
|
||||
initial: initialOf(project.name),
|
||||
mono: true,
|
||||
}))
|
||||
|
||||
return [{ id: 'local', title: '本地服务', cards: localCards }, ...MOCK_SECTIONS]
|
||||
}, [projects])
|
||||
|
||||
const query = keyword.trim().toLowerCase()
|
||||
const searching = query !== ''
|
||||
|
||||
// 搜索时过滤卡片,并丢掉没有命中的分区
|
||||
const sectionsToRender = useMemo(() => {
|
||||
if (!searching) return sections
|
||||
|
||||
return sections
|
||||
.map((section) => ({
|
||||
...section,
|
||||
cards: section.cards.filter(
|
||||
(card) =>
|
||||
card.title.toLowerCase().includes(query) ||
|
||||
card.desc.toLowerCase().includes(query),
|
||||
),
|
||||
}))
|
||||
.filter((section) => section.cards.length > 0)
|
||||
}, [sections, searching, query])
|
||||
|
||||
const noMatch = searching && sectionsToRender.length === 0
|
||||
|
||||
function renderLocalPlaceholder() {
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="notice notice-error" role="alert">
|
||||
<strong>连不上 Stash 服务</strong>
|
||||
<p>{error}</p>
|
||||
<p>
|
||||
请确认后端已启动(默认 <code>127.0.0.1:8080</code>),开发时 Vite 会把{' '}
|
||||
<code>/api</code> 代理到后端。
|
||||
</p>
|
||||
<button type="button" className="notice-action" onClick={refresh}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="notice">
|
||||
<strong>正在读取项目列表…</strong>
|
||||
<p>首次加载会扫描一次托管目录。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="notice">
|
||||
<strong>这里还没有项目</strong>
|
||||
<p>
|
||||
在托管根目录下创建子目录并放入 <code>index.html</code>,刷新后即可在这里看到它。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderSection(section: CatalogSection) {
|
||||
return (
|
||||
<section className="section-group" id={section.id} key={section.id}>
|
||||
<div className="section-heading">
|
||||
<h2 className="section-title">{section.title}</h2>
|
||||
<span className="section-badge">{section.cards.length}</span>
|
||||
</div>
|
||||
|
||||
{section.cards.length > 0 ? (
|
||||
<div className="grid">
|
||||
{section.cards.map((card, index) => (
|
||||
<SiteCard key={card.id} card={card} index={index} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
renderLocalPlaceholder()
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<TopBar
|
||||
projects={projects}
|
||||
live={live}
|
||||
onFocusSearch={() => searchRef.current?.focus()}
|
||||
/>
|
||||
|
||||
<div className="search-row">
|
||||
<SearchBar value={keyword} onChange={setKeyword} inputRef={searchRef} />
|
||||
</div>
|
||||
|
||||
<div className="layout">
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
live={live}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
|
||||
<main className="content">
|
||||
<div className="content-scroll" id="content-scroll">
|
||||
<h1 className="sr-only">Stash 本地应用工作区</h1>
|
||||
|
||||
{sectionsToRender.map(renderSection)}
|
||||
|
||||
{noMatch && (
|
||||
<div className="notice">
|
||||
<strong>没有匹配「{keyword.trim()}」的内容</strong>
|
||||
<p>换个关键字试试,或清空搜索查看全部。</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<BackToTop />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user