"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);
}