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:
2026-10-21 17:59:08 +08:00
parent 92c510e959
commit e522a98f6f
48 changed files with 5113 additions and 273 deletions
+69
View File
@@ -0,0 +1,69 @@
export type Project = {
name: string
route_path: string
}
type ProjectsPayload = {
projects?: Project[]
}
const PROJECTS_ENDPOINT = '/api/projects'
const EVENTS_ENDPOINT = '/api/projects/events'
/** 读取服务端扫描出的项目列表。 */
export async function fetchProjects(signal?: AbortSignal): Promise<Project[]> {
const response = await fetch(PROJECTS_ENDPOINT, {
signal,
headers: { Accept: 'application/json' },
})
if (!response.ok) {
throw new Error(`请求 ${PROJECTS_ENDPOINT} 失败:HTTP ${response.status}`)
}
const payload = (await response.json()) as ProjectsPayload
return Array.isArray(payload.projects) ? payload.projects : []
}
export type ProjectEventsHandlers = {
onProjects: (projects: Project[]) => void
/** SSE 连接建立 / 断开时回调,浏览器的 EventSource 会自行重连。 */
onConnectionChange?: (connected: boolean) => void
}
/**
* 订阅服务端的项目快照推送。
* 浏览器不支持 SSE 时静默降级为空清理函数,页面仍可用手动刷新。
*/
export function subscribeProjects(handlers: ProjectEventsHandlers): () => void {
if (typeof EventSource === 'undefined') {
return () => {}
}
const source = new EventSource(EVENTS_ENDPOINT)
const handleProjects = (event: Event) => {
try {
const payload = JSON.parse((event as MessageEvent<string>).data) as ProjectsPayload
if (Array.isArray(payload.projects)) {
handlers.onProjects(payload.projects)
}
} catch {
// 单条事件格式异常时保留旧数据,等待下一次推送
}
}
const handleOpen = () => handlers.onConnectionChange?.(true)
const handleError = () => handlers.onConnectionChange?.(false)
source.addEventListener('projects', handleProjects)
source.addEventListener('open', handleOpen)
source.addEventListener('error', handleError)
return () => {
source.removeEventListener('projects', handleProjects)
source.removeEventListener('open', handleOpen)
source.removeEventListener('error', handleError)
source.close()
}
}