forked from boos/BoosAPI
feat: 签到记录新增日期筛选
This commit is contained in:
+351
-102
@@ -1,13 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { api } from '../api.js';
|
import { api } from "../api.js";
|
||||||
import { MobileCard, MobileField } from '../components/MobileCard.js';
|
import { MobileCard, MobileField } from "../components/MobileCard.js";
|
||||||
import { MobileDrawer } from '../components/MobileDrawer.js';
|
import { MobileDrawer } from "../components/MobileDrawer.js";
|
||||||
import { useToast } from '../components/Toast.js';
|
import { useToast } from "../components/Toast.js";
|
||||||
import { useIsMobile } from '../components/useIsMobile.js';
|
import { useIsMobile } from "../components/useIsMobile.js";
|
||||||
import { formatCheckinLogTime } from './helpers/checkinLogTime.js';
|
import {
|
||||||
import { tr } from '../i18n.js';
|
formatCheckinLogTime,
|
||||||
|
parseServerUtcDateTime,
|
||||||
|
} from "./helpers/checkinLogTime.js";
|
||||||
|
import { tr } from "../i18n.js";
|
||||||
|
|
||||||
type LogFilter = 'all' | 'success' | 'failed' | 'skipped';
|
type LogFilter = "all" | "success" | "failed" | "skipped";
|
||||||
|
|
||||||
type FailureReason = {
|
type FailureReason = {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -17,23 +20,131 @@ type FailureReason = {
|
|||||||
detailHint: string;
|
detailHint: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function pad2(value: number) {
|
||||||
|
return String(value).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTimeInputValue(value: Date) {
|
||||||
|
return `${value.getFullYear()}-${pad2(value.getMonth() + 1)}-${pad2(value.getDate())}T${pad2(value.getHours())}:${pad2(value.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTodayTimeRangeInput(now = new Date()): {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
} {
|
||||||
|
const start = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const end = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
from: formatDateTimeInputValue(start),
|
||||||
|
to: formatDateTimeInputValue(end),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLocalDateTimeInput(value: string): Date | null {
|
||||||
|
const text = value.trim();
|
||||||
|
if (!text) return null;
|
||||||
|
const parsed = new Date(text);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return null;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CheckinLog() {
|
export default function CheckinLog() {
|
||||||
const [logs, setLogs] = useState<any[]>([]);
|
const [logs, setLogs] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [triggering, setTriggering] = useState(false);
|
const [triggering, setTriggering] = useState(false);
|
||||||
const [filter, setFilter] = useState<LogFilter>('all');
|
const [filter, setFilter] = useState<LogFilter>("all");
|
||||||
const [expandedLogId, setExpandedLogId] = useState<number | null>(null);
|
const [expandedLogId, setExpandedLogId] = useState<number | null>(null);
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const isMobile = useIsMobile(768);
|
const isMobile = useIsMobile(768);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
function getStatus(log: any): "success" | "failed" | "skipped" {
|
||||||
|
const raw = (log.checkin_logs?.status || log.status || "failed") as string;
|
||||||
|
if (raw === "success" || raw === "skipped") return raw;
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreatedAtDate(log: any): Date | null {
|
||||||
|
const createdAt = log.checkin_logs?.createdAt || log.createdAt;
|
||||||
|
return parseServerUtcDateTime(createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialTimeRange = useMemo(() => getTodayTimeRangeInput(), []);
|
||||||
|
const [fromInput, setFromInput] = useState(initialTimeRange.from);
|
||||||
|
const [toInput, setToInput] = useState(initialTimeRange.to);
|
||||||
|
|
||||||
|
const fromRaw = useMemo(
|
||||||
|
() => parseLocalDateTimeInput(fromInput),
|
||||||
|
[fromInput],
|
||||||
|
);
|
||||||
|
const toRaw = useMemo(() => parseLocalDateTimeInput(toInput), [toInput]);
|
||||||
|
const fromMs = fromRaw?.getTime() ?? null;
|
||||||
|
const toMs = toRaw?.getTime() ?? null;
|
||||||
|
const toExclusiveMs = toMs === null ? null : toMs + 60_000;
|
||||||
|
|
||||||
|
const hasInvalidTimeRange = Boolean(
|
||||||
|
fromMs !== null && toMs !== null && fromMs >= toMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeFilteredLogs = useMemo(() => {
|
||||||
|
if (hasInvalidTimeRange) return [];
|
||||||
|
|
||||||
|
return logs.filter((log) => {
|
||||||
|
const createdAtDate = getCreatedAtDate(log);
|
||||||
|
if (!createdAtDate) return false;
|
||||||
|
|
||||||
|
const createdAtMs = createdAtDate.getTime();
|
||||||
|
if (fromMs !== null && createdAtMs < fromMs) return false;
|
||||||
|
if (toExclusiveMs !== null && createdAtMs >= toExclusiveMs) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [fromMs, hasInvalidTimeRange, logs, toExclusiveMs]);
|
||||||
|
|
||||||
|
const statusFilteredLogs = useMemo(
|
||||||
|
() =>
|
||||||
|
filter === "all"
|
||||||
|
? timeFilteredLogs
|
||||||
|
: timeFilteredLogs.filter((log) => getStatus(log) === filter),
|
||||||
|
[filter, timeFilteredLogs],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 兼容旧命名:后面渲染统一用 filtered
|
||||||
|
const filtered = statusFilteredLogs;
|
||||||
|
|
||||||
|
const countBy = useMemo(
|
||||||
|
() => (target: Exclude<LogFilter, "all">) =>
|
||||||
|
timeFilteredLogs.filter((log) => getStatus(log) === target).length,
|
||||||
|
[timeFilteredLogs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearTimeRange = () => {
|
||||||
|
setFromInput("");
|
||||||
|
setToInput("");
|
||||||
|
};
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await api.getCheckinLogs('limit=100');
|
const data = await api.getCheckinLogs("limit=100");
|
||||||
setLogs(Array.isArray(data) ? data : []);
|
setLogs(Array.isArray(data) ? data : []);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast.error(e.message || '加载签到记录失败');
|
toast.error(e.message || "加载签到记录失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -48,38 +159,28 @@ export default function CheckinLog() {
|
|||||||
try {
|
try {
|
||||||
const res = await api.triggerCheckinAll();
|
const res = await api.triggerCheckinAll();
|
||||||
if (res?.queued) {
|
if (res?.queued) {
|
||||||
toast.info(res.message || '已开始签到,请稍后查看签到记录');
|
toast.info(res.message || "已开始签到,请稍后查看签到记录");
|
||||||
} else {
|
} else {
|
||||||
toast.success(res?.message || '签到已执行');
|
toast.success(res?.message || "签到已执行");
|
||||||
}
|
}
|
||||||
await load();
|
await load();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast.error(e.message || '触发签到失败');
|
toast.error(e.message || "触发签到失败");
|
||||||
} finally {
|
} finally {
|
||||||
setTriggering(false);
|
setTriggering(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatus = (log: any): 'success' | 'failed' | 'skipped' => {
|
const statusLabel = (status: "success" | "failed" | "skipped") => {
|
||||||
const raw = (log.checkin_logs?.status || log.status || 'failed') as string;
|
if (status === "success") return "成功";
|
||||||
if (raw === 'success' || raw === 'skipped') return raw;
|
if (status === "skipped") return "跳过";
|
||||||
return 'failed';
|
return "失败";
|
||||||
};
|
};
|
||||||
|
|
||||||
const filtered = filter === 'all' ? logs : logs.filter((log) => getStatus(log) === filter);
|
const statusClass = (status: "success" | "failed" | "skipped") => {
|
||||||
|
if (status === "success") return "badge-success";
|
||||||
const countBy = (target: Exclude<LogFilter, 'all'>) => logs.filter((log) => getStatus(log) === target).length;
|
if (status === "skipped") return "badge-muted";
|
||||||
|
return "badge-error";
|
||||||
const statusLabel = (status: 'success' | 'failed' | 'skipped') => {
|
|
||||||
if (status === 'success') return '成功';
|
|
||||||
if (status === 'skipped') return '跳过';
|
|
||||||
return '失败';
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusClass = (status: 'success' | 'failed' | 'skipped') => {
|
|
||||||
if (status === 'success') return 'badge-success';
|
|
||||||
if (status === 'skipped') return 'badge-muted';
|
|
||||||
return 'badge-error';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFailureReason = (log: any): FailureReason | null => {
|
const getFailureReason = (log: any): FailureReason | null => {
|
||||||
@@ -88,14 +189,60 @@ export default function CheckinLog() {
|
|||||||
return reason;
|
return reason;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeRangeControls = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 10,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="proxy-logs-time-field">
|
||||||
|
<span>开始</span>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={fromInput}
|
||||||
|
max={toInput || undefined}
|
||||||
|
onChange={(e) => setFromInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="proxy-logs-time-field">
|
||||||
|
<span>结束</span>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={toInput}
|
||||||
|
min={fromInput || undefined}
|
||||||
|
onChange={(e) => setToInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost proxy-logs-filter-reset"
|
||||||
|
onClick={clearTimeRange}
|
||||||
|
>
|
||||||
|
清空筛选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const filterTabs = (
|
const filterTabs = (
|
||||||
<div className="tabs" style={{ marginBottom: 0 }}>
|
<div className="pill-tabs">
|
||||||
{(['all', 'success', 'failed', 'skipped'] as const).map((tab) => (
|
{[
|
||||||
<button key={tab} className={`tab ${filter === tab ? 'active' : ''}`} onClick={() => setFilter(tab)}>
|
{ key: "all" as const, label: "全部", count: timeFilteredLogs.length },
|
||||||
{tab === 'all' && `全部 (${logs.length})`}
|
{ key: "success" as const, label: "成功", count: countBy("success") },
|
||||||
{tab === 'success' && `成功 (${countBy('success')})`}
|
{ key: "failed" as const, label: "失败", count: countBy("failed") },
|
||||||
{tab === 'failed' && `失败 (${countBy('failed')})`}
|
{ key: "skipped" as const, label: "跳过", count: countBy("skipped") },
|
||||||
{tab === 'skipped' && `跳过 (${countBy('skipped')})`}
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
className={`pill-tab ${filter === tab.key ? "active" : ""}`}
|
||||||
|
onClick={() => setFilter(tab.key)}
|
||||||
|
>
|
||||||
|
{tab.label}{" "}
|
||||||
|
<span style={{ fontVariantNumeric: "tabular-nums", opacity: 0.7 }}>
|
||||||
|
{tab.count}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +251,7 @@ export default function CheckinLog() {
|
|||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2 className="page-title">{tr('签到记录')}</h2>
|
<h2 className="page-title">{tr("签到记录")}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={handleTriggerAll}
|
onClick={handleTriggerAll}
|
||||||
disabled={triggering}
|
disabled={triggering}
|
||||||
@@ -116,7 +263,7 @@ export default function CheckinLog() {
|
|||||||
触发中...
|
触发中...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'运行所有签到'
|
"运行所有签到"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,27 +274,67 @@ export default function CheckinLog() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost"
|
className="btn btn-ghost"
|
||||||
style={{ border: '1px solid var(--color-border)' }}
|
style={{ border: "1px solid var(--color-border)" }}
|
||||||
onClick={() => setShowFilters(true)}
|
onClick={() => setShowFilters(true)}
|
||||||
>
|
>
|
||||||
筛选
|
筛选
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MobileDrawer open={showFilters} onClose={() => setShowFilters(false)}>
|
<MobileDrawer
|
||||||
<div className="mobile-filter-panel">
|
open={showFilters}
|
||||||
|
onClose={() => setShowFilters(false)}
|
||||||
|
>
|
||||||
|
<div className="mobile-filter-panel" style={{ gap: 12 }}>
|
||||||
|
{timeRangeControls}
|
||||||
|
{hasInvalidTimeRange && (
|
||||||
|
<div className="alert alert-error">
|
||||||
|
结束时间必须晚于开始时间
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{filterTabs}
|
{filterTabs}
|
||||||
</div>
|
</div>
|
||||||
</MobileDrawer>
|
</MobileDrawer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
filterTabs
|
<div className="toolbar" style={{ marginBottom: "12px" }}>
|
||||||
|
<div style={{ minWidth: 280 }}>{filterTabs}</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: "0 0 auto",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timeRangeControls}
|
||||||
|
</div>
|
||||||
|
{hasInvalidTimeRange && (
|
||||||
|
<div className="alert alert-error" style={{ width: "100%" }}>
|
||||||
|
结束时间必须晚于开始时间
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card" style={{ overflowX: 'auto', borderTopLeftRadius: 0, borderTopRightRadius: 0 }}>
|
<div
|
||||||
|
className="card"
|
||||||
|
style={{
|
||||||
|
overflowX: "auto",
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<div key={i} style={{ display: 'flex', gap: 16 }}>
|
<div key={i} style={{ display: "flex", gap: 16 }}>
|
||||||
<div className="skeleton" style={{ width: 120, height: 16 }} />
|
<div className="skeleton" style={{ width: 120, height: 16 }} />
|
||||||
<div className="skeleton" style={{ width: 80, height: 16 }} />
|
<div className="skeleton" style={{ width: 80, height: 16 }} />
|
||||||
<div className="skeleton" style={{ width: 120, height: 16 }} />
|
<div className="skeleton" style={{ width: 120, height: 16 }} />
|
||||||
@@ -162,49 +349,74 @@ export default function CheckinLog() {
|
|||||||
{filtered.map((log: any) => {
|
{filtered.map((log: any) => {
|
||||||
const status = getStatus(log);
|
const status = getStatus(log);
|
||||||
const reason = getFailureReason(log);
|
const reason = getFailureReason(log);
|
||||||
const isExpanded = expandedLogId === (log.checkin_logs?.id || log.id);
|
const isExpanded =
|
||||||
|
expandedLogId === (log.checkin_logs?.id || log.id);
|
||||||
const logId = log.checkin_logs?.id || log.id;
|
const logId = log.checkin_logs?.id || log.id;
|
||||||
return (
|
return (
|
||||||
<MobileCard
|
<MobileCard
|
||||||
key={logId}
|
key={logId}
|
||||||
title={log.accounts?.username || '未知'}
|
title={log.accounts?.username || "未知"}
|
||||||
actions={(
|
actions={
|
||||||
<span className={`badge ${statusClass(status)}`} style={{ fontSize: 10 }}>
|
<span
|
||||||
|
className={`badge ${statusClass(status)}`}
|
||||||
|
style={{ fontSize: 10 }}
|
||||||
|
>
|
||||||
{statusLabel(status)}
|
{statusLabel(status)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
}
|
||||||
>
|
>
|
||||||
<MobileField label="时间" value={formatCheckinLogTime(log.checkin_logs?.createdAt || log.createdAt)} />
|
<MobileField
|
||||||
|
label="时间"
|
||||||
|
value={formatCheckinLogTime(
|
||||||
|
log.checkin_logs?.createdAt || log.createdAt,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<MobileField
|
<MobileField
|
||||||
label="站点"
|
label="站点"
|
||||||
value={log.sites?.url ? (
|
value={
|
||||||
<a
|
log.sites?.url ? (
|
||||||
href={log.sites.url}
|
<a
|
||||||
target="_blank"
|
href={log.sites.url}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="badge-link"
|
rel="noopener noreferrer"
|
||||||
>
|
className="badge-link"
|
||||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
>
|
||||||
{log.sites?.name || '-'}
|
<span
|
||||||
|
className="badge badge-muted"
|
||||||
|
style={{ fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{log.sites?.name || "-"}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="badge badge-muted"
|
||||||
|
style={{ fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{log.sites?.name || "-"}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
)
|
||||||
) : (
|
}
|
||||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
|
||||||
{log.sites?.name || '-'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<MobileField
|
<MobileField
|
||||||
label="分类"
|
label="分类"
|
||||||
value={reason ? (
|
value={
|
||||||
<span className="badge badge-info" data-tooltip={reason.detailHint}>
|
reason ? (
|
||||||
{reason.title}
|
<span
|
||||||
</span>
|
className="badge badge-info"
|
||||||
) : (
|
data-tooltip={reason.detailHint}
|
||||||
<span className="badge badge-muted">-</span>
|
>
|
||||||
)}
|
{reason.title}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="badge badge-muted">-</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MobileField
|
||||||
|
label="奖励"
|
||||||
|
value={log.checkin_logs?.reward || "-"}
|
||||||
/>
|
/>
|
||||||
<MobileField label="奖励" value={log.checkin_logs?.reward || '-'} />
|
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<div className="mobile-card-extra">
|
<div className="mobile-card-extra">
|
||||||
<MobileField
|
<MobileField
|
||||||
@@ -213,7 +425,7 @@ export default function CheckinLog() {
|
|||||||
/>
|
/>
|
||||||
<MobileField
|
<MobileField
|
||||||
label="建议"
|
label="建议"
|
||||||
value={reason?.actionHint || '-'}
|
value={reason?.actionHint || "-"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -221,9 +433,11 @@ export default function CheckinLog() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-link"
|
className="btn btn-link"
|
||||||
onClick={() => setExpandedLogId(isExpanded ? null : logId)}
|
onClick={() =>
|
||||||
|
setExpandedLogId(isExpanded ? null : logId)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isExpanded ? '收起' : '详情'}
|
{isExpanded ? "收起" : "详情"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
@@ -250,11 +464,18 @@ export default function CheckinLog() {
|
|||||||
const reason = getFailureReason(log);
|
const reason = getFailureReason(log);
|
||||||
return (
|
return (
|
||||||
<tr key={log.checkin_logs?.id || log.id}>
|
<tr key={log.checkin_logs?.id || log.id}>
|
||||||
<td style={{ fontSize: 12, whiteSpace: 'nowrap' }}>
|
<td style={{ fontSize: 12, whiteSpace: "nowrap" }}>
|
||||||
{formatCheckinLogTime(log.checkin_logs?.createdAt || log.createdAt)}
|
{formatCheckinLogTime(
|
||||||
|
log.checkin_logs?.createdAt || log.createdAt,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ fontWeight: 600, color: 'var(--color-text-primary)' }}>
|
<td
|
||||||
{log.accounts?.username || '未知'}
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{log.accounts?.username || "未知"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{log.sites?.url ? (
|
{log.sites?.url ? (
|
||||||
@@ -264,22 +485,33 @@ export default function CheckinLog() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="badge-link"
|
className="badge-link"
|
||||||
>
|
>
|
||||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
<span
|
||||||
{log.sites?.name || '-'}
|
className="badge badge-muted"
|
||||||
|
style={{ fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{log.sites?.name || "-"}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
<span
|
||||||
{log.sites?.name || '-'}
|
className="badge badge-muted"
|
||||||
|
style={{ fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{log.sites?.name || "-"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`badge ${statusClass(status)}`}>{statusLabel(status)}</span>
|
<span className={`badge ${statusClass(status)}`}>
|
||||||
|
{statusLabel(status)}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{reason ? (
|
{reason ? (
|
||||||
<span className="badge badge-info" data-tooltip={reason.detailHint}>
|
<span
|
||||||
|
className="badge badge-info"
|
||||||
|
data-tooltip={reason.detailHint}
|
||||||
|
>
|
||||||
{reason.title}
|
{reason.title}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -287,26 +519,33 @@ export default function CheckinLog() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ maxWidth: 360 }}>
|
<td style={{ maxWidth: 360 }}>
|
||||||
<span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
<span
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{log.checkin_logs?.message || log.message}
|
{log.checkin_logs?.message || log.message}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ maxWidth: 220 }}>
|
<td style={{ maxWidth: 220 }}>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: "block",
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
color: 'var(--color-text-secondary)',
|
color: "var(--color-text-secondary)",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
}}
|
}}
|
||||||
data-tooltip={reason?.detailHint || ''}
|
data-tooltip={reason?.detailHint || ""}
|
||||||
>
|
>
|
||||||
{reason?.actionHint || '-'}
|
{reason?.actionHint || "-"}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{log.checkin_logs?.reward || '-'}</td>
|
<td>{log.checkin_logs?.reward || "-"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -316,8 +555,18 @@ export default function CheckinLog() {
|
|||||||
|
|
||||||
{!loading && filtered.length === 0 && (
|
{!loading && filtered.length === 0 && (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<svg className="empty-state-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
className="empty-state-icon"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1}
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="empty-state-title">暂无签到记录</div>
|
<div className="empty-state-title">暂无签到记录</div>
|
||||||
<div className="empty-state-desc">点击“运行所有签到”开始执行</div>
|
<div className="empty-state-desc">点击“运行所有签到”开始执行</div>
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ function parseServerUtcDate(value: string | null | undefined): { date: Date | nu
|
|||||||
return { date, raw };
|
return { date, raw };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseServerUtcDateTime(value: string | null | undefined): Date | null {
|
||||||
|
return parseServerUtcDate(value).date;
|
||||||
|
}
|
||||||
|
|
||||||
function formatWithParts(
|
function formatWithParts(
|
||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
options: Intl.DateTimeFormatOptions,
|
options: Intl.DateTimeFormatOptions,
|
||||||
|
|||||||
Reference in New Issue
Block a user