From a7693b859103d9179d4b35c16675ba36d4a16d6a Mon Sep 17 00:00:00 2001 From: shishengliang Date: Sat, 24 Oct 2026 13:37:09 +0800 Subject: [PATCH] refactor(ui): share date range picker across asset and energy views --- src/components/ui/DateRangePicker.tsx | 120 ++++++++++++++++++ .../overview/FlowDateRangePicker.tsx | 120 ++---------------- .../energy/daily-range/DailyRangeControls.tsx | 12 +- 3 files changed, 140 insertions(+), 112 deletions(-) create mode 100644 src/components/ui/DateRangePicker.tsx diff --git a/src/components/ui/DateRangePicker.tsx b/src/components/ui/DateRangePicker.tsx new file mode 100644 index 0000000..1fd561f --- /dev/null +++ b/src/components/ui/DateRangePicker.tsx @@ -0,0 +1,120 @@ +import { useId, useRef, useState } from 'react'; +import { ArrowRight, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react'; +export interface DateRange { start: string; end: string } +interface DateRangePickerProps { + value: DateRange; + onChange: (range: DateRange) => void; + quickPicks: Array<{ label: string; range: DateRange }>; + label?: string; + testId?: string; + className?: string; + maxDays?: number; +} + +const weekdays = ['一', '二', '三', '四', '五', '六', '日']; +const ymd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; +const parseDate = (value: string) => new Date(`${value}T12:00:00`); +const dayCount = (start: string, end: string) => Math.round((Date.parse(`${end}T00:00:00Z`) - Date.parse(`${start}T00:00:00Z`)) / 86400000) + 1; + +export function DateRangePicker({ value, onChange, quickPicks, label = '统计日期', testId, className = '', maxDays = Infinity }: DateRangePickerProps) { + const titleId = useId(); + const dialog = useRef(null); + const [draft, setDraft] = useState(value); + const [selecting, setSelecting] = useState<'start' | 'end'>('start'); + const [month, setMonth] = useState(() => parseDate(value.start)); + const [hovered, setHovered] = useState(''); + const today = ymd(new Date()); + const count = draft.end ? dayCount(draft.start, draft.end) : 0; + const valid = count > 0 && count <= maxDays; + const monthStart = new Date(month.getFullYear(), month.getMonth(), 1); + const previewEnd = draft.end || hovered || draft.start; + const rangeStart = draft.start < previewEnd ? draft.start : previewEnd; + const rangeEnd = draft.start > previewEnd ? draft.start : previewEnd; + const open = () => { + setDraft(value); setSelecting('start'); setMonth(parseDate(value.start)); setHovered(''); + dialog.current?.showModal(); + }; + const choose = (date: string) => { + if (selecting === 'start') { + setDraft({ start: date, end: '' }); setSelecting('end'); + } else { + setDraft(date < draft.start ? { start: date, end: draft.start } : { start: draft.start, end: date }); + setSelecting('start'); + } + setHovered(''); + }; + const calendar = (offset: number) => { + const current = new Date(monthStart.getFullYear(), monthStart.getMonth() + offset, 1); + const year = current.getFullYear(), m = current.getMonth(); + const padding = (current.getDay() + 6) % 7; + const length = new Date(year, m + 1, 0).getDate(); + return
+
{year} 年 {m + 1} 月
+
+ {weekdays.map(day =>
{day}
)} + {Array.from({ length: padding }, (_, i) =>
)} + {Array.from({ length }, (_, i) => { + const date = ymd(new Date(year, m, i + 1)); + const endpoint = date === draft.start || date === draft.end; + const inRange = date >= rangeStart && date <= rangeEnd; + return ; + })} +
+
; + }; + return <> + + { if (e.target === e.currentTarget) dialog.current?.close(); }} + className="fixed inset-0 m-auto max-h-[90dvh] w-[calc(100%-24px)] max-w-[680px] overflow-y-auto rounded-3xl border-0 bg-white p-0 text-slate-700 shadow-2xl backdrop:bg-slate-950/30 backdrop:backdrop-blur-sm"> +
+
+

选择统计日期

先选开始日期,再选结束日期

