Files
real-time-fund/app/components/SettingsModal.jsx

250 lines
9.3 KiB
JavaScript
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.
"use client";
import { useEffect, useRef, useState } from "react";
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { Progress } from '@/components/ui/progress';
import { Switch } from '@/components/ui/switch';
import ConfirmModal from './ConfirmModal';
import { ResetIcon, SettingsIcon } from './Icons';
export default function SettingsModal({
onClose,
tempSeconds,
setTempSeconds,
saveSettings,
exportLocalData,
importFileRef,
handleImportFileChange,
importMsg,
isMobile,
containerWidth = 1200,
setContainerWidth,
onResetContainerWidth,
showMarketIndexPc = true,
showMarketIndexMobile = true,
}) {
const [sliderDragging, setSliderDragging] = useState(false);
const [resetWidthConfirmOpen, setResetWidthConfirmOpen] = useState(false);
const [localSeconds, setLocalSeconds] = useState(tempSeconds);
const [localShowMarketIndexPc, setLocalShowMarketIndexPc] = useState(showMarketIndexPc);
const [localShowMarketIndexMobile, setLocalShowMarketIndexMobile] = useState(showMarketIndexMobile);
const pageWidthTrackRef = useRef(null);
const clampedWidth = Math.min(2000, Math.max(600, Number(containerWidth) || 1200));
const pageWidthPercent = ((clampedWidth - 600) / (2000 - 600)) * 100;
const updateWidthByClientX = (clientX) => {
if (!pageWidthTrackRef.current || !setContainerWidth) return;
const rect = pageWidthTrackRef.current.getBoundingClientRect();
if (!rect.width) return;
const ratio = (clientX - rect.left) / rect.width;
const clampedRatio = Math.min(1, Math.max(0, ratio));
const rawWidth = 600 + clampedRatio * (2000 - 600);
const snapped = Math.round(rawWidth / 10) * 10;
setContainerWidth(snapped);
};
useEffect(() => {
if (!sliderDragging) return;
const onPointerUp = () => setSliderDragging(false);
document.addEventListener('pointerup', onPointerUp);
document.addEventListener('pointercancel', onPointerUp);
return () => {
document.removeEventListener('pointerup', onPointerUp);
document.removeEventListener('pointercancel', onPointerUp);
};
}, [sliderDragging]);
// 外部的 tempSeconds 变更时,同步到本地显示,但不立即生效
useEffect(() => {
setLocalSeconds(tempSeconds);
}, [tempSeconds]);
useEffect(() => {
setLocalShowMarketIndexPc(showMarketIndexPc);
}, [showMarketIndexPc]);
useEffect(() => {
setLocalShowMarketIndexMobile(showMarketIndexMobile);
}, [showMarketIndexMobile]);
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) onClose?.();
}}
>
<DialogContent
overlayClassName={`modal-overlay ${sliderDragging ? 'modal-overlay-translucent' : ''} z-[9999]`}
className="!p-0 z-[10000]"
showCloseButton={false}
>
<div className="glass card modal">
<div className="title" style={{ marginBottom: 12 }}>
<SettingsIcon width="20" height="20" />
<DialogTitle asChild>
<span>设置</span>
</DialogTitle>
</div>
<div className="form-group" style={{ marginBottom: 16 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>刷新频率</div>
<div className="chips" style={{ marginBottom: 12 }}>
{[30, 60, 120, 300].map((s) => (
<button
key={s}
type="button"
className={`chip ${localSeconds === s ? 'active' : ''}`}
onClick={() => setLocalSeconds(s)}
aria-pressed={localSeconds === s}
>
{s}
</button>
))}
</div>
<input
className="input"
type="number"
inputMode="numeric"
min="30"
step="5"
value={localSeconds}
onChange={(e) => setLocalSeconds(Number(e.target.value))}
placeholder="自定义秒数"
/>
{localSeconds < 30 && (
<div className="error-text" style={{ marginTop: 8 }}>
最小 30
</div>
)}
</div>
{!isMobile && setContainerWidth && (
<div className="form-group" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
<div className="muted" style={{ fontSize: '0.8rem' }}>页面宽度</div>
{onResetContainerWidth && (
<button
type="button"
className="icon-button"
onClick={() => setResetWidthConfirmOpen(true)}
title="重置页面宽度"
style={{
border: 'none',
width: '24px',
height: '24px',
padding: 0,
backgroundColor: 'transparent',
color: 'var(--muted)',
}}
>
<ResetIcon width="14" height="14" />
</button>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div
ref={pageWidthTrackRef}
className="group relative"
style={{ flex: 1, height: 14, cursor: 'pointer', display: 'flex', alignItems: 'center' }}
onPointerDown={(e) => {
setSliderDragging(true);
updateWidthByClientX(e.clientX);
e.currentTarget.setPointerCapture?.(e.pointerId);
}}
onPointerMove={(e) => {
if (!sliderDragging) return;
updateWidthByClientX(e.clientX);
}}
>
<Progress value={pageWidthPercent} />
<div
className="pointer-events-none absolute top-1/2 -translate-y-1/2"
style={{ left: `${pageWidthPercent}%`, transform: 'translate(-50%, -50%)' }}
>
<div
className="h-3 w-3 rounded-full bg-primary shadow-md shadow-primary/40"
/>
</div>
</div>
<span className="muted" style={{ fontSize: '0.8rem', minWidth: 48 }}>
{clampedWidth}px
</span>
</div>
</div>
)}
<div className="form-group" style={{ marginBottom: 16 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>显示大盘指数</div>
<div className="row" style={{ justifyContent: 'flex-start', alignItems: 'center' }}>
<Switch
checked={isMobile ? localShowMarketIndexMobile : localShowMarketIndexPc}
className="ml-2 scale-125"
onCheckedChange={(checked) => {
const nextValue = Boolean(checked);
if (isMobile) setLocalShowMarketIndexMobile(nextValue);
else setLocalShowMarketIndexPc(nextValue);
}}
aria-label="显示大盘指数"
/>
</div>
</div>
<div className="form-group" style={{ marginBottom: 16 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>数据导出</div>
<div className="row" style={{ gap: 8 }}>
<button type="button" className="button" onClick={exportLocalData}>导出配置</button>
</div>
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem', marginTop: 26 }}>数据导入</div>
<div className="row" style={{ gap: 8, marginTop: 8 }}>
<button type="button" className="button" onClick={() => importFileRef.current?.click?.()}>导入配置</button>
</div>
<input
ref={importFileRef}
type="file"
accept="application/json"
style={{ display: 'none' }}
onChange={handleImportFileChange}
/>
{importMsg && (
<div className="muted" style={{ marginTop: 8 }}>
{importMsg}
</div>
)}
</div>
<div className="row" style={{ justifyContent: 'flex-end', marginTop: 24 }}>
<button
className="button"
onClick={(e) => saveSettings(
e,
localSeconds,
isMobile ? localShowMarketIndexMobile : localShowMarketIndexPc,
isMobile
)}
disabled={localSeconds < 30}
>
保存并关闭
</button>
</div>
</div>
</DialogContent>
{resetWidthConfirmOpen && onResetContainerWidth && (
<ConfirmModal
title="重置页面宽度"
message="是否重置页面宽度为默认值 1200px"
icon={<ResetIcon width="20" height="20" className="shrink-0 text-[var(--primary)]" />}
confirmVariant="primary"
onConfirm={() => {
onResetContainerWidth();
setResetWidthConfirmOpen(false);
}}
onCancel={() => setResetWidthConfirmOpen(false)}
confirmText="重置"
/>
)}
</Dialog>
);
}