"use client"; import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { AnimatePresence, motion } from "framer-motion"; import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter, } from "@dnd-kit/core"; import { restrictToParentElement } from "@dnd-kit/modifiers"; import { SortableContext, rectSortingStrategy, useSortable, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerClose, } from "@/components/ui/drawer"; import { CloseIcon, MinusIcon, ResetIcon, SettingsIcon } from "./Icons"; import ConfirmModal from "./ConfirmModal"; import { cn } from "@/lib/utils"; function SortableIndexItem({ item, canRemove, onRemove, isMobile }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.code }); const style = { transform: CSS.Transform.toString(transform), transition, cursor: isDragging ? "grabbing" : "grab", flex: isMobile ? "0 0 calc((100% - 24px) / 3)" : "0 0 calc((100% - 48px) / 5)", touchAction: "none", ...(isDragging && { position: "relative", zIndex: 10, opacity: 0.9, boxShadow: "0 8px 24px rgba(0,0,0,0.15)", }), }; const isUp = item.change >= 0; const color = isUp ? "var(--danger)" : "var(--success)"; return (
{canRemove && ( )}
{item.name}
{item.price?.toFixed ? item.price.toFixed(2) : String(item.price ?? "-")}
{(item.change >= 0 ? "+" : "") + item.change.toFixed(2)}{" "} {(item.changePercent >= 0 ? "+" : "") + item.changePercent.toFixed(2)}%
); } /** * 指数个性化设置弹框 * * - 移动端:使用 Drawer(自底向上抽屉) * - PC 端:使用 Dialog(居中弹窗) * * @param {Object} props * @param {boolean} props.open - 是否打开 * @param {() => void} props.onClose - 关闭回调 * @param {boolean} props.isMobile - 是否为移动端(由上层传入) * @param {Array<{code:string,name:string,price:number,change:number,changePercent:number}>} props.indices - 当前可用的大盘指数列表 * @param {string[]} props.selectedCodes - 已选中的指数 code,决定展示顺序 * @param {(codes: string[]) => void} props.onChangeSelected - 更新选中指数集合 * @param {() => void} props.onResetDefault - 恢复默认选中集合 */ export default function MarketSettingModal({ open, onClose, isMobile, indices = [], selectedCodes = [], onChangeSelected, onResetDefault, }) { const selectedList = useMemo(() => { if (!indices?.length || !selectedCodes?.length) return []; const map = new Map(indices.map((it) => [it.code, it])); return selectedCodes .map((code) => map.get(code)) .filter(Boolean); }, [indices, selectedCodes]); const allIndices = indices || []; const selectedSet = useMemo( () => new Set(selectedCodes || []), [selectedCodes] ); const [resetConfirmOpen, setResetConfirmOpen] = useState(false); useEffect(() => { if (!open) setResetConfirmOpen(false); }, [open]); useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor) ); const handleToggleCode = (code) => { if (!code) return; if (selectedSet.has(code)) { // 至少保留一个指数,阻止把最后一个也移除 if (selectedCodes.length <= 1) return; const next = selectedCodes.filter((c) => c !== code); onChangeSelected?.(next); } else { const next = [...selectedCodes, code]; onChangeSelected?.(next); } }; const handleDragEnd = (event) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = selectedCodes.indexOf(active.id); const newIndex = selectedCodes.indexOf(over.id); if (oldIndex === -1 || newIndex === -1) return; const next = arrayMove(selectedCodes, oldIndex, newIndex); onChangeSelected?.(next); }; const body = (
已添加指数
拖动下方指数即可排序
{selectedList.length === 0 ? (
暂未添加指数,请在下方选择想要关注的指数。
) : (
{selectedList.map((item) => ( 1} onRemove={handleToggleCode} isMobile={isMobile} /> ))}
)}
点击即可选指数
{onResetDefault && ( )}
{allIndices.map((item) => { const active = selectedSet.has(item.code); return ( ); })}
); if (!open) return null; if (isMobile) { return ( { if (!v) onClose?.(); }} direction="bottom" > 指数个性化设置
{body}
{resetConfirmOpen && ( } confirmVariant="primary" confirmText="恢复默认" onConfirm={() => { onResetDefault?.(); setResetConfirmOpen(false); }} onCancel={() => setResetConfirmOpen(false)} /> )}
); } const pcContent = ( {open && ( e.stopPropagation()} style={{ width: 690 }} >
指数个性化设置
{body}
)} {resetConfirmOpen && ( } confirmVariant="primary" confirmText="恢复默认" onConfirm={() => { onResetDefault?.(); setResetConfirmOpen(false); }} onCancel={() => setResetConfirmOpen(false)} /> )}
); if (typeof document === "undefined") return null; return createPortal(pcContent, document.body); }