页面调整

This commit is contained in:
cheney
2026-08-06 11:26:54 +08:00
parent 7f72bf809b
commit 7fcd0c9569
6 changed files with 314 additions and 221 deletions
+10 -75
View File
@@ -1,68 +1,13 @@
import { useRef, useState, useEffect } from 'react'
import { Tabs } from 'antd-mobile'
// 滑屏容器 + 顶栏 tabs
// - 顶部: antd-mobile Tabs (带底部激活线动画, 仿原生)
// - 下方: 横向滑屏容器, 手势切换 tab
// 顶部分类 + 下方仅渲染当前激活 tab 的内容
// - 顶部: antd-mobile Tabs (点击切换)
// - 下方: 同时挂载所有 tab, 通过 [hidden] 仅显示激活的那个 (保留各 tab 内部状态)
// props:
// tabs: [{ key, label, render }]
// active: 当前激活 key
// onChange(key): 切换回调 (用户滑动或点击 tab)
// onChange(key): 切换回调
export default function SwipeTabs({ tabs, active, onChange }) {
const idx = Math.max(0, tabs.findIndex((t) => t.key === active))
const [dragging, setDragging] = useState(false)
const [offsetPx, setOffsetPx] = useState(0)
const [animOff, setAnimOff] = useState(false)
const startX = useRef(0)
const containerRef = useRef(null)
const widthRef = useRef(0)
// idx 变化时关闭拖动动画 (避免快速切换时滑动残影)
useEffect(() => { setOffsetPx(0); setAnimOff(false) }, [idx])
// 监听容器宽度变化, 拖动偏移按宽度百分比计算
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver(() => { widthRef.current = el.offsetWidth })
ro.observe(el)
widthRef.current = el.offsetWidth
return () => ro.disconnect()
}, [])
function onTouchStart(e) {
startX.current = e.touches[0].clientX
setDragging(true)
setAnimOff(true)
}
function onTouchMove(e) {
if (!dragging) return
const dx = e.touches[0].clientX - startX.current
const w = widthRef.current || window.innerWidth
// 限制左右溢出 (超过边界阻力增大)
const limited = Math.max(Math.min(dx, w * 0.4), -w * 0.4)
setOffsetPx(limited)
}
function onTouchEnd() {
if (!dragging) return
setDragging(false)
setAnimOff(false)
const w = widthRef.current || window.innerWidth
const dx = offsetPx
let next = idx
if (dx < -w * 0.18 && idx < tabs.length - 1) next = idx + 1
else if (dx > w * 0.18 && idx > 0) next = idx - 1
setOffsetPx(0)
if (next !== idx) onChange(tabs[next].key)
}
// 用 -33.33% × idx 作为基础位移, 再叠加拖动偏移
const basePct = idx * 33.3333
const dragPct = widthRef.current
? (offsetPx / widthRef.current) * 100
: 0
const translate = `calc(-${basePct}% + ${dragPct}%)`
return (
<div className="swipe-tabs-wrap">
<Tabs activeKey={active} onChange={onChange}>
@@ -70,22 +15,12 @@ export default function SwipeTabs({ tabs, active, onChange }) {
<Tabs.Tab key={t.key} title={t.label} />
))}
</Tabs>
<div
ref={containerRef}
className="swipe-container"
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchEnd}
>
<div
className={'swipe-track' + (animOff || dragging ? ' no-anim' : '')}
style={{ transform: `translateX(${translate})` }}
>
{tabs.map((t) => (
<div key={t.key} className="swipe-pane">{t.render()}</div>
))}
</div>
<div className="tab-content">
{tabs.map((t) => (
<div key={t.key} className="tab-pane" hidden={t.key !== active}>
{t.render()}
</div>
))}
</div>
</div>
)
+54
View File
@@ -0,0 +1,54 @@
// 趋势徽标: 上升 / 下降 / 持平 / 震荡
// props:
// trend: 'up' | 'down' | 'flat' | 'volatile'
// showText: 是否显示文字 (默认 true)
const META = {
up: { icon: '\u2191', text: '上升' },
down: { icon: '\u2193', text: '下降' },
flat: { icon: '=', text: '持平' },
volatile: { icon: '\u223F', text: '震荡' },
}
// 根据最近 N 个数据点计算趋势:
// - 数据不足 2 个 → flat
// - 取最近 6 个点的变化, 整体涨幅 < 1% → flat
// - 单调递增 → up, 单调递减 → down
// - 其余 (有升有降) → volatile
// 返回: { trend, change } (change 为百分比变化, 保留 1 位小数)
export function calcTrend(rows) {
if (!rows || rows.length < 2) return { trend: 'flat', change: 0 }
const N = Math.min(6, rows.length)
const recent = rows.slice(-N).map((r) => r.value).filter((v) => typeof v === 'number')
if (recent.length < 2) return { trend: 'flat', change: 0 }
const first = recent[0]
const last = recent[recent.length - 1]
const change = first === 0 ? 0 : ((last - first) / Math.abs(first)) * 100
if (Math.abs(change) < 1) return { trend: 'flat', change }
let up = 0, down = 0
for (let i = 1; i < recent.length; i++) {
if (recent[i] > recent[i - 1]) up++
else if (recent[i] < recent[i - 1]) down++
}
if (up === recent.length - 1) return { trend: 'up', change }
if (down === recent.length - 1) return { trend: 'down', change }
return { trend: 'volatile', change }
}
// 取数据序列最后一个有效点: { month, value } 或 null
export function lastPoint(data) {
const rows = (data && data.rows) || []
for (let i = rows.length - 1; i >= 0; i--) {
if (rows[i] && typeof rows[i].value === 'number') return rows[i]
}
return null
}
export default function TrendBadge({ trend, showText = true }) {
const m = META[trend] || META.flat
return (
<span className={'trend-badge trend-' + (trend || 'flat')}>
<span className="trend-icon">{m.icon}</span>
{showText && <span>{m.text}</span>}
</span>
)
}