UNPKG

@blocklet/ui-react

Version:

Some useful front-end web components that can be used in Blocklets.

321 lines (291 loc) 8.68 kB
import isUrl from 'is-url'; import { withHttps } from 'ufo'; import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; import { DurationEnum, UserMetadata } from '../../../@types'; // 扩展 dayjs 插件 dayjs.extend(utc); dayjs.extend(timezone); const HOUR = 3600; const MINUTES_30 = 1800; const MINUTES_10 = 600; const MINUTES_5 = 300; const MINUTES_1 = 60; const SECOND = 1; export const currentTimezone = dayjs.tz.guess(); // 常用时区列表,作为兼容性 fallback const COMMON_TIMEZONES = [ 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Singapore', 'Asia/Seoul', 'Asia/Kolkata', 'Australia/Sydney', 'Australia/Melbourne', 'Pacific/Auckland', 'America/Sao_Paulo', 'America/Mexico_City', 'Africa/Cairo', 'UTC', ]; // 获取时区列表的兼容性函数 const getTimezoneList = () => { // 优先使用现代 API if (typeof Intl !== 'undefined' && Intl.supportedValuesOf) { try { return Intl.supportedValuesOf('timeZone'); } catch (error) { console.warn('Intl.supportedValuesOf not supported, falling back to common timezones'); } } // 尝试使用 Intl.DateTimeFormat 获取时区 if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { try { // 使用 resolvedOptions 检测当前时区是否可用 const formatter = new Intl.DateTimeFormat('en', { timeZone: 'UTC' }); if (formatter.resolvedOptions().timeZone) { return COMMON_TIMEZONES; } } catch (error) { console.warn('Intl.DateTimeFormat timezone support limited'); } } // 最后的 fallback return COMMON_TIMEZONES; }; export const getTimezones = () => { const timezones = getTimezoneList(); const formattedTimezones = timezones .map((tz) => { try { const offset = dayjs.tz(dayjs(), tz).utcOffset() / 60; // 计算 UTC 偏移 (小时) const hours = Math.floor(offset); const minutes = (offset % 1) * 60; const label = `GMT${hours >= 0 ? '+' : ''}${hours}:${minutes === 30 ? '30' : '00'}`; return { label, value: tz }; } catch (error) { // 如果时区不支持,跳过 console.warn(`Timezone ${tz} not supported, skipping`); return null; } }) .filter((tz): tz is { label: string; value: string } => tz !== null); // 类型守卫 return formattedTimezones .sort((a, b) => { const [hoursA, minutesA] = a.label.replace('GMT', '').split(':').map(Number); const [hoursB, minutesB] = b.label.replace('GMT', '').split(':').map(Number); const totalOffsetA = hoursA * 60 + minutesA; // 统一为分钟数 const totalOffsetB = hoursB * 60 + minutesB; return totalOffsetB - totalOffsetA; // **降序排列** }) .map((tz) => ({ label: `(${tz.label}) ${tz.value}`, value: tz.value, })); }; export const isValidUrl = (url: string) => { return isUrl(withHttps(url)); // 补充协议后在进行验证是否是合法的url }; export const isDuplicateUrl = (url1: string, url2: string) => { if (!url1 || !url2) { return false; } if (!isValidUrl(url1) || !isValidUrl(url2)) { return false; } const parsedUrl1 = withHttps(url1.trim()); const parsedUrl2 = withHttps(url2.trim()); return parsedUrl1.toLowerCase() === parsedUrl2.toLowerCase(); }; /** * 根据 duration 类型,计算出date range * @param status * @returns */ export const getStatusDuration = (status: UserMetadata['status']) => { let dateRange: dayjs.Dayjs[] = status?.dateRange?.map((d) => dayjs(d)) ?? []; const current = dayjs(); switch (status?.duration) { case DurationEnum.ThirtyMinutes: dateRange = [current, current.add(30, 'minutes')]; break; case DurationEnum.OneHour: dateRange = [current, current.add(1, 'hour')]; break; case DurationEnum.FourHours: dateRange = [current, current.add(4, 'hours')]; break; case DurationEnum.Today: dateRange = [current, current.endOf('day')]; break; case DurationEnum.ThisWeek: dateRange = [current, current.endOf('week')]; break; case DurationEnum.NoClear: dateRange = [current, current]; break; default: break; } return dateRange.map((d) => d.toDate()); }; /** * 根据状态的 duration,判断是否在时间范围内 * @param status * @returns */ export const isWithinTimeRange = (dateRange: [Date, Date]) => { const current = dayjs(); return current.isAfter(dayjs(dateRange[0])) && current.isBefore(dayjs(dateRange[1])); }; /** * 判断状态持续时间是否为不可清除 * @param status * @returns */ export const isNotClear = (status: UserMetadata['status']) => { const { duration, dateRange } = status ?? {}; if (!duration || !dateRange) { return false; } return duration === DurationEnum.NoClear || dayjs(dateRange?.[0]).isSame(dayjs(dateRange?.[1])); }; /** * 获取当前时间距离结束时间还有多久 */ export const getTimeRemaining = (date: Date) => { const now = dayjs(); const end = dayjs(date); const diffSeconds = end.diff(now, 'seconds'); // 转换为毫秒 const toMilliseconds = (seconds: number) => seconds * 1000; if (diffSeconds >= HOUR) { return toMilliseconds(HOUR); // 1小时 = 3600000ms } if (diffSeconds >= MINUTES_30) { return toMilliseconds(MINUTES_30); // 30分钟 = 1800000ms } if (diffSeconds >= MINUTES_10) { return toMilliseconds(MINUTES_10); // 10分钟 = 600000ms } if (diffSeconds >= MINUTES_5) { return toMilliseconds(MINUTES_5); // 5分钟 = 300000ms } if (diffSeconds >= MINUTES_1) { return toMilliseconds(MINUTES_1); // 1分钟 = 60000ms } if (diffSeconds >= SECOND) { return toMilliseconds(SECOND); // 1秒 = 1000ms } return 0; // 如果时间已过期,返回0 }; // 只支持在 sx 中使用 export const defaultButtonStyle = { color: 'text.primary', borderColor: 'grey.100', backgroundColor: 'background.default', '&:hover': { borderColor: 'grey.100', backgroundColor: 'action.hover', }, py: 0.5, borderRadius: 1, }; export const primaryButtonStyle = { color: 'primary.contrastText', borderColor: 'primary.main', backgroundColor: 'primary.main', '&:hover': { borderColor: 'primary.main', backgroundColor: 'primary.main', }, py: 0.5, borderRadius: 1, }; // 域名关键词到平台标识符的映射表 const DOMAIN_PLATFORM_MAP: Record<string, { domains: string[]; options?: Record<string, any> }> = { x: { domains: ['twitter.com', 'x.com'], }, facebook: { domains: ['facebook.com', 'fb.com'], options: { skipDarkInvert: true }, }, 'linkedin-icon': { domains: ['linkedin.com'], options: { skipDarkInvert: true }, }, 'github-icon': { domains: ['github.com'], }, 'instagram-icon': { domains: ['instagram.com'], }, 'youtube-icon': { domains: ['youtube.com', 'youtu.be'], options: { skipDarkInvert: true }, }, 'tiktok-icon': { domains: ['tiktok.com'], }, 'reddit-icon': { domains: ['reddit.com'], options: { skipDarkInvert: true }, }, 'medium-icon': { domains: ['medium.com'], }, 'discord-icon': { domains: ['discord.com', 'discord.gg'], options: { skipDarkInvert: true }, }, telegram: { domains: ['telegram.org', 't.me'], options: { skipDarkInvert: true }, }, 'whatsapp-monochrome-icon': { domains: ['whatsapp.com'], }, producthunt: { domains: ['producthunt.com'], options: { skipDarkInvert: true }, }, ycombinator: { domains: ['ycombinator.com', 'news.ycombinator.com'], options: { skipDarkInvert: true }, }, }; export const getLogoByUrl = (url: string): { icon: string; options?: Record<string, any> } | undefined => { try { // 添加协议(如果没有) const fullUrl = withHttps(url); // 解析 URL const urlObj = new URL(fullUrl); const hostname = urlObj.hostname.toLowerCase(); // 移除 www. 前缀 const domain = hostname.replace(/^www\./, ''); // 根据域名返回对应的平台标识符 for (const [platform, { domains, options }] of Object.entries(DOMAIN_PLATFORM_MAP)) { if (domains.some((d) => domain === d || domain.endsWith(`.${d}`))) { return { icon: `logos:${platform}`, options, }; } } // 对于未匹配的域名,返回空字符串 return undefined; } catch { return undefined; } };