feat(mileage): refine daily report controls
This commit is contained in:
@@ -83,12 +83,132 @@ function dateShift(date: string, days: number): string {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function monthShift(month: string, months: number): string {
|
||||
const value = new Date(`${month}-01T00:00:00Z`);
|
||||
value.setUTCMonth(value.getUTCMonth() + months);
|
||||
return value.toISOString().slice(0, 7);
|
||||
}
|
||||
|
||||
function reportDateLabel(value: string): string {
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
}).format(date).replace(/日(?=周)/, '日 ');
|
||||
}
|
||||
|
||||
function initialReportDate(): string {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const value = params.get('mileageReportDate');
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value || '') ? value! : shanghaiYmd(-1);
|
||||
}
|
||||
|
||||
function ReportDatePicker({
|
||||
value,
|
||||
maxDate,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
maxDate: string;
|
||||
onChange: (date: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [month, setMonth] = useState(value.slice(0, 7));
|
||||
const monthStart = new Date(`${month}-01T00:00:00Z`);
|
||||
const firstGridDate = new Date(monthStart);
|
||||
firstGridDate.setUTCDate(1 - monthStart.getUTCDay());
|
||||
const days = Array.from({ length: 42 }, (_, index) => {
|
||||
const date = new Date(firstGridDate);
|
||||
date.setUTCDate(firstGridDate.getUTCDate() + index);
|
||||
return date;
|
||||
});
|
||||
const weekdayLabels = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
const monthLabel = new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'UTC',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
}).format(monthStart);
|
||||
|
||||
return (
|
||||
<div className="relative min-w-0 flex-1 sm:flex-none">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMonth(value.slice(0, 7));
|
||||
setOpen(current => !current);
|
||||
}}
|
||||
className="flex h-9 w-full min-w-[190px] items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 text-left text-xs font-black text-slate-700 transition-colors hover:border-blue-200 hover:bg-blue-50/50 sm:w-[208px]"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="dialog"
|
||||
aria-label="选择日报日期"
|
||||
>
|
||||
<CalendarDays size={15} className="shrink-0 text-blue-500" />
|
||||
<span className="min-w-0 flex-1 truncate">{reportDateLabel(value)}</span>
|
||||
<ChevronDown size={14} className={`shrink-0 text-slate-400 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<div role="dialog" aria-label="日报日期日历" className="absolute right-0 top-full z-40 mt-2 w-[308px] rounded-xl border border-slate-200 bg-white p-3 shadow-xl">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMonth(current => monthShift(current, -1))}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100"
|
||||
title="上个月"
|
||||
aria-label="上个月"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<div className="text-sm font-black text-slate-900">{monthLabel}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMonth(current => monthShift(current, 1))}
|
||||
disabled={month >= maxDate.slice(0, 7)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
title="下个月"
|
||||
aria-label="下个月"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1 text-center">
|
||||
{weekdayLabels.map(day => <span key={day} className="py-1 text-[10px] font-black text-slate-400">{day}</span>)}
|
||||
{days.map(date => {
|
||||
const dateValue = date.toISOString().slice(0, 10);
|
||||
const inMonth = dateValue.slice(0, 7) === month;
|
||||
const selected = dateValue === value;
|
||||
const disabled = dateValue > maxDate;
|
||||
return (
|
||||
<button
|
||||
key={dateValue}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
onChange(dateValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`flex h-9 items-center justify-center rounded-md text-xs font-black transition-colors ${
|
||||
selected
|
||||
? 'bg-blue-600 text-white shadow-sm'
|
||||
: inMonth
|
||||
? 'text-slate-700 hover:bg-blue-50 hover:text-blue-600'
|
||||
: 'text-slate-300 hover:bg-slate-50'
|
||||
} disabled:cursor-not-allowed disabled:text-slate-200 disabled:hover:bg-transparent`}
|
||||
>
|
||||
{date.getUTCDate()}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function updateReportDateInUrl(date: string): void {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('mileageReportDate', date);
|
||||
@@ -711,7 +831,7 @@ function roundMileageForDisplay(value: number): number {
|
||||
}
|
||||
|
||||
export default function DailyReportView() {
|
||||
const maxDate = shanghaiYmd(-1);
|
||||
const maxDate = shanghaiYmd();
|
||||
const [selectedDate, setSelectedDate] = useState(initialReportDate);
|
||||
const [report, setReport] = useState<DailyMileageReport | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -777,10 +897,12 @@ export default function DailyReportView() {
|
||||
<p className="mt-1 text-[10px] font-bold text-slate-400">
|
||||
{fmtDate(report.reportDate)} · {report.source === 'XLSX_IMPORT'
|
||||
? '人工台账归档'
|
||||
: report.status === 'ARCHIVED'
|
||||
? `归档于 ${fmtDateTime(report.generatedAt)}`
|
||||
: `仪表数据更新至 ${fmtDateTime(report.sourceUpdatedAt)}`}
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] font-bold text-slate-500">
|
||||
统计口径:每天 00:30 自动汇总前一日里程
|
||||
统计周期:自然日 · 汇总频率:每日 00:30 汇总前一日里程
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -794,17 +916,16 @@ export default function DailyReportView() {
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<label className="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 sm:flex-none">
|
||||
<CalendarDays size={15} className="shrink-0 text-blue-500" />
|
||||
<input
|
||||
type="date"
|
||||
max={maxDate}
|
||||
value={selectedDate}
|
||||
onChange={event => event.target.value && setSelectedDate(event.target.value)}
|
||||
className="min-w-0 border-0 bg-transparent text-xs font-black text-slate-700 outline-none"
|
||||
aria-label="日报日期"
|
||||
/>
|
||||
</label>
|
||||
<ReportDatePicker value={selectedDate} maxDate={maxDate} onChange={setSelectedDate} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedDate(maxDate)}
|
||||
disabled={selectedDate === maxDate}
|
||||
className="flex h-9 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white px-2.5 text-[11px] font-black text-blue-600 hover:border-blue-200 hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-35"
|
||||
title="回到今天"
|
||||
>
|
||||
今天
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedDate(dateShift(selectedDate, 1))}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { hasDailyReportSnapshot } from './daily-report-store.js';
|
||||
|
||||
const FIRST_CHECK_DELAY_MS = 15 * 1000;
|
||||
const SUMMARY_MINUTE_OF_DAY = 30;
|
||||
const SUMMARY_WINDOW_MINUTES = 5;
|
||||
let archiving = false;
|
||||
|
||||
function shanghaiMinuteOfDay(): number {
|
||||
@@ -39,7 +40,12 @@ function millisecondsUntilNextSummary(): number {
|
||||
}
|
||||
|
||||
async function checkAndArchive(): Promise<void> {
|
||||
if (archiving || shanghaiMinuteOfDay() < SUMMARY_MINUTE_OF_DAY) return;
|
||||
const minuteOfDay = shanghaiMinuteOfDay();
|
||||
if (
|
||||
archiving
|
||||
|| minuteOfDay < SUMMARY_MINUTE_OF_DAY
|
||||
|| minuteOfDay >= SUMMARY_MINUTE_OF_DAY + SUMMARY_WINDOW_MINUTES
|
||||
) return;
|
||||
const reportDate = previousShanghaiDate();
|
||||
archiving = true;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user