feat(playground): 新增示例项目

This commit is contained in:
2026-06-30 11:21:16 +08:00
parent 4759dfe6b2
commit c22b72ac7b
78 changed files with 9058 additions and 213 deletions
+26 -13
View File
@@ -9,19 +9,32 @@
export type InputValue = string | number | null;
export interface InputOptions {
min?: number; // 最小值
max?: number; // 最大值
init?: string | number; // 初始值
digits?: number; // 小数位数
pattern?: string | RegExp; // 匹配模式
required?: boolean; // 是否必填
integer?: boolean; // 是否整数
number?: boolean; // 是否转成数字
text?: boolean; // 是否使用文本模式
floor?: boolean; // 是否向下取数
ceil?: boolean; // 是否向上取数
keepDecimal?: boolean; // 是否保留小数,即使是小数位是0
noSign?: boolean; // 不允许输入正负号
/** 最小值 */
min?: number;
/** 最大值 */
max?: number;
/** 初始值,当输入非法且 required 为 true 时回退到此值 */
init?: string | number;
/** 小数位数,设置后会对结果进行精度处理 */
digits?: number;
/** 匹配模式,文本模式下用于校验输入是否匹配该正则 */
pattern?: string | RegExp;
/** 是否必填,为 true 时非法输入将回退到 init 或 min */
required?: boolean;
/** 是否取整,为 true 时结果将取整 */
integer?: boolean;
/** 是否将结果转为数字类型 */
number?: boolean;
/** 是否使用文本模式,文本模式只做 pattern 校验和必填回退 */
text?: boolean;
/** 是否向下取整 */
floor?: boolean;
/** 是否向上取整 */
ceil?: boolean;
/** 是否保留小数位,为 true 时即使小数位为 0 也会保留指定位数(需配合 digits 使用) */
keepDecimal?: boolean;
/** 是否不允许输入正负号 */
noSign?: boolean;
}
/**
+80 -3
View File
@@ -1,31 +1,80 @@
/*
* @file \src\knock-test\KnockTest.ts
* @description 敲击工具,根据点击的次数、延迟执行回调
* @author tsl (randy1924@163.com)
* @date 2026-02-09 18:26:16
* @lastModified 2026-06-30 10:05:44
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { debounce } from "lodash-es";
/** 操作项,定义一次敲击阶段的参数 */
export class Operation {
/** 敲击有效时长(ms),超过该时长未达到指定次数则重置 */
duration = 1000;
/** 当前操作完成后进入下一操作前的等待时长(ms),等待期间点击会重置 */
delay = 1000;
/** 当前操作需要敲击的次数 */
times = 1;
}
/** KnockTest 配置项 */
export class Config {
/** 最大等待时间(ms),从首次敲击开始计算,超时未完成所有操作则自动重置 */
maxWaitTime = 5000;
/** 操作项列表,按顺序执行 */
operations: Operation[] = [];
}
/**
* 敲击测试工具
*
* 通过按顺序执行多阶段敲击操作来触发回调,类似安卓开发者模式的连续点击触发。
*
* 状态流转:
* - idle(空闲) → 首次敲击进入 knocking
* - knocking(敲击中) → 达到指定次数进入 wait
* - wait(等待) → 等待延迟结束后回到 idle;等待期间再次敲击则重置
*
* @example
* ```ts
* const kt = new KnockTest({
* maxWaitTime: 10000,
* operations: [
* { duration: 1000, delay: 500, times: 3 }, // 1秒内敲3次
* { duration: 1000, delay: 500, times: 2 }, // 然后等待0.5秒后1秒内敲2次
* ]
* })
* kt.addCallback(() => console.log('解锁成功'))
* kt.knock() // 在UI点击事件中调用
* ```
*/
export class KnockTest {
/** 配置项 */
config: Config;
/** 当前操作项索引 */
index = 0;
/** 当前阶段已敲击次数 */
times = 0;
/**
* 空闲状态:"idle"times从0开始计数,进入duration期
* duration期间:"knocking"times达到Operation的指定次数后进入delay
* delay期间:"wait",此期间不能点击,如果点击则重置,否则进入下一个空闲状态
* 当前状态:
* - "idle" 空闲,times从0开始计数,进入duration
* - "knocking" 敲击中,times达到Operation的指定次数后进入delay期
* - "wait" 等待,此期间不能点击,如果点击则重置,否则进入下一个空闲状态
*/
status: "idle" | "knocking" | "wait" = "idle";
/** duration 定时器 ID */
durationTimerId: number | null = null;
/** delay 定时器 ID */
delayTimerId: number | null = null;
/** 成功回调列表,所有操作完成后依次执行 */
callbackList: (() => void)[] = [];
/** 最大等待防抖函数,超时自动重置 */
maxWaitFun: () => void;
/**
* @param config 配置项,未指定的字段使用默认值
*/
constructor(config: Config) {
const c = new Config();
this.config = Object.assign(c, config);
@@ -36,10 +85,22 @@ export class KnockTest {
}, this.config.maxWaitTime);
}
/**
* 添加成功回调,所有操作按序完成后触发
* @param cb 回调函数
*/
addCallback(cb: () => void) {
this.callbackList.push(cb);
}
/**
* 敲击一次,在 UI 点击事件中调用
*
* 每次调用会使 times + 1,并根据当前状态决定后续行为:
* - idle → 开始新的敲击阶段
* - knocking → 检查是否达到当前操作指定次数
* - wait → 重置所有状态(等待期间不允许敲击)
*/
knock() {
console.log("knock");
this.maxWaitFun();
@@ -60,6 +121,7 @@ export class KnockTest {
}
}
/** 重置所有状态,回到初始空闲状态并清除所有定时器 */
private reset() {
this.status = "idle";
this.times = 0;
@@ -75,6 +137,11 @@ export class KnockTest {
}
}
/**
* 处理空闲状态的敲击:启动新的敲击阶段,设置 duration 定时器
* 超时未达到指定次数则自动重置
* @throws 当操作项列表为空时抛出错误
*/
private handleIdle() {
if (this.config.operations.length === 0) {
throw new Error("至少添加一个操作项");
@@ -92,10 +159,17 @@ export class KnockTest {
this.checkKnock();
}
/**
* 处理敲击中状态:检查当前敲击次数是否满足操作项要求
*/
private handleKnocking() {
this.checkKnock();
}
/**
* 检查当前敲击次数是否达到操作项要求,若达到则进入等待状态
* 若已完成所有操作项,则触发回调并重置
*/
private checkKnock() {
const operation = this.config.operations[this.index];
@@ -127,6 +201,9 @@ export class KnockTest {
}
}
/**
* 处理等待状态的敲击:等待期间不允许敲击,直接重置
*/
private handleWait() {
if (this.delayTimerId != null) {
clearTimeout(this.delayTimerId);
+40 -3
View File
@@ -1,17 +1,54 @@
/*
* @file \src\permission\Permission.ts
* @description 权限工具
* @author tsl (randy1924@163.com)
* @date 2023-11-27 13:35:18
* @lastModified 2026-06-30 10:06:50
* @lastModifiedBy tsl (randy1924@163.com)
*/
/**
* 权限工具类
*
* 用于管理和校验权限字符串格式,权限字符串采用多级分段格式,
* 例如 "module:action:resource",默认使用 ":" 作为分隔符,默认 3 级。
*
* @example
* ```ts
* const permission = new Permission(['user:read:info', 'admin:write:config'], 3, ':')
* permission.isValid('user:read:info') // true
* permission.isValid('user:read') // false,级数不足
* permission.isValid('user read info') // false,分隔符不匹配
* ```
*/
export class Permission {
/**
*
*/
/** 权限字符串列表 */
list: string[];
/** 权限分隔符,默认为 ":" */
separator: string;
/** 权限级数,默认为 3(如 "module:action:resource" */
level: number;
/**
* @param list 权限字符串列表
* @param level 权限级数,默认 3
* @param separator 分隔符,默认 ":"
*/
constructor(list: string[], level = 3, separator = ":") {
this.list = list;
this.level = level;
this.separator = separator;
}
/**
* 校验权限字符串格式是否合法
*
* 检查字符串是否符合指定的级数和分隔符格式,
* 例如 level=3、separator=":" 时,"module:action:resource" 合法,"module:action" 不合法。
*
* @param str 待校验的权限字符串
* @returns 是否合法
*/
isValid(str: string) {
const p = new Array(this.level).fill("\\w+?").join(this.separator);
// ^\w+?:\w+?:\w+?$
+43 -10
View File
@@ -3,20 +3,47 @@ import { Duration } from "dayjs/plugin/duration";
type TimerCallback = (time: string) => void;
/**
* 倒计时工具类
*
* 基于 dayjs duration 的倒计时,支持步骤监听和完成监听。
* 每秒递减一次,并通过 stepEventListener 回调当前格式化时间;
* 倒计时归零时触发 countdownEventListener 回调。
*
* @example
* ```ts
* const countdown = new Countdown(60000) // 60秒倒计时
* countdown.format = 'mm:ss'
* countdown.addStepEventListener((time) => console.log('剩余:', time))
* countdown.addCountdownEventListener((time) => console.log('倒计时结束:', time))
* countdown.start()
* ```
*/
export default class Countdown {
/** 定时器 ID,-1 表示未运行 */
id = -1;
/** dayjs duration 对象,表示剩余时长 */
duration: Duration;
/** 输出时间格式,默认 "HH:mm:ss" */
format = "HH:mm:ss";
/**
* 倒计时步骤监听器
* 倒计时步骤监听器列表
*
* 每秒触发一次,参数为当前格式化后的剩余时间字符串
*/
stepEventListenerList = <TimerCallback[]>[];
/**
* 倒计时完成时间监听器
* 倒计时完成监听器列表
*
* 倒计时归零时触发,参数为最终的格式化时间字符串
*/
countdownEventListenerList = <TimerCallback[]>[];
/** 初始时间(毫秒),用于 restart 时重置 */
initialTime = 0;
/**
* @param time 倒计时总时长(毫秒)
*/
constructor(time: number) {
this.initialTime = time;
this.duration = dayjs.duration(time);
@@ -24,8 +51,11 @@ export default class Countdown {
/**
* 添加倒计时步骤监听器
* @param cb
* @returns
*
* 每秒触发一次回调,参数为当前格式化后的剩余时间字符串
*
* @param cb 步骤回调函数,参数为格式化时间字符串
* @returns 若 cb 不是函数则返回 TypeError
*/
addStepEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@@ -35,9 +65,12 @@ export default class Countdown {
}
/**
* 添加倒计时完成时间监听器
* @param cb
* @returns
* 添加倒计时完成监听器
*
* 倒计时归零时触发,参数为最终的格式化时间字符串
*
* @param cb 完成回调函数,参数为格式化时间字符串
* @returns 若 cb 不是函数则返回 TypeError
*/
addCountdownEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@@ -47,7 +80,7 @@ export default class Countdown {
}
/**
* 开始倒计时
* 开始倒计时,每秒递减一次并触发步骤监听器,归零时自动停止
*/
start() {
this.id = window.setInterval(() => {
@@ -63,7 +96,7 @@ export default class Countdown {
}
/**
* 结束倒计时
* 停止倒计时,清除定时器并触发完成监听器
*/
stop() {
console.log("stop", this.id);
@@ -75,7 +108,7 @@ export default class Countdown {
}
/**
* 重新开始计时
* 重新开始计时,重置为初始时间后重新启动
*/
restart() {
clearInterval(this.id);
+56 -19
View File
@@ -1,19 +1,49 @@
type TimerCallback = (time: number) => void;
/**
* 超时定时器
*
* 基于数值的倒计时器,从 startTime 递减到 endTime
* 每隔 interval 毫秒触发步骤监听器,到达 endTime 时触发完成监听器。
*
* 与 Countdown 不同的是,TimeoutTimer 使用数值而非 dayjs duration
* 适合不需要格式化输出的纯数值倒计时场景。
*
* @example
* ```ts
* // 实例方式
* const timer = new TimeoutTimer(10, 0, 1000)
* timer.addStepEventListener((time) => console.log('剩余:', time))
* timer.addCountdownEventListener((time) => console.log('结束:', time))
* timer.start()
*
* // 静态工厂方式
* const timer2 = TimeoutTimer.start(10, 0, 1000,
* (time) => console.log('剩余:', time),
* (time) => console.log('结束:', time),
* )
* ```
*/
export default class TimeoutTimer {
/** 起始时间(数值) */
startTime = 0;
/** 结束时间(数值),倒计时到此值停止 */
endTime = 0;
/** 递减间隔(毫秒) */
interval = 0;
/** 当前剩余时间 */
time = 0;
/** 定时器 ID,-1 表示未运行 */
id = -1;
/** 步骤监听器列表,每次递减时触发,参数为当前剩余时间 */
stepEventListenerList = <TimerCallback[]>[];
/** 完成监听器列表,倒计时结束时触发,参数为结束时间 */
countdownEventListenerList = <TimerCallback[]>[];
/**
*
* @param startTime 起始时间
* @param endTime 结束时间
* @param interval 间隔(ms)
* @param startTime 起始时间(数值)
* @param endTime 结束时间(数值),默认 0
* @param interval 递减间隔(毫秒),默认 1000
*/
constructor(startTime = 5, endTime = 0, interval = 1000) {
this.startTime = startTime;
@@ -22,9 +52,12 @@ export default class TimeoutTimer {
}
/**
* 倒计时步骤监听器
* @param cb
* @returns
* 添加步骤监听器
*
* 每次递减时触发,参数为当前剩余时间
*
* @param cb 步骤回调函数,参数为当前时间(数值)
* @returns 若 cb 不是函数则返回 TypeError
*/
addStepEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@@ -34,9 +67,12 @@ export default class TimeoutTimer {
}
/**
* 倒计时完成时间监听器
* @param cb
* @returns
* 添加完成监听器
*
* 倒计时到达 endTime 时触发,参数为结束时间
*
* @param cb 完成回调函数,参数为结束时间(数值)
* @returns 若 cb 不是函数则返回 TypeError
*/
addCountdownEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@@ -46,13 +82,14 @@ export default class TimeoutTimer {
}
/**
* 开始倒计时
* @param startTime
* @param endTime
* @param interval
* @param stepEventListener
* @param countdownEventListener
* @returns
* 静态工厂方法,创建并配置超时定时器
*
* @param startTime 起始时间(数值)
* @param endTime 结束时间(数值),默认 0
* @param interval 递减间隔(毫秒),默认 1000
* @param stepEventListener 步骤监听器(可选)
* @param countdownEventListener 完成监听器(可选)
* @returns 配置好的 TimeoutTimer 实例(需手动调用 start
*/
static start(
startTime: number,
@@ -73,7 +110,7 @@ export default class TimeoutTimer {
}
/**
* 开始倒计时
* 开始倒计时,从 startTime 递减到 endTime,到达后自动停止
*/
start() {
this.time = this.startTime;
@@ -88,7 +125,7 @@ export default class TimeoutTimer {
}
/**
* 结束倒计时
* 停止倒计时,清除定时器并触发完成监听器
*/
stop() {
console.log("stop", this.id);