@blocklet/ui-react
Version:
Some useful front-end web components that can be used in Blocklets.
321 lines (291 loc) • 8.68 kB
text/typescript
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;
}
};