From efea6243764d45e275c19c9ae0ad151e0182129f Mon Sep 17 00:00:00 2001 From: Timi Date: Sat, 1 Aug 2026 17:35:48 +0800 Subject: [PATCH] v0.0.24 --- package.json | 2 +- src/api/RoleAPI.ts | 9 ++- src/assets/style/timi-web.less | 10 +-- src/types/Model.ts | 5 +- src/utils/Network.ts | 4 +- src/utils/Scroller.ts | 141 ++++++++++++++++++++++++++------- 6 files changed, 132 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 3688b06..a4d66d5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "module": "./dist/timi-web.mjs", "style": "./dist/timi-web.css", "private": false, - "version": "0.0.23", + "version": "0.0.24", "license": "MIT", "scripts": { "dev": "vite", diff --git a/src/api/RoleAPI.ts b/src/api/RoleAPI.ts index dbce1bd..bab927c 100644 --- a/src/api/RoleAPI.ts +++ b/src/api/RoleAPI.ts @@ -1,5 +1,5 @@ import { axios } from "./BaseAPI"; -import type { Page, PageResult, Permission, Role, RolePayload, UserRoleAuthorizeReq } from "../types"; +import type { ModuleCode, Page, PageResult, Permission, Role, RolePayload, UserRoleAuthorizeReq } from "../types"; const BASE_URI = "/user/role"; @@ -27,9 +27,12 @@ async function remove(id: string): Promise { }); } -async function authorizedList(userId: string): Promise { +async function authorizedList(userId: string, moduleCode?: ModuleCode): Promise { return axios.post(`${BASE_URI}/authorized/list`, undefined, { - params: { userId } + params: { + userId, + moduleCode + } }); } diff --git a/src/assets/style/timi-web.less b/src/assets/style/timi-web.less index 6d926b5..2a759c9 100644 --- a/src/assets/style/timi-web.less +++ b/src/assets/style/timi-web.less @@ -103,23 +103,23 @@ textarea { -webkit-filter: grayscale(1); } -.bold { +.text-bold { font-weight: bold !important; } -.italic { +.text-italic { font-style: italic !important; } -.delete { +.text-delete { text-decoration: line-through !important; } -.underline { +.text-underline { text-decoration: underline !important; } -.not-underline { +.text-not-underline { text-decoration: none !important; } diff --git a/src/types/Model.ts b/src/types/Model.ts index 72a4cdd..2828de3 100644 --- a/src/types/Model.ts +++ b/src/types/Model.ts @@ -15,7 +15,10 @@ export type Model = { export type ApiResponse = { code: number; - msg?: string; + message: string; + messageCode: string; + success: boolean; + fail: boolean; data: T; } diff --git a/src/utils/Network.ts b/src/utils/Network.ts index 00c1646..b3f79a6 100644 --- a/src/utils/Network.ts +++ b/src/utils/Network.ts @@ -62,13 +62,13 @@ const applyDefaultInterceptors = (instance: AxiosInstance) => { status: axiosResp?.status, response: axiosResp, serverResponse: serverResp, - error: serverResp.msg + error: serverResp.message }); if (globalErrorCallback) { globalErrorCallback(serverResp); } // 由调用方处理 - return Promise.reject(serverResp.msg); + return Promise.reject(serverResp.message); } } notifyResponseObserver({ diff --git a/src/utils/Scroller.ts b/src/utils/Scroller.ts index 0725915..af7e1ee 100644 --- a/src/utils/Scroller.ts +++ b/src/utils/Scroller.ts @@ -3,6 +3,7 @@ export type ScrollListener = { source: Event; viewWidth: number; viewHeight: number; + scrollHeight: number; top: number; bottom: number; } @@ -14,35 +15,64 @@ export type ScrollBottomListenerOptions = { /** 返回 true 时才触发 */ canLoad?: () => boolean; + + /** 自定义滚动容器,默认 window */ + getElement?: () => HTMLElement | null | undefined; + } export default class Scroller { private static instance: Scroller; - listeners = new Map(); + listeners = new Map(); - private constructor() { + private constructor() {} - window.addEventListener("scroll", source => { - // 滚动距离 - const top = document.body.scrollTop || document.documentElement.scrollTop; - // 可视高度 - const viewWidth = document.documentElement.clientWidth || document.body.clientWidth; - // 可视高度 - const viewHeight = document.documentElement.clientHeight || document.body.clientHeight; - // 滚动高度 - const sH = document.documentElement.scrollHeight || document.body.scrollHeight; - // 触发事件 - const bottom = sH - top - viewHeight; - this.listeners.forEach(listener => listener({ - source, - viewWidth, - viewHeight, - top, - bottom - } as ScrollListener)); - }, true); + private static waitForLayout(): Promise { + return new Promise(resolve => { + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()); + }); + }); + } + + private static getScrollState(element?: HTMLElement | null, source?: Event): ScrollListener { + if (element) { + return { + source: source || new Event("scroll"), + viewWidth: element.clientWidth, + viewHeight: element.clientHeight, + scrollHeight: element.scrollHeight, + top: element.scrollTop, + bottom: element.scrollHeight - element.scrollTop - element.clientHeight + }; + } + const top = document.body.scrollTop || document.documentElement.scrollTop; + const viewWidth = document.documentElement.clientWidth || document.body.clientWidth; + const viewHeight = document.documentElement.clientHeight || document.body.clientHeight; + const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; + return { + source: source || new Event("scroll"), + viewWidth, + viewHeight, + scrollHeight, + top, + bottom: scrollHeight - top - viewHeight + }; + } + + private static bindTarget(name: string, target: HTMLElement | Window, listener: EventListener) { + target.addEventListener("scroll", listener, { + passive: true + }); + Scroller.getInstance().listeners.set(name, { + target, + listener + }); } private static getInstance(): Scroller { @@ -58,8 +88,24 @@ export default class Scroller { * @param name 事件名 * @param listener 监听事件 */ - public static addListener(name: string, listener: (event: ScrollListener) => void) { - Scroller.getInstance().listeners.set(name, listener); + public static addListener(name: string, listener: (event: ScrollListener) => void, getElement?: () => HTMLElement | null | undefined) { + Scroller.removeListener(name); + const targetElement = getElement?.(); + if (getElement && !targetElement) { + return; + } + const target = targetElement || window; + const eventListener: EventListener = source => { + if (!targetElement) { + const eventTarget = source.target; + const isWindowScrollTarget = eventTarget === document || eventTarget === document.documentElement || eventTarget === document.body; + if (!isWindowScrollTarget) { + return; + } + } + listener(Scroller.getScrollState(targetElement, source)); + }; + Scroller.bindTarget(name, target, eventListener); } /** @@ -71,15 +117,51 @@ export default class Scroller { */ public static addBottomListener(name: string, listener: (event: ScrollListener) => void | Promise, options: ScrollBottomListenerOptions = {}) { const distance = options.distance ?? 120; - Scroller.addListener(name, event => { - if (distance < event.bottom) { + let loading = false; + let armed = true; + const triggerLoad = async (event: ScrollListener): Promise => { + const inRange = event.bottom <= distance; + if (!inRange) { + armed = true; + return; + } + if (loading) { + return; + } + if (!armed) { return; } if (options.canLoad && !options.canLoad()) { return; } - void listener(event); - }); + armed = false; + loading = true; + try { + let currentEvent = event; + while (true) { + const previousScrollHeight = currentEvent.scrollHeight; + await listener(currentEvent); + await Scroller.waitForLayout(); + if (options.canLoad && !options.canLoad()) { + break; + } + currentEvent = Scroller.getScrollState(options.getElement?.()); + if (distance < currentEvent.bottom) { + armed = true; + break; + } + if (currentEvent.scrollHeight <= previousScrollHeight) { + armed = true; + break; + } + } + } finally { + loading = false; + } + }; + Scroller.addListener(name, event => { + void triggerLoad(event); + }, options.getElement); } /** @@ -88,6 +170,11 @@ export default class Scroller { * @param name 事件名 */ public static removeListener(name: string) { + const entry = Scroller.getInstance().listeners.get(name); + if (!entry) { + return; + } + entry.target.removeEventListener("scroll", entry.listener); Scroller.getInstance().listeners.delete(name); }