@flecblog/core-nuxt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.vue +172 -0
- package/assets/css/base.scss +96 -0
- package/composables/api/article.ts +25 -0
- package/composables/api/category.ts +19 -0
- package/composables/api/comment.ts +25 -0
- package/composables/api/createApi.ts +242 -0
- package/composables/api/feedback.ts +14 -0
- package/composables/api/friend.ts +14 -0
- package/composables/api/moment.ts +10 -0
- package/composables/api/music.ts +39 -0
- package/composables/api/notification.ts +19 -0
- package/composables/api/stats.ts +14 -0
- package/composables/api/subscribe.ts +13 -0
- package/composables/api/sysconfig.ts +9 -0
- package/composables/api/tag.ts +19 -0
- package/composables/api/theme.ts +9 -0
- package/composables/api/upload.ts +13 -0
- package/composables/api/user.ts +77 -0
- package/composables/useArticle.ts +227 -0
- package/composables/useAuth.ts +180 -0
- package/composables/useComment.ts +143 -0
- package/composables/useDarkMode.ts +29 -0
- package/composables/useEmoji.ts +39 -0
- package/composables/useFeedback.ts +38 -0
- package/composables/useFriendList.ts +100 -0
- package/composables/useMermaid.ts +30 -0
- package/composables/useMoment.ts +86 -0
- package/composables/useMusic.ts +37 -0
- package/composables/useSearchState.ts +90 -0
- package/composables/useStores.ts +557 -0
- package/composables/useSubscribe.ts +13 -0
- package/composables/useTheme.ts +61 -0
- package/composables/useUser.ts +202 -0
- package/layouts/default.vue +1 -0
- package/nuxt.config.ts +170 -0
- package/package.json +58 -0
- package/pages/index.vue +1 -0
- package/plugins/console-banner.client.ts +11 -0
- package/plugins/custom-code.ts +121 -0
- package/plugins/syncThemeMeta.ts +28 -0
- package/plugins/tracker.client.ts +107 -0
- package/server/plugins/sitemap.ts +170 -0
- package/server/routes/atom.xml.ts +21 -0
- package/server/routes/manifest.json.ts +45 -0
- package/server/routes/rss.xml.ts +21 -0
- package/types/article.ts +48 -0
- package/types/auth.ts +8 -0
- package/types/category.ts +12 -0
- package/types/comment.ts +72 -0
- package/types/emoji.ts +10 -0
- package/types/feedback.ts +31 -0
- package/types/friend.ts +63 -0
- package/types/markdown.ts +7 -0
- package/types/moment.ts +85 -0
- package/types/notification.ts +56 -0
- package/types/request.ts +30 -0
- package/types/stats.ts +38 -0
- package/types/sysconfig.ts +2 -0
- package/types/tag.ts +11 -0
- package/types/theme.ts +26 -0
- package/types/upload.ts +5 -0
- package/types/user.ts +106 -0
- package/utils/avatar.ts +21 -0
- package/utils/date.ts +136 -0
- package/utils/download.ts +11 -0
- package/utils/emoji.ts +90 -0
- package/utils/format.ts +42 -0
- package/utils/markdown.ts +1458 -0
- package/utils/scroll.ts +31 -0
- package/utils/upload.ts +57 -0
package/types/stats.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 网站统计信息
|
|
3
|
+
*/
|
|
4
|
+
export interface SiteStats {
|
|
5
|
+
total_words: string; // 本站总字数
|
|
6
|
+
total_visitors: number; // 本站访客数
|
|
7
|
+
total_page_views: number; // 本站总浏览量
|
|
8
|
+
online_users: number; // 当前在线人数
|
|
9
|
+
total_articles: number; // 已发布文章数
|
|
10
|
+
total_comments: number; // 公开可见评论数
|
|
11
|
+
total_friends: number; // 友链数
|
|
12
|
+
total_moments: number; // 动态数
|
|
13
|
+
total_categories: number; // 已发布文章分类数
|
|
14
|
+
total_tags: number; // 已发布文章标签数
|
|
15
|
+
|
|
16
|
+
// 详细访问统计
|
|
17
|
+
today_visitors: number; // 今日访客数(UV)
|
|
18
|
+
today_pageviews: number; // 今日访问量(PV)
|
|
19
|
+
yesterday_visitors: number; // 昨日访客数(UV)
|
|
20
|
+
yesterday_pageviews: number; // 昨日访问量(PV)
|
|
21
|
+
month_pageviews: number; // 本月访问量(PV)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 归档统计项
|
|
26
|
+
*/
|
|
27
|
+
export interface ArchiveItem {
|
|
28
|
+
year: string;
|
|
29
|
+
month: string;
|
|
30
|
+
count: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 归档统计数据
|
|
35
|
+
*/
|
|
36
|
+
export interface ArchiveStats {
|
|
37
|
+
archives: ArchiveItem[];
|
|
38
|
+
}
|
package/types/tag.ts
ADDED
package/types/theme.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** 菜单项接口 */
|
|
2
|
+
export interface Menu {
|
|
3
|
+
id: number;
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
icon: string;
|
|
7
|
+
sort: number;
|
|
8
|
+
is_enabled: boolean;
|
|
9
|
+
children?: Menu[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ThemeSchemaResponse {
|
|
13
|
+
slug: string;
|
|
14
|
+
name: string;
|
|
15
|
+
schema: Record<string, unknown>;
|
|
16
|
+
config: Record<string, unknown>;
|
|
17
|
+
menus: Record<string, Menu[]>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ThemeState {
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
schema: Record<string, unknown>;
|
|
24
|
+
config: Record<string, unknown>;
|
|
25
|
+
loaded: boolean;
|
|
26
|
+
}
|
package/types/upload.ts
ADDED
package/types/user.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户角色枚举
|
|
3
|
+
*/
|
|
4
|
+
export type UserRole = 'super_admin' | 'admin' | 'user';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 用户基本信息
|
|
8
|
+
*/
|
|
9
|
+
export interface UserInfo {
|
|
10
|
+
id: number;
|
|
11
|
+
email: string;
|
|
12
|
+
email_hash: string;
|
|
13
|
+
is_virtual_email: boolean; // 是否为虚拟邮箱(需绑定真实邮箱)
|
|
14
|
+
avatar?: string;
|
|
15
|
+
badge?: string;
|
|
16
|
+
nickname: string;
|
|
17
|
+
website?: string;
|
|
18
|
+
last_login?: string;
|
|
19
|
+
created_at: string;
|
|
20
|
+
role: UserRole;
|
|
21
|
+
has_password: boolean;
|
|
22
|
+
linked_oauths: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 用户资料更新参数(所有字段均为可选)
|
|
27
|
+
*/
|
|
28
|
+
export interface UpdateProfileParams {
|
|
29
|
+
nickname?: string;
|
|
30
|
+
email?: string;
|
|
31
|
+
avatar?: string;
|
|
32
|
+
badge?: string;
|
|
33
|
+
website?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 登录请求参数
|
|
38
|
+
*/
|
|
39
|
+
export interface LoginParams {
|
|
40
|
+
email: string;
|
|
41
|
+
password: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 登录响应数据
|
|
46
|
+
*/
|
|
47
|
+
export interface LoginResponse {
|
|
48
|
+
access_token: string;
|
|
49
|
+
user: UserInfo;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 注册请求参数
|
|
54
|
+
*/
|
|
55
|
+
export interface RegisterParams {
|
|
56
|
+
email: string;
|
|
57
|
+
nickname: string;
|
|
58
|
+
password: string;
|
|
59
|
+
website?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 注册响应数据
|
|
64
|
+
*/
|
|
65
|
+
export interface RegisterResponse {
|
|
66
|
+
access_token: string;
|
|
67
|
+
user: UserInfo;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 忘记密码请求参数
|
|
72
|
+
*/
|
|
73
|
+
export interface ForgotPasswordParams {
|
|
74
|
+
email: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 重置密码请求参数
|
|
79
|
+
*/
|
|
80
|
+
export interface ResetPasswordParams {
|
|
81
|
+
email: string;
|
|
82
|
+
code: string;
|
|
83
|
+
password: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 修改密码请求参数
|
|
88
|
+
*/
|
|
89
|
+
export interface ChangePasswordParams {
|
|
90
|
+
old_password: string;
|
|
91
|
+
new_password: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 注销账户请求参数
|
|
96
|
+
*/
|
|
97
|
+
export interface DeactivateAccountParams {
|
|
98
|
+
password: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 刷新Token响应数据
|
|
103
|
+
*/
|
|
104
|
+
export interface RefreshTokenResponse {
|
|
105
|
+
access_token: string;
|
|
106
|
+
}
|
package/utils/avatar.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useSysConfig } from '@/composables/useStores';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CRAVATAR_URL = 'https://cravatar.cn/avatar/%s?s=200&d=robohash';
|
|
4
|
+
|
|
5
|
+
export function getAvatarUrl(user: { avatar?: string; email_hash?: string }, size = 48): string {
|
|
6
|
+
if (user.avatar) return user.avatar;
|
|
7
|
+
if (!user.email_hash) {
|
|
8
|
+
return `data:image/svg+xml,%3Csvg width='${size}' height='${size}' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='24' cy='24' r='24' fill='%23E5E7EB'/%3E%3Cpath d='M24 12C17.3726 12 12 17.3726 12 24C12 30.6274 17.3726 36 24 36C30.6274 36 36 30.6274 36 24C36 17.3726 30.6274 12 24 12Z' fill='%239CA3AF'/%3E%3Ccircle cx='24' cy='24' r='8' fill='%236B7280'/%3E%3C/svg%3E`;
|
|
9
|
+
}
|
|
10
|
+
let cravatarUrl = DEFAULT_CRAVATAR_URL;
|
|
11
|
+
try {
|
|
12
|
+
const { basicConfig } = useSysConfig();
|
|
13
|
+
if (basicConfig.value.cravatar_url) {
|
|
14
|
+
cravatarUrl = basicConfig.value.cravatar_url;
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
// useSysConfig 可能在非 Nuxt 上下文中调用,使用默认值
|
|
18
|
+
}
|
|
19
|
+
const url = cravatarUrl.replace('%s', user.email_hash);
|
|
20
|
+
return url.replace(/s=\d+/, `s=${size}`);
|
|
21
|
+
}
|
package/utils/date.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import 'dayjs/locale/zh-cn';
|
|
3
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
4
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
5
|
+
|
|
6
|
+
// 配置 dayjs
|
|
7
|
+
dayjs.locale('zh-cn');
|
|
8
|
+
dayjs.extend(relativeTime);
|
|
9
|
+
dayjs.extend(customParseFormat);
|
|
10
|
+
|
|
11
|
+
// 后端统一格式
|
|
12
|
+
export const BACKEND_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 格式化日期为完整的日期时间
|
|
16
|
+
* @param date 日期字符串或 Date 对象
|
|
17
|
+
* @returns 格式化后的字符串,如 "2025-10-03 13:46:59"
|
|
18
|
+
*/
|
|
19
|
+
export function formatDateTime(date: string | Date | null | undefined): string {
|
|
20
|
+
if (!date) return '-';
|
|
21
|
+
return dayjs(date).format(BACKEND_DATE_FORMAT);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 格式化日期为日期(不含时间)
|
|
26
|
+
* @param date 日期字符串或 Date 对象
|
|
27
|
+
* @returns 格式化后的字符串,如 "2025-10-03"
|
|
28
|
+
*/
|
|
29
|
+
export function formatDate(date: string | Date | null | undefined): string {
|
|
30
|
+
if (!date) return '-';
|
|
31
|
+
return dayjs(date).format('YYYY-MM-DD');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 格式化为相对时间(如:2小时前)
|
|
36
|
+
* @param date 日期字符串或 Date 对象
|
|
37
|
+
* @returns 相对时间字符串
|
|
38
|
+
*/
|
|
39
|
+
export function formatRelativeTime(date: string | Date | null | undefined): string {
|
|
40
|
+
if (!date) return '-';
|
|
41
|
+
return dayjs(date).fromNow();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 解析后端日期格式为 Date 对象
|
|
46
|
+
* @param dateString 后端返回的日期字符串
|
|
47
|
+
* @returns Date 对象或 null
|
|
48
|
+
*/
|
|
49
|
+
export function parseBackendDate(dateString: string | null | undefined): Date | null {
|
|
50
|
+
if (!dateString?.trim()) return null;
|
|
51
|
+
const parsed = dayjs(dateString, BACKEND_DATE_FORMAT, true);
|
|
52
|
+
return parsed.isValid() ? parsed.toDate() : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 将 Date 对象格式化为后端需要的格式
|
|
57
|
+
* @param date Date 对象
|
|
58
|
+
* @returns 后端格式的日期字符串
|
|
59
|
+
*/
|
|
60
|
+
export function formatForBackend(date: Date | null | undefined): string {
|
|
61
|
+
if (!date) return '';
|
|
62
|
+
return dayjs(date).format(BACKEND_DATE_FORMAT);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 判断日期是否有效
|
|
67
|
+
* @param date 日期字符串或 Date 对象
|
|
68
|
+
* @returns 是否有效
|
|
69
|
+
*/
|
|
70
|
+
export function isValidDate(date: string | Date | null | undefined): boolean {
|
|
71
|
+
return date ? dayjs(date).isValid() : false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 格式化为友好的显示格式
|
|
76
|
+
* @param date 日期字符串或 Date 对象
|
|
77
|
+
* @returns 友好的日期字符串,如 "2025年10月3日"
|
|
78
|
+
*/
|
|
79
|
+
export function formatFriendly(date: string | Date | null | undefined): string {
|
|
80
|
+
if (!date) return '-';
|
|
81
|
+
|
|
82
|
+
const target = dayjs(date);
|
|
83
|
+
|
|
84
|
+
// 始终显示完整的年月日
|
|
85
|
+
return target.format('YYYY年M月D日');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 格式化为动态友好时间
|
|
90
|
+
* @param date 日期字符串或 Date 对象
|
|
91
|
+
* @returns 友好时间字符串:n小时前(24小时内)、n天前(3天内)、几月几日(本年)、几年几月几日(非本年)
|
|
92
|
+
*/
|
|
93
|
+
export function formatMomentTime(date: string | Date | null | undefined): string {
|
|
94
|
+
if (!date) return '-';
|
|
95
|
+
|
|
96
|
+
const now = dayjs();
|
|
97
|
+
const target = dayjs(date);
|
|
98
|
+
const diffHours = now.diff(target, 'hour');
|
|
99
|
+
const diffDays = now.diff(target, 'day');
|
|
100
|
+
|
|
101
|
+
// 24小时内显示小时
|
|
102
|
+
if (diffHours < 24) {
|
|
103
|
+
if (diffHours < 1) {
|
|
104
|
+
const diffMinutes = now.diff(target, 'minute');
|
|
105
|
+
return diffMinutes < 1 ? '刚刚' : `${diffMinutes}分钟前`;
|
|
106
|
+
}
|
|
107
|
+
return `${diffHours}小时前`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3天内显示天数
|
|
111
|
+
if (diffDays < 3) {
|
|
112
|
+
return `${diffDays}天前`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 今年的日期显示月日
|
|
116
|
+
if (now.year() === target.year()) {
|
|
117
|
+
return target.format('M月D日');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 其他年份显示年月日
|
|
121
|
+
return target.format('YYYY年M月D日');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 计算从指定日期到今天的运行天数 */
|
|
125
|
+
export function calcRunningDays(dateStr: string): number {
|
|
126
|
+
const startDate = new Date(dateStr).getTime();
|
|
127
|
+
return Math.floor((Date.now() - startDate) / 86400000);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 将秒数格式化为 mm:ss */
|
|
131
|
+
export function formatDuration(seconds: number): string {
|
|
132
|
+
if (!isFinite(seconds) || isNaN(seconds)) return '00:00';
|
|
133
|
+
const mins = Math.floor(seconds / 60);
|
|
134
|
+
const secs = Math.floor(seconds % 60);
|
|
135
|
+
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
136
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 通过 fetch 下载远程文件并触发浏览器保存 */
|
|
2
|
+
export async function downloadFile(url: string, filename?: string): Promise<void> {
|
|
3
|
+
const blob = await fetch(url).then(r => r.blob());
|
|
4
|
+
const name = filename || url.split('/').pop() || 'download';
|
|
5
|
+
const a = Object.assign(document.createElement('a'), {
|
|
6
|
+
href: URL.createObjectURL(blob),
|
|
7
|
+
download: name,
|
|
8
|
+
});
|
|
9
|
+
a.click();
|
|
10
|
+
URL.revokeObjectURL(a.href);
|
|
11
|
+
}
|
package/utils/emoji.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { EmojiGroup } from '@@/types/emoji';
|
|
2
|
+
|
|
3
|
+
let emojiMapCache: Map<string, string> | null = null;
|
|
4
|
+
let emojiLoadPromise: Promise<Map<string, string>> | null = null;
|
|
5
|
+
let emojiGroupsCache: EmojiGroup[] | null = null;
|
|
6
|
+
let emojiGroupsLoadPromise: Promise<EmojiGroup[]> | null = null;
|
|
7
|
+
|
|
8
|
+
export async function loadEmojiMap(emojisUrl: string): Promise<Map<string, string>> {
|
|
9
|
+
if (emojiMapCache) {
|
|
10
|
+
return emojiMapCache;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (emojiLoadPromise) {
|
|
14
|
+
return emojiLoadPromise;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
emojiLoadPromise = (async () => {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(emojisUrl);
|
|
20
|
+
if (!response.ok) throw new Error('加载表情包失败');
|
|
21
|
+
|
|
22
|
+
const data: EmojiGroup[] = await response.json();
|
|
23
|
+
const map = new Map<string, string>();
|
|
24
|
+
|
|
25
|
+
for (const group of data) {
|
|
26
|
+
if (group.type === 'image') {
|
|
27
|
+
for (const item of group.items) {
|
|
28
|
+
map.set(item.key, item.val);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
emojiMapCache = map;
|
|
34
|
+
return map;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('加载表情映射失败:', error);
|
|
37
|
+
emojiLoadPromise = null;
|
|
38
|
+
return new Map();
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
return emojiLoadPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getEmojiMapSync(): Map<string, string> | null {
|
|
46
|
+
return emojiMapCache;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearEmojiCache(): void {
|
|
50
|
+
emojiMapCache = null;
|
|
51
|
+
emojiLoadPromise = null;
|
|
52
|
+
emojiGroupsCache = null;
|
|
53
|
+
emojiGroupsLoadPromise = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 加载完整表情分组(供 EmojiPicker 使用)
|
|
57
|
+
export async function loadAllEmojiGroups(emojisUrl: string): Promise<EmojiGroup[]> {
|
|
58
|
+
if (emojiGroupsCache) return emojiGroupsCache;
|
|
59
|
+
if (emojiGroupsLoadPromise) return emojiGroupsLoadPromise;
|
|
60
|
+
|
|
61
|
+
emojiGroupsLoadPromise = (async () => {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(emojisUrl);
|
|
64
|
+
if (!response.ok) throw new Error('加载表情包失败');
|
|
65
|
+
const groups: EmojiGroup[] = await response.json();
|
|
66
|
+
emojiGroupsCache = groups;
|
|
67
|
+
return groups;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('加载表情分组失败:', error);
|
|
70
|
+
emojiGroupsLoadPromise = null;
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
return emojiGroupsLoadPromise;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getAllEmojiGroupsSync(): EmojiGroup[] | null {
|
|
79
|
+
return emojiGroupsCache;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function replaceEmojisInText(text: string, emojiMap: Map<string, string>): string {
|
|
83
|
+
return text.replace(/:([^:\s]+):/g, (match, key) => {
|
|
84
|
+
const url = emojiMap.get(key);
|
|
85
|
+
if (url) {
|
|
86
|
+
return `<img src="${url}" alt="${key}" class="emoji-image" title="${key}" />`;
|
|
87
|
+
}
|
|
88
|
+
return match;
|
|
89
|
+
});
|
|
90
|
+
}
|
package/utils/format.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** 数字缩写:≥1万显示 w,≥1千显示 k */
|
|
2
|
+
export function formatWords(words: string): string {
|
|
3
|
+
const n = +words;
|
|
4
|
+
if (n >= 1e4) return (n / 1e4).toFixed(1) + 'w';
|
|
5
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
|
|
6
|
+
return words;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ROLE_NAMES: Record<string, string> = {
|
|
10
|
+
super_admin: '超级管理员',
|
|
11
|
+
admin: '管理员',
|
|
12
|
+
user: '普通用户',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** 用户角色显示名称 */
|
|
16
|
+
export function getRoleName(role: string): string {
|
|
17
|
+
return ROLE_NAMES[role] || role;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PROVIDER_NAMES: Record<string, string> = {
|
|
21
|
+
github: 'GitHub',
|
|
22
|
+
google: 'Google',
|
|
23
|
+
qq: 'QQ',
|
|
24
|
+
microsoft: 'Microsoft',
|
|
25
|
+
oidc: 'OIDC',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** OAuth 提供商显示名称 */
|
|
29
|
+
export function getProviderName(provider: string): string {
|
|
30
|
+
return PROVIDER_NAMES[provider] || provider;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
34
|
+
pending: '待处理',
|
|
35
|
+
resolved: '已解决',
|
|
36
|
+
closed: '已关闭',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 反馈状态显示标签 */
|
|
40
|
+
export function getStatusLabel(status: string): string {
|
|
41
|
+
return STATUS_LABELS[status] || status;
|
|
42
|
+
}
|