feat: 排序个性化新增排序形式切换

This commit is contained in:
hzm
2026-03-20 09:03:51 +08:00
parent 4f438d0dc5
commit 9f6d1bb768
4 changed files with 243 additions and 43 deletions

View File

@@ -3,6 +3,7 @@
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';
@@ -19,10 +20,13 @@ export default function SettingsModal({
containerWidth = 1200,
setContainerWidth,
onResetContainerWidth,
showMarketIndex = true,
setShowMarketIndex,
}) {
const [sliderDragging, setSliderDragging] = useState(false);
const [resetWidthConfirmOpen, setResetWidthConfirmOpen] = useState(false);
const [localSeconds, setLocalSeconds] = useState(tempSeconds);
const [localShowMarketIndex, setLocalShowMarketIndex] = useState(showMarketIndex);
const pageWidthTrackRef = useRef(null);
const clampedWidth = Math.min(2000, Math.max(600, Number(containerWidth) || 1200));
@@ -55,6 +59,10 @@ export default function SettingsModal({
setLocalSeconds(tempSeconds);
}, [tempSeconds]);
useEffect(() => {
setLocalShowMarketIndex(showMarketIndex);
}, [showMarketIndex]);
return (
<Dialog
open
@@ -162,6 +170,22 @@ export default function SettingsModal({
</div>
)}
<div hidden 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={localShowMarketIndex}
className="ml-2 scale-125"
onCheckedChange={(checked) => {
const nextValue = Boolean(checked);
setLocalShowMarketIndex(nextValue);
setShowMarketIndex?.(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 }}>
@@ -188,7 +212,7 @@ export default function SettingsModal({
<div className="row" style={{ justifyContent: 'flex-end', marginTop: 24 }}>
<button
className="button"
onClick={(e) => saveSettings(e, localSeconds)}
onClick={(e) => saveSettings(e, localSeconds, localShowMarketIndex)}
disabled={localSeconds < 30}
>
保存并关闭

View File

@@ -10,6 +10,7 @@ import {
DrawerTitle,
DrawerClose,
} from "@/components/ui/drawer";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { CloseIcon, DragIcon, ResetIcon, SettingsIcon } from "./Icons";
import ConfirmModal from "./ConfirmModal";
@@ -33,6 +34,8 @@ export default function SortSettingModal({
rules = [],
onChangeRules,
onResetRules,
sortDisplayMode = "buttons",
onChangeSortDisplayMode,
}) {
const [localRules, setLocalRules] = useState(rules);
const [editingId, setEditingId] = useState(null);
@@ -120,6 +123,59 @@ export default function SortSettingModal({
: "pc-table-setting-body"
}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
marginBottom: 16,
}}
>
<h3
className="pc-table-setting-subtitle"
style={{ margin: 0, fontSize: 14 }}
>
排序形式
</h3>
<div style={{ display: "flex", justifyContent: "flex-end", marginLeft: "auto" }}>
<RadioGroup
value={sortDisplayMode}
onValueChange={(value) => onChangeSortDisplayMode?.(value)}
className="flex flex-row items-center gap-4"
>
<label
htmlFor="sort-display-mode-buttons"
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 13,
color: "var(--text)",
cursor: "pointer",
}}
>
<RadioGroupItem id="sort-display-mode-buttons" value="buttons" />
<span>按钮</span>
</label>
<label
htmlFor="sort-display-mode-dropdown"
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 13,
color: "var(--text)",
cursor: "pointer",
}}
>
<RadioGroupItem id="sort-display-mode-dropdown" value="dropdown" />
<span>下拉单选</span>
</label>
</RadioGroup>
</div>
</div>
<div
style={{
display: "flex",

View File

@@ -71,6 +71,13 @@ import packageJson from '../package.json';
import PcFundTable from './components/PcFundTable';
import MobileFundTable from './components/MobileFundTable';
import { useFundFuzzyMatcher } from './hooks/useFundFuzzyMatcher';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -129,6 +136,7 @@ export default function HomePage() {
const [settingsOpen, setSettingsOpen] = useState(false);
const [tempSeconds, setTempSeconds] = useState(60);
const [containerWidth, setContainerWidth] = useState(1200);
const [showMarketIndex, setShowMarketIndex] = useState(true);
useEffect(() => {
if (typeof window === 'undefined') return;
@@ -141,6 +149,9 @@ export default function HomePage() {
if (Number.isFinite(num)) {
setContainerWidth(Math.min(2000, Math.max(600, num)));
}
if (typeof parsed?.showMarketIndex === 'boolean') {
setShowMarketIndex(parsed.showMarketIndex);
}
} catch { }
}, []);
@@ -174,10 +185,12 @@ export default function HomePage() {
{ id: 'holding', label: '持有收益', enabled: true },
{ id: 'name', label: '基金名称', alias: '名称', enabled: true },
];
const SORT_DISPLAY_MODES = new Set(['buttons', 'dropdown']);
// 排序状态
const [sortBy, setSortBy] = useState('default'); // default, name, yield, yesterdayIncrease, holding, holdingAmount
const [sortOrder, setSortOrder] = useState('desc'); // asc | desc
const [sortDisplayMode, setSortDisplayMode] = useState('buttons'); // buttons | dropdown
const [isSortLoaded, setIsSortLoaded] = useState(false);
const [sortRules, setSortRules] = useState(DEFAULT_SORT_RULES);
const [sortSettingOpen, setSortSettingOpen] = useState(false);
@@ -199,6 +212,13 @@ export default function HomePage() {
if (parsed && Array.isArray(parsed.localSortRules)) {
rulesFromSettings = parsed.localSortRules;
}
if (
parsed &&
typeof parsed.localSortDisplayMode === 'string' &&
SORT_DISPLAY_MODES.has(parsed.localSortDisplayMode)
) {
setSortDisplayMode(parsed.localSortDisplayMode);
}
}
} catch {
// ignore
@@ -267,6 +287,7 @@ export default function HomePage() {
const next = {
...(parsed && typeof parsed === 'object' ? parsed : {}),
localSortRules: sortRules,
localSortDisplayMode: sortDisplayMode,
};
window.localStorage.setItem('customSettings', JSON.stringify(next));
// 更新后标记 customSettings 脏并触发云端同步
@@ -275,7 +296,7 @@ export default function HomePage() {
// ignore
}
}
}, [sortBy, sortOrder, sortRules, isSortLoaded]);
}, [sortBy, sortOrder, sortRules, sortDisplayMode, isSortLoaded]);
// 当用户关闭某个排序规则时,如果当前 sortBy 不再可用,则自动切换到第一个启用的规则
useEffect(() => {
@@ -2785,19 +2806,25 @@ export default function HomePage() {
await refreshAll(codes);
};
const saveSettings = (e, secondsOverride) => {
const saveSettings = (e, secondsOverride, showMarketIndexOverride) => {
e?.preventDefault?.();
const seconds = secondsOverride ?? tempSeconds;
const shouldShowMarketIndex = typeof showMarketIndexOverride === 'boolean' ? showMarketIndexOverride : showMarketIndex;
const ms = Math.max(30, Number(seconds)) * 1000;
setTempSeconds(Math.round(ms / 1000));
setRefreshMs(ms);
setShowMarketIndex(shouldShowMarketIndex);
storageHelper.setItem('refreshMs', String(ms));
const w = Math.min(2000, Math.max(600, Number(containerWidth) || 1200));
setContainerWidth(w);
try {
const raw = window.localStorage.getItem('customSettings');
const parsed = raw ? JSON.parse(raw) : {};
window.localStorage.setItem('customSettings', JSON.stringify({ ...parsed, pcContainerWidth: w }));
window.localStorage.setItem('customSettings', JSON.stringify({
...parsed,
pcContainerWidth: w,
showMarketIndex: shouldShowMarketIndex,
}));
triggerCustomSettingsSync();
} catch { }
setSettingsOpen(false);
@@ -3955,13 +3982,15 @@ export default function HomePage() {
</div>
</div>
</div>
<MarketIndexAccordion
navbarHeight={navbarHeight}
onHeightChange={setMarketIndexAccordionHeight}
isMobile={isMobile}
onCustomSettingsChange={triggerCustomSettingsSync}
refreshing={refreshing}
/>
{showMarketIndex && (
<MarketIndexAccordion
navbarHeight={navbarHeight}
onHeightChange={setMarketIndexAccordionHeight}
isMobile={isMobile}
onCustomSettingsChange={triggerCustomSettingsSync}
refreshing={refreshing}
/>
)}
<div className="grid">
<div className="col-12">
<div ref={filterBarRef} className="filter-bar" style={{ top: navbarHeight + marketIndexAccordionHeight, marginTop: 0, marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
@@ -4085,40 +4114,81 @@ export default function HomePage() {
<span className="muted">排序</span>
<SettingsIcon width="14" height="14" />
</button>
<div className="chips">
{sortRules.filter((s) => s.enabled).map((s) => (
<button
key={s.id}
className={`chip ${sortBy === s.id ? 'active' : ''}`}
onClick={() => {
if (sortBy === s.id) {
// 同一按钮重复点击,切换升序/降序
setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
} else {
// 切换到新的排序字段,默认用降序
setSortBy(s.id);
setSortOrder('desc');
}
{sortDisplayMode === 'dropdown' ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Select
value={sortBy}
onValueChange={(nextSortBy) => {
setSortBy(nextSortBy);
if (nextSortBy !== sortBy) setSortOrder('desc');
}}
style={{ height: '28px', fontSize: '12px', padding: '0 10px', display: 'flex', alignItems: 'center', gap: 4 }}
>
<span>{s.alias || s.label}</span>
{s.id !== 'default' && sortBy === s.id && (
<span
style={{
display: 'inline-flex',
flexDirection: 'column',
lineHeight: 1,
fontSize: '8px',
}}
>
<span style={{ opacity: sortOrder === 'asc' ? 1 : 0.3 }}></span>
<span style={{ opacity: sortOrder === 'desc' ? 1 : 0.3 }}></span>
</span>
)}
</button>
))}
</div>
<SelectTrigger
className="h-4 min-w-[110px] py-0 text-xs shadow-none"
style={{ background: 'var(--card-bg)', height: 36 }}
>
<SelectValue placeholder="选择排序规则" />
</SelectTrigger>
<SelectContent>
{sortRules.filter((s) => s.enabled).map((s) => (
<SelectItem key={s.id} value={s.id}>
{s.alias || s.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={sortOrder}
onValueChange={(value) => setSortOrder(value)}
>
<SelectTrigger
className="h-4 min-w-[84px] py-0 text-xs shadow-none"
style={{ background: 'var(--card-bg)', height: 36 }}
>
<SelectValue placeholder="排序方向" />
</SelectTrigger>
<SelectContent>
<SelectItem value="desc">降序</SelectItem>
<SelectItem value="asc">升序</SelectItem>
</SelectContent>
</Select>
</div>
) : (
<div className="chips">
{sortRules.filter((s) => s.enabled).map((s) => (
<button
key={s.id}
className={`chip ${sortBy === s.id ? 'active' : ''}`}
onClick={() => {
if (sortBy === s.id) {
// 同一按钮重复点击,切换升序/降序
setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'));
} else {
// 切换到新的排序字段,默认用降序
setSortBy(s.id);
setSortOrder('desc');
}
}}
style={{ height: '28px', fontSize: '12px', padding: '0 10px', display: 'flex', alignItems: 'center', gap: 4 }}
>
<span>{s.alias || s.label}</span>
{s.id !== 'default' && sortBy === s.id && (
<span
style={{
display: 'inline-flex',
flexDirection: 'column',
lineHeight: 1,
fontSize: '8px',
}}
>
<span style={{ opacity: sortOrder === 'asc' ? 1 : 0.3 }}></span>
<span style={{ opacity: sortOrder === 'desc' ? 1 : 0.3 }}></span>
</span>
)}
</button>
))}
</div>
)}
</div>
</div>
</div>
@@ -4740,6 +4810,8 @@ export default function HomePage() {
containerWidth={containerWidth}
setContainerWidth={setContainerWidth}
onResetContainerWidth={handleResetContainerWidth}
showMarketIndex={showMarketIndex}
setShowMarketIndex={setShowMarketIndex}
/>
)}
@@ -4792,6 +4864,8 @@ export default function HomePage() {
isMobile={isMobile}
rules={sortRules}
onChangeRules={setSortRules}
sortDisplayMode={sortDisplayMode}
onChangeSortDisplayMode={setSortDisplayMode}
onResetRules={() => setSortRules(DEFAULT_SORT_RULES)}
/>