feat(mileage): refine daily report controls

This commit is contained in:
kkfluous
2026-08-12 23:11:35 +08:00
parent bd0627d00f
commit 7fce6b8474
2 changed files with 142 additions and 15 deletions
+134 -13
View File
@@ -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 {