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:
stash
2026-10-28 12:44:50 +08:00
parent 19ead7b716
commit 64f1fbf906
35 changed files with 735 additions and 2154 deletions
+5 -24
View File
@@ -1,33 +1,14 @@
import { ArrowUpIcon } from './Icon'
import { ScrollTop } from '@stash/ui'
/**
* 回到顶部:复刻 Alright 主题的 .scroll-toolbar .totop-button。
*
* - 42px 圆形、浅灰底,固定在右侧(bottom: 50%)
* - 默认隐藏,只有内容区滚动后才淡入,回到顶部后再淡出
* 回到顶部:直接用组件库的 ScrollTop。
* 页面整体滚动(没有内部滚动容器),所以不传 targetId。
*/
type BackToTopProps = {
/** 内容区是否已滚动(由 App 传入) */
/** 页面是否已滚动(由 App 传入) */
visible: boolean
}
export function BackToTop({ visible }: BackToTopProps) {
const handleClick = () => {
// 桌面端滚动的是内容容器,窄屏滚动的是窗口,两处都滚一遍
document.getElementById('content-scroll')?.scrollTo({ top: 0, behavior: 'smooth' })
window.scrollTo({ top: 0, behavior: 'smooth' })
}
return (
<button
type="button"
className={`back-to-top${visible ? ' is-visible' : ''}`}
title="回到顶部"
aria-label="回到顶部"
tabIndex={visible ? 0 : -1}
onClick={handleClick}
>
<ArrowUpIcon />
</button>
)
return <ScrollTop visible={visible} />
}
@@ -1,11 +1,13 @@
import { Badge } from '@stash/ui'
/**
* 数量角标:贴在分区标题右上角的小红标。
* 纯展示组件,只接收数字,宽度由内容撑开(两位数不会变形)。
* 数量角标:贴在分区标题旁边的小红标。
* 组件库的 Badge 已经处理了两位数和 99+ 的宽度,这里只做语义封装。
*/
type CountBadgeProps = {
count: number
}
export function CountBadge({ count }: CountBadgeProps) {
return <span className="count-badge">{count}</span>
return <Badge count={count} tone="danger" />
}
+17 -23
View File
@@ -1,63 +1,57 @@
/**
* 通用图标组件。
*
* 全部使用 Alright 主题的 iconfont 线性图标(见 src/iconfont.css),
* 尺寸与颜色由使用处的 CSS 控制(font-size / color),
* 因此同一套图标可以复用在按钮、统计条、导航等不同位置。
*/
import { Icon } from '@stash/ui'
/**
* 语义图标:stash 内部一直按用途引用(SearchIcon / RefreshIcon…),
* 这里统一映射到 @stash/ui 的 iconfont 字形,避免各页面记字形名。
*/
type IconProps = {
/** 附加类名,便于局部覆盖字号或颜色 */
className?: string
}
const base = (name: string, className?: string) =>
`iconfont icon-${name}${className !== undefined ? ` ${className}` : ''}`
export function SearchIcon({ className }: IconProps) {
return <i className={base('search', className)} aria-hidden="true" />
return <Icon name="search_3_line" className={className} />
}
export function UserIcon({ className }: IconProps) {
return <i className={base('user', className)} aria-hidden="true" />
return <Icon name="user_3_line" className={className} />
}
export function RefreshIcon({ className }: IconProps) {
return <i className={base('refresh', className)} aria-hidden="true" />
return <Icon name="refresh_3_line" className={className} />
}
export function LinkIcon({ className }: IconProps) {
return <i className={base('link', className)} aria-hidden="true" />
return <Icon name="link_3_line" className={className} />
}
export function ExternalIcon({ className }: IconProps) {
return <i className={base('external', className)} aria-hidden="true" />
return <Icon name="a-external_link_line1" className={className} />
}
export function EyeIcon({ className }: IconProps) {
return <i className={base('eye', className)} aria-hidden="true" />
return <Icon name="eye_line" className={className} />
}
export function ThumbUpIcon({ className }: IconProps) {
return <i className={base('thumb-up', className)} aria-hidden="true" />
return <Icon name="thumb_up_2_line" className={className} />
}
export function ShareIcon({ className }: IconProps) {
return <i className={base('share', className)} aria-hidden="true" />
return <Icon name="share_3_line" className={className} />
}
export function CloseIcon({ className }: IconProps) {
return <i className={base('close', className)} aria-hidden="true" />
return <Icon name="close_line" className={className} />
}
export function MoreIcon({ className }: IconProps) {
return <i className={base('more', className)} aria-hidden="true" />
return <Icon name="more_1_line" className={className} />
}
export function ArrowUpIcon({ className }: IconProps) {
return <i className={base('arrow-up', className)} aria-hidden="true" />
return <Icon name="arrow_up_line" className={className} />
}
export function ArrowDownIcon({ className }: IconProps) {
return <i className={base('arrow-down', className)} aria-hidden="true" />
return <Icon name="arrow_down_line" className={className} />
}