's'
This commit is contained in:
123
app/lib/dates.ts
123
app/lib/dates.ts
@@ -6,6 +6,7 @@ const TZ = "Asia/Shanghai";
|
||||
|
||||
/**
|
||||
* 获取中国时区下的日期数字(月、日、星期)
|
||||
* 无论服务器/客户端在何地,均返回中国公历
|
||||
*/
|
||||
function getChinaDateParts(): { year: number; month: number; day: number; weekday: number } {
|
||||
const now = new Date();
|
||||
@@ -28,6 +29,27 @@ function getChinaDateParts(): { year: number; month: number; day: number; weekda
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前中国公历时间字符串,用于上传
|
||||
* 格式:YYYY-MM-DD HH:mm:ss(中国时区)
|
||||
*/
|
||||
export function getNowChinaStr(): string {
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: TZ,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
const parts = formatter.formatToParts(new Date());
|
||||
const pad = (s: string) => s.padStart(2, "0");
|
||||
const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "0";
|
||||
return `${get("year")}-${pad(get("month"))}-${pad(get("day"))} ${pad(get("hour"))}:${pad(get("minute"))}:${pad(get("second"))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为中文日期:M月D日 周X
|
||||
*/
|
||||
@@ -36,48 +58,75 @@ function formatDateCN(month: number, day: number, weekday: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个周六、周日的日期(中国日历)
|
||||
* session-1 对应周六,session-2 对应周日
|
||||
* 获取下一个周六、周日的日期(中国公历)
|
||||
* 使用 +08:00 显式构造,避免服务器/客户端时区影响
|
||||
*/
|
||||
export function getNextWeekendDates(): { saturday: string; sunday: string } {
|
||||
const { year, month, day, weekday } = getChinaDateParts();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const todayStr = `${year}-${pad(month)}-${pad(day)}T12:00:00+08:00`;
|
||||
const today = new Date(todayStr);
|
||||
|
||||
let satM = month;
|
||||
let satD = day;
|
||||
let satW = 6;
|
||||
let sunM = month;
|
||||
let sunD = day;
|
||||
let sunW = 0;
|
||||
|
||||
if (weekday === 0) {
|
||||
// 今天周日:下周六 +6 天,下周日 +7 天
|
||||
const d = new Date(year, month - 1, day);
|
||||
d.setDate(d.getDate() + 6);
|
||||
satM = d.getMonth() + 1;
|
||||
satD = d.getDate();
|
||||
d.setDate(d.getDate() + 1);
|
||||
sunM = d.getMonth() + 1;
|
||||
sunD = d.getDate();
|
||||
} else if (weekday === 6) {
|
||||
// 今天周六:明天周日
|
||||
const d = new Date(year, month - 1, day);
|
||||
d.setDate(d.getDate() + 1);
|
||||
sunM = d.getMonth() + 1;
|
||||
sunD = d.getDate();
|
||||
} else {
|
||||
// 周一至周五
|
||||
const daysToSat = 6 - weekday;
|
||||
const d = new Date(year, month - 1, day);
|
||||
d.setDate(d.getDate() + daysToSat);
|
||||
satM = d.getMonth() + 1;
|
||||
satD = d.getDate();
|
||||
d.setDate(d.getDate() + 1);
|
||||
sunM = d.getMonth() + 1;
|
||||
sunD = d.getDate();
|
||||
}
|
||||
const daysToSat = weekday === 0 ? 6 : weekday === 6 ? 0 : 6 - weekday;
|
||||
const saturday = new Date(today);
|
||||
saturday.setUTCDate(saturday.getUTCDate() + daysToSat);
|
||||
const sunday = new Date(saturday);
|
||||
sunday.setUTCDate(sunday.getUTCDate() + 1);
|
||||
|
||||
return {
|
||||
saturday: formatDateCN(satM, satD, 6),
|
||||
sunday: formatDateCN(sunM, sunD, 0),
|
||||
saturday: formatDateCN(saturday.getUTCMonth() + 1, saturday.getUTCDate(), 6),
|
||||
sunday: formatDateCN(sunday.getUTCMonth() + 1, sunday.getUTCDate(), 0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中国公历本周一 00:00 至周日 23:59:59 的 UTC 时间范围
|
||||
* 用于 PocketBase created 过滤。PocketBase 要求 Y-m-d H:i:s.uZ 格式(空格非 T)
|
||||
*/
|
||||
export function getThisWeekRangeISO(): { start: string; end: string } {
|
||||
const { year, month, day, weekday } = getChinaDateParts();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const todayStr = `${year}-${pad(month)}-${pad(day)}T12:00:00+08:00`;
|
||||
const today = new Date(todayStr);
|
||||
const daysToMonday = weekday === 0 ? 6 : weekday - 1;
|
||||
const monday = new Date(today);
|
||||
monday.setUTCDate(monday.getUTCDate() - daysToMonday);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setUTCDate(sunday.getUTCDate() + 6);
|
||||
const mondayStr = `${monday.getUTCFullYear()}-${pad(monday.getUTCMonth() + 1)}-${pad(monday.getUTCDate())}T00:00:00+08:00`;
|
||||
const sundayStr = `${sunday.getUTCFullYear()}-${pad(sunday.getUTCMonth() + 1)}-${pad(sunday.getUTCDate())}T23:59:59.999+08:00`;
|
||||
const startDate = new Date(mondayStr);
|
||||
const endDate = new Date(sundayStr);
|
||||
const fmt = (d: Date, ms: string) => {
|
||||
const y = d.getUTCFullYear();
|
||||
const m = pad(d.getUTCMonth() + 1);
|
||||
const day = pad(d.getUTCDate());
|
||||
const h = pad(d.getUTCHours());
|
||||
const min = pad(d.getUTCMinutes());
|
||||
const s = pad(d.getUTCSeconds());
|
||||
return `${y}-${m}-${day} ${h}:${min}:${s}.${ms}Z`;
|
||||
};
|
||||
return {
|
||||
start: fmt(startDate, "000"),
|
||||
end: fmt(endDate, "999"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中国公历本周一、周日的日期字符串(YYYY-MM-DD),用于 submitted_at_cn 过滤
|
||||
*/
|
||||
export function getThisWeekRangeChinaStr(): { start: string; end: string } {
|
||||
const { year, month, day, weekday } = getChinaDateParts();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const todayStr = `${year}-${pad(month)}-${pad(day)}T12:00:00+08:00`;
|
||||
const today = new Date(todayStr);
|
||||
const daysToMonday = weekday === 0 ? 6 : weekday - 1;
|
||||
const monday = new Date(today);
|
||||
monday.setUTCDate(monday.getUTCDate() - daysToMonday);
|
||||
const sunday = new Date(monday);
|
||||
sunday.setUTCDate(sunday.getUTCDate() + 6);
|
||||
return {
|
||||
start: `${monday.getUTCFullYear()}-${pad(monday.getUTCMonth() + 1)}-${pad(monday.getUTCDate())} 00:00:00`,
|
||||
end: `${sunday.getUTCFullYear()}-${pad(sunday.getUTCMonth() + 1)}-${pad(sunday.getUTCDate())} 23:59:59`,
|
||||
};
|
||||
}
|
||||
|
||||
55
app/lib/joiners.ts
Normal file
55
app/lib/joiners.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/** 报名者项(mock 用 name,DB 用 nickname) */
|
||||
export type JoinerItem = {
|
||||
name?: string;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
xiaohongshu_url?: string;
|
||||
};
|
||||
|
||||
/** 去重 key:小红书 url 相同视为 1 人,无 url 时用 avatar,再 fallback nickname */
|
||||
function dedupeKey(j: JoinerItem, useAvatarForMock: boolean): string {
|
||||
const xh = (j.xiaohongshu_url || "").trim();
|
||||
if (xh && !useAvatarForMock) return `xh:${xh}`;
|
||||
const avatar = (j.avatar || "").trim();
|
||||
if (avatar) return `av:${avatar}`;
|
||||
return `_${(j.nickname || j.name || "").trim()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 mock 与数据库报名者:mock 固定 3 个在左,DB 在右
|
||||
* 小红书 url 相同视为 1 人,avatar 相同也只显示 1 个
|
||||
* 最多显示 8 个,保证 PC/H5 排版合理
|
||||
*/
|
||||
export function mergeJoiners(
|
||||
mock: JoinerItem[],
|
||||
db: JoinerItem[] | undefined
|
||||
): JoinerItem[] {
|
||||
const seen = new Set<string>();
|
||||
const result: JoinerItem[] = [];
|
||||
mock.forEach((j) => {
|
||||
const key = dedupeKey(j, true);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(j);
|
||||
}
|
||||
});
|
||||
(db || []).forEach((j) => {
|
||||
const key = dedupeKey(j, false);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(j);
|
||||
}
|
||||
});
|
||||
return result.slice(0, 8);
|
||||
}
|
||||
|
||||
/** 数据库报名者按小红书 url 去重后的数量(用于显示报名人数) */
|
||||
export function getUniqueDbCount(db: JoinerItem[] | undefined): number {
|
||||
if (!db?.length) return 0;
|
||||
const seen = new Set<string>();
|
||||
for (const j of db) {
|
||||
const key = dedupeKey(j, false);
|
||||
seen.add(key);
|
||||
}
|
||||
return seen.size;
|
||||
}
|
||||
Reference in New Issue
Block a user