Files
iboard/src/components/SwipeTabs.jsx
T
cheney c765755906 feat(UI): 四 Tab 头部统一 + 清单模板分类 + 宏观 tab 顺序与溢出提示
头部统一
- 新增 PageHeader 组件: 高度 23px (原 NavBar 45px 的一半), 标题 12px 主色加粗, 背景取页面底色
- 打卡 / 宏观 / 监控 三页各自引用, 互不影响; 宏观与监控补齐「宏观」「监控」标题
- 「我」页移除 NavBar 空白条, 蓝色头像区直接贴顶 (不引用 PageHeader, 与其余三页解耦)
- 清理失效样式 .me-navbar

清单模板分类
- 分段由 2 项扩为 3 项: 进行中 / 已归档 / 模板; 右上角按钮随分段切换
- 模板独立增删改查: 新增 GET /templates/:id、PUT /templates/:id; 预置模板拒绝改删
- 模板按名称字母顺序排序 (Intl.Collator zh-Hans-CN, 中文按拼音/数字按数值), 前后端同口径
- 新建清单去掉场景选择器, 仅保留名称; scene 改为沿用模板自带场景
- 模板改为从右侧滑出 78vw 面板选择, 首项「不使用模板」

宏观 tab
- 顺序改为 报警 → 新闻 → 油价 → 经济数据 → 策略研究 → 经济文章, 默认选中新闻
- /macro 由路由直接重定向到 /macro/news, activeFromPath 兜底同样返回 news
- SwipeTabs 增加右侧溢出提示: 渐变遮罩 + 主色圆底箭头, 可点击向右翻页, 滚到末尾自动消失
- 切换 tab 时自动把激活项滚到可视区中央
2026-10-23 16:40:32 +08:00

91 lines
3.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from 'react'
import { Tabs } from 'antd-mobile'
// 顶部分类 + 下方仅渲染当前激活 tab 的内容
// - 顶部: antd-mobile Tabs (点击切换)
// - 下方: 同时挂载所有 tab, 通过 [hidden] 仅显示激活的那个 (保留各 tab 内部状态)
// - 右侧溢出提示: 当 tab 条还能向右滚动时, 右边缘显示渐变 + 箭头,
// 提示"右侧还有更多 tab"; 滚动到末尾后自动消失
// props:
// tabs: [{ key, label, render }]
// active: 当前激活 key
// onChange(key): 切换回调
export default function SwipeTabs({ tabs, active, onChange }) {
const wrapRef = useRef(null)
const [more, setMore] = useState(false)
const [headerH, setHeaderH] = useState(45)
// 判断右侧是否还有未展示的 tab (顺带记录 tab 条高度, 让提示层只覆盖页签行)
const measure = useCallback(() => {
const wrap = wrapRef.current
if (!wrap) return
const list = wrap.querySelector('.adm-tabs-tab-list')
const header = wrap.querySelector('.adm-tabs-header')
if (header && header.offsetHeight > 0) setHeaderH(header.offsetHeight)
if (!list) return
setMore(list.scrollLeft + list.clientWidth < list.scrollWidth - 2)
}, [])
useEffect(() => {
const wrap = wrapRef.current
if (!wrap) return
const list = wrap.querySelector('.adm-tabs-tab-list')
const header = wrap.querySelector('.adm-tabs-header')
if (!list) return
// 首次布局未稳定 (字体/宽度), 下一帧 + 300ms 各补测一次
const raf = requestAnimationFrame(measure)
const timer = setTimeout(measure, 300)
list.addEventListener('scroll', measure, { passive: true })
const ro = new ResizeObserver(measure)
ro.observe(list)
if (header) ro.observe(header)
return () => {
cancelAnimationFrame(raf)
clearTimeout(timer)
list.removeEventListener('scroll', measure)
ro.disconnect()
}
}, [measure, tabs.length])
// 切换 tab 后把激活项滚到可见区域中间, 避免选中项被藏在屏幕外
useEffect(() => {
const wrap = wrapRef.current
if (!wrap) return
const list = wrap.querySelector('.adm-tabs-tab-list')
const activeEl = wrap.querySelector('.adm-tabs-tab-active')
if (!list || !activeEl) return
const target = activeEl.offsetLeft - (list.clientWidth - activeEl.offsetWidth) / 2
list.scrollTo({ left: Math.max(0, target), behavior: 'smooth' })
}, [active, tabs.length])
return (
<div className="swipe-tabs-wrap" ref={wrapRef}>
<Tabs activeKey={active} onChange={onChange}>
{tabs.map((t) => (
<Tabs.Tab key={t.key} title={t.label} />
))}
</Tabs>
{more && (
<div
className="tab-more-hint"
style={{ height: headerH }}
onClick={() => {
const list = wrapRef.current?.querySelector('.adm-tabs-tab-list')
if (list) list.scrollTo({ left: list.scrollLeft + list.clientWidth * 0.6, behavior: 'smooth' })
}}
>
<span className="tab-more-arrow">›</span>
</div>
)}
<div className="tab-content">
{tabs.map((t) => (
<div key={t.key} className="tab-pane" hidden={t.key !== active}>
{t.render()}
</div>
))}
</div>
</div>
)
}