+ +
+
+ {quickPicks.map(pick => )} +
+
+ {(['start', 'end'] as const).map(field => )} +
+
+ +
+ + +
+ +
+
setHovered('')}>{calendar(0)}{calendar(1)}
+
+
+ maxDays ? 'text-red-500' : 'text-slate-400'}`}>{!draft.end ? '请选择结束日期' : count > maxDays ? `最多选择 ${maxDays} 天` : `已选 ${count} 天(包含首尾)`} +
+ + +
+
+
+ ; +} diff --git a/src/modules/assets/components/overview/FlowDateRangePicker.tsx b/src/modules/assets/components/overview/FlowDateRangePicker.tsx index 5118728..f18aebd 100644 --- a/src/modules/assets/components/overview/FlowDateRangePicker.tsx +++ b/src/modules/assets/components/overview/FlowDateRangePicker.tsx @@ -1,118 +1,18 @@ -import { useRef, useState } from 'react'; -import { ArrowRight, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react'; +import { DateRangePicker } from '../../../../components/ui/DateRangePicker'; import { getRecentFlowRange, getWeeklyFlowRange, type DateRange } from '../../model'; -const weekdays = ['一', '二', '三', '四', '五', '六', '日']; const ymd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; -const parseDate = (value: string) => new Date(`${value}T12:00:00`); const shiftDay = (date: Date, offset: number) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + offset); -const dayCount = (start: string, end: string) => Math.round((Date.parse(`${end}T00:00:00Z`) - Date.parse(`${start}T00:00:00Z`)) / 86400000) + 1; export function FlowDateRangePicker({ value, onChange, dateLabel = '运维签章时间' }: { value: DateRange; onChange: (range: DateRange) => void; dateLabel?: string }) { - const dialog = useRef(null); - const [draft, setDraft] = useState(value); - const [selecting, setSelecting] = useState<'start' | 'end'>('start'); - const [month, setMonth] = useState(() => parseDate(value.start)); - const [hovered, setHovered] = useState(''); const today = ymd(new Date()); - const count = draft.end ? dayCount(draft.start, draft.end) : 0; - const valid = count > 0 && count <= 370; - const monthStart = new Date(month.getFullYear(), month.getMonth(), 1); - const previewEnd = draft.end || hovered || draft.start; - const rangeStart = draft.start < previewEnd ? draft.start : previewEnd; - const rangeEnd = draft.start > previewEnd ? draft.start : previewEnd; - const quickPicks = [ - { label: '今天', range: { start: today, end: today } }, - { label: '昨天', range: { start: ymd(shiftDay(new Date(), -1)), end: ymd(shiftDay(new Date(), -1)) } }, - { label: '业务周', range: getWeeklyFlowRange() }, - { label: '近 7 天', range: getRecentFlowRange() }, - { label: '近 30 天', range: { start: ymd(shiftDay(new Date(), -29)), end: today } }, - ]; - const open = () => { - setDraft(value); setSelecting('start'); setMonth(parseDate(value.start)); setHovered(''); - dialog.current?.showModal(); - }; - const choose = (date: string) => { - if (selecting === 'start') { - setDraft({ start: date, end: '' }); setSelecting('end'); - } else { - setDraft(date < draft.start ? { start: date, end: draft.start } : { start: draft.start, end: date }); - setSelecting('start'); - } - setHovered(''); - }; - const calendar = (offset: number) => { - const current = new Date(monthStart.getFullYear(), monthStart.getMonth() + offset, 1); - const year = current.getFullYear(), m = current.getMonth(); - const padding = (current.getDay() + 6) % 7; - const length = new Date(year, m + 1, 0).getDate(); - return
-
{year} 年 {m + 1} 月
-
- {weekdays.map(day =>
{day}
)} - {Array.from({ length: padding }, (_, i) =>
)} - {Array.from({ length }, (_, i) => { - const date = ymd(new Date(year, m, i + 1)); - const endpoint = date === draft.start || date === draft.end; - const inRange = date >= rangeStart && date <= rangeEnd; - return ; - })} -
-
; - }; - return <> - - { if (e.target === e.currentTarget) dialog.current?.close(); }} - className="fixed inset-0 m-auto max-h-[90dvh] w-[calc(100%-24px)] max-w-[680px] overflow-y-auto rounded-3xl border-0 bg-white p-0 text-slate-700 shadow-2xl backdrop:bg-slate-950/30 backdrop:backdrop-blur-sm"> -
-
-

选择统计日期

先选开始日期,再选结束日期

- -
-
- {quickPicks.map(pick => )} -
-
- {(['start', 'end'] as const).map(field => )} -
-
- -
- - -
- -
-
setHovered('')}>{calendar(0)}{calendar(1)}
-
-
- 370 ? 'text-red-500' : 'text-slate-400'}`}>{!draft.end ? '请选择结束日期' : count > 370 ? '最多选择 370 天' : `已选 ${count} 天(包含首尾)`} -
- - -
-
-
- ; + return ; } diff --git a/src/modules/energy/daily-range/DailyRangeControls.tsx b/src/modules/energy/daily-range/DailyRangeControls.tsx index 3df5f19..d060ffa 100644 --- a/src/modules/energy/daily-range/DailyRangeControls.tsx +++ b/src/modules/energy/daily-range/DailyRangeControls.tsx @@ -1,4 +1,5 @@ import { Truck } from 'lucide-react'; +import type { ReactNode } from 'react'; import { SurfaceCard } from '../../../components/ui/surface'; import type { CustomerType, DateQuickPick } from '../types'; import { QUICK_PICK_OPTIONS, type RangeMode } from './model'; @@ -11,6 +12,8 @@ interface DailyRangeControlsProps { onCustomPick: () => void; onDateRangeChange: (field: 'start' | 'end', value: string) => void; onCustomerChange: (customer: CustomerType) => void; + children?: ReactNode; + dateControl?: ReactNode; } export default function DailyRangeControls({ @@ -21,9 +24,12 @@ export default function DailyRangeControls({ onCustomPick, onDateRangeChange, onCustomerChange, + children, + dateControl, }: DailyRangeControlsProps) { return ( + {dateControl ?? <>
{QUICK_PICK_OPTIONS.map(opt => (
-
+ } + + {children ??
{(['lingniu', 'external'] as const).map(option => (
+
}
); }