feat: 移动端指数排序
This commit is contained in:
@@ -1,7 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { AnimatePresence, Reorder } from "framer-motion";
|
import { AnimatePresence } 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 {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
@@ -20,6 +36,94 @@ import { CloseIcon, MinusIcon, ResetIcon, SettingsIcon } from "./Icons";
|
|||||||
import ConfirmModal from "./ConfirmModal";
|
import ConfirmModal from "./ConfirmModal";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function SortableIndexItem({ item, canRemove, onRemove }) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: item.code });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
cursor: isDragging ? "grabbing" : "grab",
|
||||||
|
flex: "0 0 calc((100% - 24px) / 3)",
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={cn(
|
||||||
|
"glass card",
|
||||||
|
"relative flex flex-col gap-1.5 rounded-xl border border-[var(--border)] bg-[var(--card)] px-3 py-2"
|
||||||
|
)}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
{canRemove && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove(item.code);
|
||||||
|
}}
|
||||||
|
className="icon-button"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: "999px",
|
||||||
|
backgroundColor: "rgba(255,96,96,0.1)",
|
||||||
|
color: "var(--danger)",
|
||||||
|
border: "none",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
aria-label={`移除 ${item.name}`}
|
||||||
|
>
|
||||||
|
<MinusIcon width="10" height="10" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
paddingRight: 18,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color }}>
|
||||||
|
{item.price?.toFixed ? item.price.toFixed(2) : String(item.price ?? "-")}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color }}>
|
||||||
|
{(item.change >= 0 ? "+" : "") + item.change.toFixed(2)}{" "}
|
||||||
|
{(item.changePercent >= 0 ? "+" : "") + item.changePercent.toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指数个性化设置弹框
|
* 指数个性化设置弹框
|
||||||
*
|
*
|
||||||
@@ -60,6 +164,11 @@ export default function MarketSettingModal({
|
|||||||
|
|
||||||
const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
|
const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
useSensor(KeyboardSensor)
|
||||||
|
);
|
||||||
|
|
||||||
const handleToggleCode = (code) => {
|
const handleToggleCode = (code) => {
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
if (selectedSet.has(code)) {
|
if (selectedSet.has(code)) {
|
||||||
@@ -73,8 +182,14 @@ export default function MarketSettingModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReorder = (newOrder) => {
|
const handleDragEnd = (event) => {
|
||||||
onChangeSelected?.(newOrder);
|
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 = (
|
const body = (
|
||||||
@@ -112,98 +227,28 @@ export default function MarketSettingModal({
|
|||||||
暂未添加指数,请在下方选择想要关注的指数。
|
暂未添加指数,请在下方选择想要关注的指数。
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Reorder.Group
|
<DndContext
|
||||||
as="div"
|
sensors={sensors}
|
||||||
axis="y"
|
collisionDetection={closestCenter}
|
||||||
values={selectedCodes}
|
onDragEnd={handleDragEnd}
|
||||||
onReorder={handleReorder}
|
modifiers={[restrictToParentElement]}
|
||||||
className="flex flex-wrap gap-3"
|
|
||||||
>
|
>
|
||||||
<AnimatePresence initial={false}>
|
<SortableContext
|
||||||
{selectedList.map((item) => {
|
items={selectedCodes}
|
||||||
const isUp = item.change >= 0;
|
strategy={rectSortingStrategy}
|
||||||
const color =
|
>
|
||||||
isUp ? "var(--danger)" : "var(--success)";
|
<div className="flex flex-wrap gap-3">
|
||||||
return (
|
{selectedList.map((item) => (
|
||||||
<Reorder.Item
|
<SortableIndexItem
|
||||||
key={item.code}
|
key={item.code}
|
||||||
value={item.code}
|
item={item}
|
||||||
className={cn(
|
canRemove={selectedCodes.length > 1}
|
||||||
"glass card",
|
onRemove={handleToggleCode}
|
||||||
"relative flex flex-col gap-1.5 rounded-xl border border-[var(--border)] bg-[var(--card)] px-3 py-2"
|
/>
|
||||||
)}
|
))}
|
||||||
style={{
|
</div>
|
||||||
cursor: "grab",
|
</SortableContext>
|
||||||
flex: "0 0 calc((100% - 24px) / 3)",
|
</DndContext>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedCodes.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleToggleCode(item.code);
|
|
||||||
}}
|
|
||||||
className="icon-button"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 4,
|
|
||||||
right: 4,
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
borderRadius: "999px",
|
|
||||||
backgroundColor: "rgba(255,96,96,0.1)",
|
|
||||||
color: "var(--danger)",
|
|
||||||
border: "none",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
aria-label={`移除 ${item.name}`}
|
|
||||||
>
|
|
||||||
<MinusIcon width="10" height="10" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 500,
|
|
||||||
paddingRight: 18,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 600,
|
|
||||||
color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.price?.toFixed
|
|
||||||
? item.price.toFixed(2)
|
|
||||||
: String(item.price ?? "-")}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(item.change >= 0 ? "+" : "") +
|
|
||||||
item.change.toFixed(2)}{" "}
|
|
||||||
{(item.changePercent >= 0 ? "+" : "") +
|
|
||||||
item.changePercent.toFixed(2)}
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
</Reorder.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Reorder.Group>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user