From 4424d9d20537fa7fb7ffa5ce966cc3328aa84388 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 19 May 2026 00:04:02 +0800 Subject: [PATCH] fix: ExportDate Page --- .../Export/ExportDateRangeDialog.tsx | 8 +- .../Export/ExportDefaultsSettingsForm.scss | 107 ++++++++- .../Export/ExportDefaultsSettingsForm.tsx | 219 ++++++++++++++---- src/pages/ExportPage.scss | 13 +- src/pages/ExportPage.tsx | 2 +- 5 files changed, 293 insertions(+), 56 deletions(-) diff --git a/src/components/Export/ExportDateRangeDialog.tsx b/src/components/Export/ExportDateRangeDialog.tsx index c858472..497251c 100644 --- a/src/components/Export/ExportDateRangeDialog.tsx +++ b/src/components/Export/ExportDateRangeDialog.tsx @@ -551,7 +551,13 @@ export function ExportDateRangeDialog({ if (!open) return null return createPortal( -
+
{ + event.stopPropagation() + onClose() + }} + >
event.stopPropagation()}>

{title}

diff --git a/src/components/Export/ExportDefaultsSettingsForm.scss b/src/components/Export/ExportDefaultsSettingsForm.scss index e237673..3bbed6a 100644 --- a/src/components/Export/ExportDefaultsSettingsForm.scss +++ b/src/components/Export/ExportDefaultsSettingsForm.scss @@ -232,6 +232,7 @@ display: inline-flex; align-items: center; gap: 5px; + position: relative; margin-bottom: 0; font-size: 13px; line-height: 1; @@ -239,11 +240,55 @@ color: var(--text-primary); cursor: pointer; white-space: nowrap; + transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease; } input[type='checkbox'] { margin: 0; - accent-color: var(--primary); + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; + + &:checked + .media-default-check { + border-color: var(--primary); + background: var(--primary); + } + + &:checked + .media-default-check::after { + opacity: 1; + transform: rotate(-45deg) scale(1); + } + + &:focus-visible + .media-default-check { + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); + } + } + + .media-default-check { + width: 16px; + height: 16px; + flex: 0 0 16px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, var(--text-tertiary)); + border-radius: 5px; + background: var(--bg-primary); + display: inline-flex; + align-items: center; + justify-content: center; + transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease; + + &::after { + content: ''; + width: 8px; + height: 5px; + border-left: 2px solid #fff; + border-bottom: 2px solid #fff; + opacity: 0; + transform: rotate(-45deg) scale(0.72); + transform-origin: center; + transition: opacity 0.16s ease, transform 0.16s ease; + } } } @@ -568,10 +613,70 @@ border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-primary); + + &:hover { + border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 5%, var(--bg-primary)); + } + + &:has(input:checked) { + border-color: color-mix(in srgb, var(--primary) 55%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-primary)); + color: var(--primary); + } } } } +.select-dropdown-floating { + background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary)); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 6px; + box-shadow: var(--shadow-md); + overflow-y: auto; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + + .select-option { + width: 100%; + text-align: left; + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: none; + border-radius: 10px; + background: transparent; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; + color: var(--text-primary); + font-size: 14px; + + &:hover { + background: var(--bg-tertiary); + } + + &.active { + background: color-mix(in srgb, var(--primary) 12%, transparent); + color: var(--primary); + } + } + + .option-label { + font-weight: 500; + } + + .option-desc { + font-size: 12px; + color: var(--text-tertiary); + } + + .select-option.active .option-desc { + color: var(--primary); + } +} + @media (max-width: 980px) { .export-defaults-settings-form.layout-split { .form-group { diff --git a/src/components/Export/ExportDefaultsSettingsForm.tsx b/src/components/Export/ExportDefaultsSettingsForm.tsx index 1e3d8b1..6b527d8 100644 --- a/src/components/Export/ExportDefaultsSettingsForm.tsx +++ b/src/components/Export/ExportDefaultsSettingsForm.tsx @@ -1,4 +1,6 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import type { CSSProperties } from 'react' +import { createPortal } from 'react-dom' import { ChevronDown } from 'lucide-react' import * as configService from '../../services/config' import { ExportDateRangeDialog } from './ExportDateRangeDialog' @@ -56,6 +58,48 @@ const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }> return options.find((option) => option.value === value)?.label ?? value } +interface SelectDropdownPlacement { + left: number + width: number + maxHeight: number + top?: number + bottom?: number +} + +const resolveSelectDropdownPlacement = (anchor: HTMLElement | null): SelectDropdownPlacement | null => { + if (!anchor || typeof window === 'undefined') return null + + const rect = anchor.getBoundingClientRect() + const viewportWidth = window.innerWidth || document.documentElement.clientWidth + const viewportHeight = window.innerHeight || document.documentElement.clientHeight + const viewportMargin = 12 + const dropdownGap = 6 + const minDropdownHeight = 128 + const availableWidth = Math.max(160, viewportWidth - viewportMargin * 2) + const width = Math.min(Math.max(rect.width, 220), availableWidth) + const left = Math.max(viewportMargin, Math.min(rect.left, viewportWidth - width - viewportMargin)) + const spaceBelow = Math.max(0, viewportHeight - rect.bottom - viewportMargin - dropdownGap) + const spaceAbove = Math.max(0, rect.top - viewportMargin - dropdownGap) + const shouldOpenAbove = spaceBelow < minDropdownHeight && spaceAbove > spaceBelow + const availableHeight = shouldOpenAbove ? spaceAbove : spaceBelow + const maxHeight = Math.max(96, Math.min(320, availableHeight)) + + return shouldOpenAbove + ? { left, width, maxHeight, bottom: viewportHeight - rect.top + dropdownGap } + : { left, width, maxHeight, top: rect.bottom + dropdownGap } +} + +const getSelectDropdownStyle = (placement: SelectDropdownPlacement): CSSProperties => ({ + position: 'fixed', + top: placement.top, + bottom: placement.bottom, + left: placement.left, + right: 'auto', + width: placement.width, + maxHeight: placement.maxHeight, + zIndex: 9300 +}) + export function ExportDefaultsSettingsForm({ onNotify, onDefaultsChanged, @@ -66,6 +110,10 @@ export function ExportDefaultsSettingsForm({ const [isExportDateRangeDialogOpen, setIsExportDateRangeDialogOpen] = useState(false) const exportExcelColumnsDropdownRef = useRef(null) const exportFileNamingModeDropdownRef = useRef(null) + const exportExcelColumnsMenuRef = useRef(null) + const exportFileNamingModeMenuRef = useRef(null) + const [exportExcelColumnsPlacement, setExportExcelColumnsPlacement] = useState(null) + const [exportFileNamingModePlacement, setExportFileNamingModePlacement] = useState(null) const [exportDefaultFormat, setExportDefaultFormat] = useState('excel') const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true) @@ -122,10 +170,20 @@ export function ExportDefaultsSettingsForm({ useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as Node - if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) { + if ( + showExportExcelColumnsSelect && + exportExcelColumnsDropdownRef.current && + !exportExcelColumnsDropdownRef.current.contains(target) && + !exportExcelColumnsMenuRef.current?.contains(target) + ) { setShowExportExcelColumnsSelect(false) } - if (showExportFileNamingModeSelect && exportFileNamingModeDropdownRef.current && !exportFileNamingModeDropdownRef.current.contains(target)) { + if ( + showExportFileNamingModeSelect && + exportFileNamingModeDropdownRef.current && + !exportFileNamingModeDropdownRef.current.contains(target) && + !exportFileNamingModeMenuRef.current?.contains(target) + ) { setShowExportFileNamingModeSelect(false) } } @@ -134,6 +192,30 @@ export function ExportDefaultsSettingsForm({ return () => document.removeEventListener('mousedown', handleClickOutside) }, [showExportExcelColumnsSelect, showExportFileNamingModeSelect]) + const updateSelectDropdownPlacements = useCallback(() => { + if (showExportExcelColumnsSelect) { + setExportExcelColumnsPlacement(resolveSelectDropdownPlacement(exportExcelColumnsDropdownRef.current)) + } + if (showExportFileNamingModeSelect) { + setExportFileNamingModePlacement(resolveSelectDropdownPlacement(exportFileNamingModeDropdownRef.current)) + } + }, [showExportExcelColumnsSelect, showExportFileNamingModeSelect]) + + useEffect(() => { + if (!showExportExcelColumnsSelect) setExportExcelColumnsPlacement(null) + if (!showExportFileNamingModeSelect) setExportFileNamingModePlacement(null) + if (!showExportExcelColumnsSelect && !showExportFileNamingModeSelect) return + + updateSelectDropdownPlacements() + window.addEventListener('resize', updateSelectDropdownPlacements) + document.addEventListener('scroll', updateSelectDropdownPlacements, true) + + return () => { + window.removeEventListener('resize', updateSelectDropdownPlacements) + document.removeEventListener('scroll', updateSelectDropdownPlacements, true) + } + }, [showExportExcelColumnsSelect, showExportFileNamingModeSelect, updateSelectDropdownPlacements]) + const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full' const exportDateRangeLabel = useMemo(() => getExportDateRangeLabel(exportDefaultDateRange), [exportDefaultDateRange]) const exportExcelColumnsLabel = useMemo(() => getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue), [exportExcelColumnsValue]) @@ -143,6 +225,73 @@ export function ExportDefaultsSettingsForm({ onNotify?.(text, success) } + const fileNamingModeDropdown = showExportFileNamingModeSelect && exportFileNamingModePlacement + ? createPortal( +
event.stopPropagation()} + > + {exportFileNamingModeOptions.map((option) => ( + + ))} +
, + document.body + ) + : null + + const excelColumnsDropdown = showExportExcelColumnsSelect && exportExcelColumnsPlacement + ? createPortal( +
event.stopPropagation()} + > + {exportExcelColumnOptions.map((option) => ( + + ))} +
, + document.body + ) + : null + return (
@@ -273,6 +422,8 @@ export function ExportDefaultsSettingsForm({ - {showExportFileNamingModeSelect && ( -
- {exportFileNamingModeOptions.map((option) => ( - - ))} -
- )} + {fileNamingModeDropdown}
@@ -317,6 +448,8 @@ export function ExportDefaultsSettingsForm({ - {showExportExcelColumnsSelect && ( -
- {exportExcelColumnOptions.map((option) => ( - - ))} -
- )} + {excelColumnsDropdown}
@@ -371,7 +483,8 @@ export function ExportDefaultsSettingsForm({ notify(`已${e.target.checked ? '开启' : '关闭'}默认导出图片`, true) }} /> - 图片 +