Compare commits

..
4 Commits
Author SHA1 Message Date
Timi 7c937a090f add pass-time-label 2026-08-18 11:56:03 +08:00
Timi bb1fdb5547 update permission 2026-08-11 12:02:38 +08:00
Timi e5473eb499 update api 2026-08-07 19:44:42 +08:00
Timi f0ba55fe25 remove deprecated api 2026-08-07 10:46:14 +08:00
25 changed files with 384 additions and 324 deletions
+38
View File
@@ -0,0 +1,38 @@
# 2026-08-06 工作日志
## TypeScript enum 重构(全量消除 enum
用户决定改用纯联合类型,自行修改依赖方。全项目 18 处 enum 分两类处理:
### 改为纯联合类型(字符串枚举,仅作类型用)
- `Attachment.ts` AttachmentBizType、`Template.ts` TemplateBizType
- `Model.ts` RunEnv / OrderType / ImageTypeImageType 值为 ir-auto 等,与 key 不同)
- `Comment.ts` CommentReplyBizType / CommentBizType
- `User.ts` UserAttachType(在 UserAPI.ts 有值引用,已同步改为字符串字面量,import 改 type-only
### 改为 const 对象 + 派生类型(数字枚举或运行时值引用,纯联合不可行)
- `User.ts` LoginType、`Article.ts` ArticleType / ArticleAttachType / ArticleSoftwareDownloadType / ArticleSoftwareRuntime(数字枚举,保留数值避免破坏数据契约)
- `deviceStore.ts` Breakpoints(数字,用于比较 width < Breakpoints.SM
- `IOSize.ts` Unit(用 Object.keys/values + Unit.KB 值引用)
- `Prismjs.ts` PrismjsType / PrismjsViewerMap 键值引用)
- `directives/Popup.ts` PopupTypeswitch case 值引用)
### 结论
- `vue-tsc --noEmit` 通过(exit 0
- ESLint 因项目缺 eslint.config.js(仍用旧 .eslintrcESLint9 不支持)无法运行,属项目既有配置问题
- 判断依据:纯联合无运行时存在,凡 `Object.keys(enum)``map.set(Enum.X)``case Enum.X``Enum.X` 作值的都无法纯联合,必须 const 对象
## Storage.ts 设计修正
- `getString` 返回 `string | null`(对齐 `localStorage.getItem` 契约),不再 throw
- `getJSON` 返回 `unknown`,键不存在时返回 `null`,值无法解析时才抛错
- `getObject` 返回 `T | null`,不再 throw(修正原 JSDoc 已写 `T | undefined` 但代码 throw 的矛盾)
- `is` / `not` 键不存在时自然返回 `false` / `true`,不再崩
- `has` 去掉死代码 `!== undefined`localStorage.getItem 只返回 string|null),单次查询
- `vue-tsc --noEmit` 通过
## Toolkit.ts debounce 修复
- **Bug 修复**`cancel()` 原先只清定时器不重置 `leading` 状态,导致立即模式下 cancel 后首次调用丢失 leading 触发
- **类型修复**:返回类型从 `T & { cancel(): void }` 改为 `((...args: Parameters<T>) => void) & { cancel(): void }`,防抖函数实际返回 void 而非 T 的返回值
- 补充完整 JSDoc(含 `@example`),说明立即模式(leading + trailing)和延迟模式(trailing only)的行为差异
- 状态变量 `immediate``leading`,参数 `defaultImmediate``immediate`,语义更清晰
- 去掉 `eslint-disable-next-line` 和多余注释
- `vue-tsc --noEmit` 通过
+9 -9
View File
@@ -1,4 +1,4 @@
import {Attachment, BizUpdateReq, TempFileResp} from "../types";
import { Attachment, BizIdUpdate, TempFile } from "../types";
import { axios } from "./BaseAPI";
import CommonAPI from "./CommonAPI";
@@ -11,7 +11,7 @@ const BASE_URI = "/attach";
* @param ttl 有效期
* @returns 临时附件列表
*/
async function uploadTemp(file: File | File[], ttl?: string): Promise<TempFileResp[]> {
async function upload(file: File | File[], ttl?: string): Promise<TempFile[]> {
const formData = new FormData();
const fileList = Array.isArray(file) ? file : [file];
for (const item of fileList) {
@@ -28,8 +28,8 @@ async function uploadTemp(file: File | File[], ttl?: string): Promise<TempFileRe
*
* @param req 更新请求
*/
async function updateByBiz(req: BizUpdateReq): Promise<void> {
return await axios.post(`${BASE_URI}/update/biz`, req);
async function updateByBizId(req: BizIdUpdate): Promise<void> {
return await axios.post(`${BASE_URI}/update/biz/id`, req);
}
/**
@@ -39,8 +39,8 @@ async function updateByBiz(req: BizUpdateReq): Promise<void> {
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
*/
async function listByBiz(bizType: string, bizId: string, attachTypeList?: string[]): Promise<Attachment[]> {
return await axios.get(`${BASE_URI}/list/biz`, {
async function listByBizId(bizType: string, bizId: string, attachTypeList?: string[]): Promise<Attachment[]> {
return await axios.get(`${BASE_URI}/list/biz/id`, {
params: {
bizType,
bizId,
@@ -50,9 +50,9 @@ async function listByBiz(bizType: string, bizId: string, attachTypeList?: string
}
export default {
uploadTemp,
updateByBiz,
listByBiz,
upload,
updateByBizId,
listByBizId,
getReadURL: CommonAPI.getAttachmentReadAPI,
getTempReadURL: CommonAPI.getAttachmentTempReadAPI
};
+1 -11
View File
@@ -1,4 +1,4 @@
import { CaptchaData, Comment, CommentReply, Page, PageResult } from "../types";
import { CaptchaData, Comment, Page, PageResult } from "../types";
import { axios } from "./BaseAPI";
const BASE_URI = "/comment";
@@ -11,17 +11,7 @@ async function create(captchaData: CaptchaData<Comment>): Promise<void> {
return axios.post(`${BASE_URI}/create`, captchaData);
}
async function pageReply(page: Page<CommentReply>): Promise<PageResult<CommentReply>> {
return axios.post(`${BASE_URI}/reply/list`, page);
}
async function createReply(captchaData: CaptchaData<CommentReply>): Promise<void> {
return axios.post(`${BASE_URI}/reply/create`, captchaData);
}
export default {
page,
create,
createReply,
pageReply
};
+1 -6
View File
@@ -1,4 +1,4 @@
import {CaptchaResult, Setting, TemplateBizType} from "../types";
import { CaptchaResult, Setting } from "../types";
import { axios } from "./BaseAPI";
function getBaseURI(): string {
@@ -19,10 +19,6 @@ const getAttachmentReadAPI = (id: string) => `${getBaseURI()}/attach/read/${id}`
const getAttachmentTempReadAPI = (id: string) => `${getBaseURI()}/attach/temp/read?id=${id}`;
async function getTemplate(bizType: TemplateBizType, code: string): Promise<string> {
return axios.get(`/template?bizType=${bizType}&bizCode=${code}`);
}
async function settingMap(map: Record<string, string[]>): Promise<Map<string, Map<string, Setting>>> {
const raw = await axios.post("/setting/map", map);
const moduleMap = new Map<string, Map<string, Setting>>();
@@ -47,6 +43,5 @@ export default {
captcha,
getAttachmentReadAPI,
getAttachmentTempReadAPI,
getTemplate,
settingMap
};
+10 -2
View File
@@ -1,5 +1,5 @@
import { axios } from "./BaseAPI";
import type { Page, PageResult, Permission, PermissionPayload } from "../types";
import type { ModuleCode, Page, PageResult, Permission, PermissionPayload } from "../types";
const BASE_URI = "/user/permission";
@@ -21,9 +21,17 @@ async function remove(id: string): Promise<void> {
});
}
/** 查询当前登录用户在指定模块内可转授的权限。 */
async function grantableList(moduleCode: ModuleCode): Promise<Permission[]> {
return await axios.post(`${BASE_URI}/grantable/list`, undefined, {
params: { moduleCode }
});
}
export default {
list,
create,
update,
remove
remove,
grantableList
};
+70 -2
View File
@@ -1,5 +1,5 @@
import { axios } from "./BaseAPI";
import type { ModuleCode, Page, PageResult, Permission, Role, RolePayload, UserRoleAuthorizeReq } from "../types";
import type { ModuleCode, Page, PageResult, Permission, Role, RolePayload, AuthorizeUserRole } from "../types";
const BASE_URI = "/user/role";
@@ -27,6 +27,65 @@ async function remove(id: string): Promise<void> {
});
}
/** 查询当前登录用户在模块内可管理的角色。 */
async function manageableList(moduleCode: ModuleCode): Promise<Role[]> {
return await axios.post(`${BASE_URI}/manageable/list`, undefined, {
params: { moduleCode }
});
}
/** 查询当前登录用户在模块内可授权给账号的角色。 */
async function grantableList(moduleCode: ModuleCode): Promise<Role[]> {
return await axios.post(`${BASE_URI}/grantable/list`, undefined, {
params: { moduleCode }
});
}
/** 查询当前登录用户直属角色详情,不按可管理范围过滤。 */
async function currentDetailList(moduleCode?: ModuleCode): Promise<Role[]> {
return await axios.post(`${BASE_URI}/current/detail/list`, undefined, {
params: { moduleCode }
});
}
/** 查询当前登录用户可以给目标角色配置的直接权限 P 候选项。 */
async function grantablePermissionList(roleId: string): Promise<Permission[]> {
return await axios.post(`${BASE_URI}/permission/grantable/list`, undefined, {
params: { roleId }
});
}
/** 查询目标角色当前可继续转授的权限 D。 */
async function delegationList(roleId: string): Promise<Permission[]> {
return await axios.post(`${BASE_URI}/delegation/list`, undefined, {
params: { roleId }
});
}
/** 查询当前登录用户可以给目标角色配置的 D 候选项。 */
async function grantableDelegationList(roleId: string): Promise<Permission[]> {
return await axios.post(`${BASE_URI}/delegation/grantable/list`, undefined, {
params: { roleId }
});
}
/** 覆盖保存目标角色可继续转授的权限 D。 */
async function updateDelegation(req: Pick<RolePayload, "id" | "delegationPermissionIdList">): Promise<void> {
await axios.post(`${BASE_URI}/delegation/update`, req);
}
/** 查询目标角色可选管理父级。 */
async function parentGrantableList(roleId: string): Promise<Role[]> {
return await axios.post(`${BASE_URI}/parent/grantable/list`, undefined, {
params: { roleId }
});
}
/** 保存目标角色管理父级。 */
async function updateParent(req: Pick<RolePayload, "id" | "parentRoleId">): Promise<void> {
await axios.post(`${BASE_URI}/parent/update`, req);
}
async function authorizedList(userId: string, moduleCode?: ModuleCode): Promise<Role[]> {
return axios.post(`${BASE_URI}/authorized/list`, undefined, {
params: {
@@ -36,7 +95,7 @@ async function authorizedList(userId: string, moduleCode?: ModuleCode): Promise<
});
}
async function authorize(req: UserRoleAuthorizeReq): Promise<void> {
async function authorize(req: AuthorizeUserRole): Promise<void> {
return axios.post(`${BASE_URI}/authorized/create`, req);
}
@@ -52,6 +111,15 @@ export default {
create,
update,
remove,
manageableList,
grantableList,
currentDetailList,
grantablePermissionList,
delegationList,
grantableDelegationList,
updateDelegation,
parentGrantableList,
updateParent,
authorizedList,
authorize,
authorizedPermission
+8 -6
View File
@@ -1,5 +1,4 @@
import type {Attachment, CaptchaData, LoginRequest, LoginResponse, RegisterRequest, UpdatePasswordRequest, User} from "../types";
import {UserAttachType} from "../types";
import type {Attachment, CaptchaData, Gender, LoginRequest, LoginResponse, RegisterRequest, UpdatePasswordRequest, User, UserAttachType} from "../types";
import {axios} from "./BaseAPI";
import CommonAPI from "./CommonAPI";
@@ -7,6 +6,9 @@ const BASE_URI = "/user";
type UserAttachmentOwner = {
attachmentList?: readonly Attachment[];
};
type UpdateCurrentRequest = Omit<Partial<User>, "gender"> & {
gender?: Gender | "";
};
async function register(req: CaptchaData<RegisterRequest>): Promise<LoginResponse> {
return await axios.post(`${BASE_URI}/register`, req);
@@ -35,7 +37,7 @@ async function logout(): Promise<void> {
return await axios.post(`${BASE_URI}/logout`);
}
async function updateCurrent(req: Partial<User>): Promise<LoginResponse> {
async function updateCurrent(req: UpdateCurrentRequest): Promise<LoginResponse> {
return await axios.post(`${BASE_URI}/current/update`, req);
}
@@ -55,17 +57,17 @@ async function view(id: string): Promise<User> {
function getAvatarURL(user?: UserAttachmentOwner) {
if (user?.attachmentList) {
return findAttachmentByType(user.attachmentList, [UserAttachType.AVATAR, UserAttachType.DEFAULT_AVATAR]);
return findAttachmentByType(user.attachmentList, "AVATAR");
}
}
function getWrapperURL(user?: UserAttachmentOwner) {
if (user?.attachmentList) {
return findAttachmentByType(user.attachmentList, [UserAttachType.WRAPPER, UserAttachType.DEFAULT_WRAPPER]);
return findAttachmentByType(user.attachmentList, "WRAPPER");
}
}
function findAttachmentByType(attachmentList: readonly Attachment[], types: UserAttachType[]) {
function findAttachmentByType(attachmentList: readonly Attachment[], ...types: UserAttachType[]) {
for (let i = 0; i < attachmentList.length; i++) {
const attachType = attachmentList[i].attachType as UserAttachType | undefined;
const id = attachmentList[i].id;
+5 -2
View File
@@ -9,6 +9,7 @@ import EmptyTips from "./empty-tips";
import MarkdownView from "./markdown-view";
import BEFlowerFall from "./background-effect/flower-fall";
import MarkdownEditor from "./markdown-editor";
import PassTimeLabel from "./passtime-label";
export default [
Icon,
@@ -20,7 +21,8 @@ export default [
EmptyTips,
MarkdownView,
BEFlowerFall,
MarkdownEditor
MarkdownEditor,
PassTimeLabel
];
export {
@@ -33,5 +35,6 @@ export {
EmptyTips,
MarkdownView,
BEFlowerFall,
MarkdownEditor
MarkdownEditor,
PassTimeLabel
};
+5
View File
@@ -0,0 +1,5 @@
import view from "./index.vue";
import Toolkit from "../../utils/Toolkit";
export const PassTimeLabel = Toolkit.withInstall(view);
export default PassTimeLabel;
+63
View File
@@ -0,0 +1,63 @@
<template>
<span
v-if="timestamp"
class="tui-passtime-label"
:class="{ underline }"
role="button"
tabindex="0"
v-text="content"
@click="toggle"
@keydown.enter.space.prevent="toggle"
/>
</template>
<script lang="ts" setup>
import Time from "../../utils/Time";
defineOptions({
name: "PassTimeLabel"
});
const props = withDefaults(defineProps<{
timestamp?: number;
onlyTime?: boolean;
onlyDate?: boolean;
underline?: boolean;
}>(), {
onlyTime: false,
onlyDate: false,
underline: false
});
const { timestamp, onlyTime, onlyDate } = toRefs(props);
const showingDetail = ref(false);
const content = computed(() => {
if (!showingDetail.value) {
return Time.toPassedDateTime(timestamp.value);
}
if (onlyTime.value) {
return Time.toTime(timestamp.value);
}
if (onlyDate.value) {
return Time.toDate(timestamp.value);
}
return Time.toDateTime(timestamp.value);
});
function toggle(): void {
showingDetail.value = !showingDetail.value;
}
</script>
<style lang="less" scoped>
.tui-passtime-label {
cursor: pointer;
&.underline {
text-decoration: underline;
text-underline-offset: .16rem;
}
}
</style>
+7 -7
View File
@@ -32,23 +32,23 @@ const isShortScreen = ref(false);
const isMobileLayout = ref(false);
/** 断点配置,单位:px */
enum Breakpoints {
const Breakpoints = {
/** 超小设备 */
XS = 480,
XS: 480,
/** 手机 */
SM = 650,
SM: 650,
/** 平板 */
MD = 768,
MD: 768,
/** 笔记本 */
LG = 1024,
LG: 1024,
/** 大屏幕 */
XL = 1440
}
XL: 1440
} as const;
Resizer.addListener("DEVICE_SIZE", (width, height) => {
screenWidth.value = width;
+7 -68
View File
@@ -2,12 +2,12 @@ import type { Attachment } from "./Attachment";
import type { Model } from "./Model";
// 文章
export type Article<E extends ArticleMusicExtendData | ArticleSoftwareExtendData> = {
export type Article<A> = {
title?: string;
type: ArticleType;
digest?: string;
data?: string;
extendData?: E;
args?: A;
reads: number;
likes: number;
showComment: boolean;
@@ -15,74 +15,13 @@ export type Article<E extends ArticleMusicExtendData | ArticleSoftwareExtendData
canRanking: boolean;
} & Model;
export type ArticleView<E extends ArticleMusicExtendData | ArticleSoftwareExtendData> = {
export type ArticleView<A> = {
comments?: number;
attachmentList: Attachment[];
} & Article<E>;
} & Article<A>;
export enum ArticleType {
export const ArticleType = "COMMON";
/** 公版 */
PUBLIC,
export type ArticleType = (typeof ArticleType)[keyof typeof ArticleType];
/** 音乐 */
MUSIC,
/** 软件 */
SOFTWARE
}
export enum ArticleAttachType {
COVER,
}
export type ArticleMusicExtendData = {
title: string;
list: ArticleMusicItem[];
info: {
key: string;
value: string;
}[];
}
export type ArticleSoftwareExtendData = {
url?: string;
downloads: ArticleSoftwareDownload[];
format: string;
runtime: ArticleSoftwareRuntime[];
size: number;
version: string;
password?: string;
}
export enum ArticleSoftwareDownloadType {
TIMI_MONGO,
TIMI_COS,
URL
}
export enum ArticleSoftwareRuntime {
JVM,
WINDOWS,
LINUX,
MAC_OS
}
export type ArticleSoftwareDownload = {
type: ArticleSoftwareDownloadType;
value: string;
}
export type ArticleMusicItem = {
title: string;
audio?: string;
video?: string;
}
export const ArticleAttachType = "COVER";
+6 -17
View File
@@ -1,10 +1,10 @@
import { Model } from "./Model";
export type Attachment = {
bizType: AttachmentBizType;
bizId: string;
bizType?: AttachmentBizType;
bizId?: string;
attachType?: string;
mongoId: string;
mongoId?: string;
title?: string;
name?: string;
mimeType?: string;
@@ -17,26 +17,15 @@ export type Attachment = {
tempFileId?: string;
} & Model;
export type TempFileResp = {
export type TempFile = {
id: string;
expireAt?: number;
};
export type BizUpdateReq = {
export type BizIdUpdate = {
bizType: AttachmentBizType;
bizId: string;
items: Partial<Attachment>[];
};
export enum AttachmentBizType {
USER = "USER",
GAO_CUSTOMER = "GAO_CUSTOMER",
GAO_REGISTER_RECORD = "GAO_REGISTER_RECORD",
TEMP_FILE = "TEMP_FILE",
MIRROR = "MIRROR"
}
export type AttachmentBizType = | "USER" | "GAO_CUSTOMER" | "GAO_REGISTER_RECORD" | "TEMP_FILE" | "MIRROR";
+3 -32
View File
@@ -13,10 +13,10 @@ export type Comment = {
user?: User;
/** 回复列表 */
replies?: CommentReply[];
replies?: Comment[];
/** 回复分页 */
repliesPage?: Page<CommentReply>;
repliesPage?: Page<Comment>;
/** 用于绑定组件当前页下标 */
repliesCurrent?: number;
@@ -31,33 +31,4 @@ export type Comment = {
repository?: object;
} & Model
export type CommentReply = {
replyId?: number;
commentId?: number;
senderId?: number;
senderNick?: string;
receiverId?: number;
receiverNick?: string;
content?: string;
comment?: Comment;
sender?: User;
receiver?: User;
} & Model;
export enum CommentReplyBizType {
COMMENT = "COMMENT",
SENDER = "SENDER",
RECEIVER = "RECEIVER"
}
export enum CommentBizType {
ARTICLE = "ARTICLE",
GIT_ISSUE = "GIT_ISSUE",
GIT_MERGE = "GIT_MERGE",
}
export type CommentBizType = "ARTICLE" | "GIT_ISSUE" | "GIT_MERGE";
+7 -17
View File
@@ -1,9 +1,3 @@
export enum RunEnv {
DEV = "DEV",
DEV_SSL = "DEV_SSL",
PROD = "PROD"
}
// 基本实体模型
export type Model = {
id?: string;
@@ -23,17 +17,17 @@ export type ApiResponse<T> = {
}
export type Page<T> = {
index: number;
size: number;
orderMap?: { [K in keyof T]?: OrderType };
equalsExample?: Partial<T>;
likesExample?: Partial<T>;
} & BasePage;
export type BasePage = {
index: number;
size: number;
}
export enum OrderType {
ASC = "ASC",
DESC = "DESC"
}
export type OrderType = "ASC" | "DESC";
export type PageResult<T> = {
total: number;
@@ -53,11 +47,7 @@ export type CaptchaResult = {
data: string;
}
export enum ImageType {
AUTO = "ir-auto",
SMOOTH = "ir-smooth",
PIXELATED = "ir-pixelated"
}
export type ImageType = "IR-AUTO" | "IR-SMOOTH" | "IR-PIXELATED";
export type KeyValue<V, K = string> = {
key: K;
+7
View File
@@ -7,11 +7,18 @@ export interface Permission {
nameLangId?: string;
name?: string;
description?: string;
builtin?: boolean;
protectedPermission?: boolean;
ownerType?: AuthOwnerType;
ownerId?: string;
createdBy?: string;
createdAt?: number;
updatedAt?: number;
deletedAt?: number;
}
export type AuthOwnerType = "SYSTEM" | "MODULE" | "TENANT" | "STORE" | "USER";
export type PermissionPayload = Pick<Permission, "moduleCode" | "code" | "nameLangId" | "name" | "description"> & {
id?: string;
};
+20 -10
View File
@@ -1,31 +1,41 @@
import type { ModuleCode, Permission } from "./Permission";
import type { AuthOwnerType, ModuleCode, Permission } from "./Permission";
import { Model } from "./Model";
export interface Role {
id: string;
export type Role = {
moduleCode: ModuleCode;
code: string;
nameLangId?: string;
name?: string;
description?: string;
builtin?: boolean;
protectedRole?: boolean;
parentRoleId?: string;
ownerType?: AuthOwnerType;
ownerId?: string;
createdBy?: string;
permissionList?: Permission[];
allPermissionList?: Permission[];
childRoleList?: Role[];
parentRole?: Role;
delegationPermissionList?: Permission[];
permissionIdList?: string[];
childRoleIdList?: string[];
createdAt?: number;
updatedAt?: number;
deletedAt?: number;
}
delegationPermissionIdList?: string[];
} & Model;
export type RolePayload = Pick<Role, "moduleCode" | "code" | "nameLangId" | "name" | "description"> & {
export type RolePayload = Partial<Pick<Role, "moduleCode" | "code" | "nameLangId" | "name" | "description">> & {
id?: string;
parentRoleId?: string;
ownerType?: AuthOwnerType;
ownerId?: string;
permissionIdList?: string[];
childRoleIdList?: string[];
delegationPermissionIdList?: string[];
};
export interface UserRoleAuthorizeReq {
export interface AuthorizeUserRole {
userId: string;
moduleCode?: ModuleCode;
roleIdList?: string[];
roleCodeList?: string[];
}
-6
View File
@@ -1,6 +0,0 @@
export enum TemplateBizType {
GIT = "GIT",
FOREVER_MC = "FOREVER_MC"
}
+7 -19
View File
@@ -1,6 +1,9 @@
import { ImageType, Model } from "./Model";
import { Attachment } from "./Attachment";
import type { Setting } from "./Setting";
import { Role } from "./Role";
export type Gender = "MALE" | "FEMALE";
export type User = {
name: string;
@@ -13,7 +16,7 @@ export type User = {
wrapperType?: ImageType;
avatarType?: ImageType;
exp?: number;
sex?: number;
gender?: Gender;
birthdate?: number;
qq?: string;
description?: string;
@@ -21,7 +24,9 @@ export type User = {
lastLoginAt?: number;
unmuteAt?: number;
unbanAt?: number;
roleList?: string[];
roleEntityList?: Role[];
permissionList?: string[];
attachmentList?: Attachment[];
settingList?: Setting[];
@@ -32,18 +37,7 @@ export type User = {
phoneNoVerified?: boolean;
} & Model;
export enum UserAttachType {
AVATAR = "AVATAR",
WRAPPER = "WRAPPER",
LICENSE = "LICENSE",
DEFAULT_AVATAR = "DEFAULT_AVATAR",
DEFAULT_WRAPPER = "DEFAULT_WRAPPER"
}
export type UserAttachType = "AVATAR" | "WRAPPER";
export type RegisterRequest = {
name: string;
@@ -76,12 +70,6 @@ export type LoginUser = {
user?: User;
};
export enum LoginType {
ALERT,
IFRAME,
REDIRECT
}
export type UserLevelType = {
exp: number; // 经验数值,和 UserData.exp 一样
value: number; // 经验对应等级,[0, 8]
-1
View File
@@ -8,7 +8,6 @@ export * from "./Model";
export * from "./Permission";
export * from "./Role";
export * from "./User";
export * from "./Template";
export * from "./Comment";
export * from "./Developer";
+12 -35
View File
@@ -1,28 +1,7 @@
import Text from "./Text";
export enum Unit {
/** B */
B = "B",
/** KB */
KB = "KB",
/** MB */
MB = "MB",
/** GB */
GB = "GB",
/** TB */
TB = "TB",
/** PB */
PB = "PB",
/** EB */
EB = "EB"
}
export const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB"] as const;
export type Unit = (typeof UNITS)[number];
/** 储存单位 */
export default class IOSize {
@@ -48,8 +27,6 @@ export default class IOSize {
/** 1 EB */
public static EB = IOSize.PB << 10;
public static Unit = Unit;
/**
* <p>格式化一个储存容量,保留两位小数
* <pre>
@@ -66,7 +43,7 @@ export default class IOSize {
if (size === undefined || size === null) {
return "0 B";
}
const units = Object.keys(Unit);
const units = Object.values(UNITS);
if (0 < size) {
for (let i = 0; i < units.length; i++, size /= 1024) {
const unit = units[i];
@@ -118,31 +95,31 @@ export default class IOSize {
let unit: Unit;
// 先尝试精确匹配枚举
if (Object.values(Unit).includes(unitStr as Unit)) {
if (Object.values(UNITS).includes(unitStr as Unit)) {
unit = unitStr as Unit;
} else {
// 处理单字母单位缩写(K/M/G/T/P/E
switch (unitStr.charAt(0)) {
case "K":
unit = Unit.KB;
unit = "KB";
break;
case "M":
unit = Unit.MB;
unit = "MB";
break;
case "G":
unit = Unit.GB;
unit = "GB";
break;
case "T":
unit = Unit.TB;
unit = "TB";
break;
case "P":
unit = Unit.PB;
unit = "PB";
break;
case "E":
unit = Unit.EB;
unit = "EB";
break;
case "B":
unit = Unit.B;
unit = "B";
break;
default:
throw new Error("Unknown unit: " + unitStr);
@@ -173,7 +150,7 @@ export default class IOSize {
if (val === undefined || val === null) {
return 0;
}
const units = Object.values(Unit);
const units = Object.values(UNITS);
const ordinal = units.indexOf(unit);
return Math.round(val * Math.pow(1024, ordinal));
}
+29 -25
View File
@@ -1,23 +1,25 @@
export enum PrismjsType {
PlainText = "PlainText",
Markdown = "Markdown",
JavaScript = "JavaScript",
TypeScript = "TypeScript",
Initialization = "Initialization",
PHP = "PHP",
SQL = "SQL",
XML = "XML",
CSS = "CSS",
VUE = "VUE",
LESS = "LESS",
Markup = "Markup",
YAML = "YAML",
Json = "Json",
Java = "Java",
Properties = "Properties",
NginxConf = "NginxConf",
ApacheConf = "ApacheConf"
}
export const PrismjsType = {
PlainText: "PlainText",
Markdown: "Markdown",
JavaScript: "JavaScript",
TypeScript: "TypeScript",
Initialization: "Initialization",
PHP: "PHP",
SQL: "SQL",
XML: "XML",
CSS: "CSS",
VUE: "VUE",
LESS: "LESS",
Markup: "Markup",
YAML: "YAML",
Json: "Json",
Java: "Java",
Properties: "Properties",
NginxConf: "NginxConf",
ApacheConf: "ApacheConf"
} as const;
export type PrismjsType = (typeof PrismjsType)[keyof typeof PrismjsType];
export type PrismjsProperties = {
@@ -26,14 +28,16 @@ export type PrismjsProperties = {
viewer: PrismjsViewer;
}
export enum PrismjsViewer {
export const PrismjsViewer = {
MARKDOWN = "MARKDOWN",
MARKDOWN: "MARKDOWN",
CODE = "CODE",
CODE: "CODE",
TEXT = "TEXT",
}
TEXT: "TEXT"
} as const;
export type PrismjsViewer = (typeof PrismjsViewer)[keyof typeof PrismjsViewer];
export default class Prismjs {
+15 -20
View File
@@ -1,7 +1,7 @@
export default class Storage {
/**
* 获取为布尔值
* 获取为布尔值,键不存在时返回 false
*
* @param key 键
* @returns 布尔值
@@ -11,7 +11,7 @@ export default class Storage {
}
/**
* 获取为布尔值并取反
* 获取为布尔值并取反,键不存在时返回 true
*
* @param key 键
* @returns 布尔值
@@ -25,13 +25,11 @@ export default class Storage {
*
* @template T 对象类型
* @param key 键
* @returns {T | undefined} 返回对象
* @returns 对象,键不存在时返回 null
*/
public static getObject<T>(key: string): T {
if (this.has(key)) {
return this.getJSON(key) as T;
}
throw Error(`not found ${key}`);
public static getObject<T>(key: string): T | null {
const value = this.getJSON(key);
return value === null ? null : value as T;
}
/**
@@ -54,24 +52,21 @@ export default class Storage {
* 获取为 JSON
*
* @param key 键
* @returns JSON 对象
* @returns JSON 对象,键不存在时返回 null;值无法解析时抛错
*/
public static getJSON(key: string) {
return JSON.parse(this.getString(key));
public static getJSON(key: string): unknown {
const value = this.getString(key);
return value === null ? null : JSON.parse(value);
}
/**
* 获取为字符串(其他获取方式一般经过这个方法,找不到配置或配置值无效时会抛错)
* 获取为字符串
*
* @param key 键
* @returns 字符串
* @returns 字符串,键不存在时返回 null
*/
public static getString(key: string): string {
const value = localStorage.getItem(key);
if (value) {
return value;
}
throw new Error(`not found: ${key}, ${value}`);
public static getString(key: string): string | null {
return localStorage.getItem(key);
}
/**
@@ -81,7 +76,7 @@ export default class Storage {
* @returns true 为存在
*/
public static has(key: string): boolean {
return localStorage.getItem(key) !== undefined && localStorage.getItem(key) !== null;
return localStorage.getItem(key) !== null;
}
/**
+46 -16
View File
@@ -232,43 +232,73 @@ export default class Toolkit {
return Object.keys(e)[Object.values(e).indexOf(value)];
}
// 防抖
// eslint-disable-next-line
public static debounce<T extends (...args: any[]) => any>(callback: T, defaultImmediate = true, delay = 600): T & {
cancel(): void
} {
let timerId: ReturnType<typeof setTimeout> | null = null; // 存储定时器
let immediate = defaultImmediate;
// 定义一个 cancel 办法,用于勾销防抖
/**
* 防抖函数
*
* 支持两种模式:
* - **立即模式**`immediate = true`,默认):首次调用立即执行,连续调用在延迟结束后再执行最后一次(leading + trailing
* - **延迟模式**`immediate = false`):所有调用在延迟结束后执行最后一次(trailing only
*
* @param callback 回调函数
* @param immediate 是否首次调用立即执行,默认 true
* @param delay 延迟毫秒数,默认 600
* @returns 防抖后的函数,附带 `cancel()` 方法用于取消 pending 执行并重置状态
*
* @example
* ```ts
* // 立即模式(默认):首次立即触发,连续调用后在延迟结束时再触发一次
* const fn = Toolkit.debounce((val: string) => {
* console.log(val);
* });
* fn("a"); // 立即输出 "a"
* fn("b"); // 600ms 后输出 "b"trailing
* fn.cancel(); // 取消 pending 的 trailing 调用,重置为初始状态
*
* // 延迟模式:连续调用只在停止后触发最后一次
* const fn2 = Toolkit.debounce((val: string) => {
* console.log(val);
* }, false);
* fn2("a"); // 不输出
* fn2("b"); // 600ms 后输出 "b"
* ```
*/
public static debounce<T extends (...args: any[]) => any>(
callback: T,
immediate = true,
delay = 300
): ((...args: Parameters<T>) => void) & { cancel(): void } {
let timerId: ReturnType<typeof setTimeout> | null = null;
let leading = immediate;
const cancel = (): void => {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
leading = immediate;
};
const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
const context = this;
if (timerId) {
cancel();
clearTimeout(timerId);
timerId = null;
}
if (immediate) {
if (leading) {
callback.apply(context, args);
immediate = false;
leading = false;
timerId = setTimeout(() => {
immediate = defaultImmediate;
leading = immediate;
}, delay);
} else {
// 设置定时器,在延迟时间后执行指标函数
timerId = setTimeout(() => {
callback.apply(context, args);
immediate = defaultImmediate;
leading = immediate;
}, delay);
}
};
// 将 cancel 方法附加到 debounced 函数上
(debounced as any).cancel = cancel;
return debounced as T & { cancel(): void };
return debounced as ((...args: Parameters<T>) => void) & { cancel(): void };
}
public static toUserLevel(exp?: number): UserLevelType {
+6 -11
View File
@@ -1,12 +1,7 @@
import type { Directive, DirectiveBinding } from "vue";
import Toolkit from "../Toolkit";
export enum PopupType {
TEXT,
IMG,
HTML,
EL
}
export type PopupType = "TEXT" | "IMG" | "HTML" | "ELEMENT";
/** */
export type PopupConfig = {
@@ -30,7 +25,7 @@ const VPopup: Directive = {
config = binding.value as PopupConfig;
} else {
config = {
type: PopupType.TEXT,
type: "TEXT",
value: binding.value as any as string,
canShow: () => true
};
@@ -58,24 +53,24 @@ const VPopup: Directive = {
popup.appendChild(el);
}
switch (config.type) {
case PopupType.TEXT:
case "TEXT":
// 文本
el = document.createElement("div");
el.className = "text";
el.textContent = config.value as string;
popup.appendChild(el);
break;
case PopupType.IMG:
case "IMG":
// 图片
el = document.createElement("img");
(el as HTMLImageElement).src = config.value as string;
popup.appendChild(el);
break;
case PopupType.HTML:
case "HTML":
// HTML 字符串
popup.appendChild(Toolkit.toDOM(config.value as string));
break;
case PopupType.EL:
case "ELEMENT":
// DOM 节点
if (config.value instanceof HTMLElement) {
const valueEl = config.value as HTMLElement;