头部统一 - 新增 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 时自动把激活项滚到可视区中央
91 lines
3.3 KiB
React
91 lines
3.3 KiB
React
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>
|
||
)
|
||
}
|