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);
+6
View File
@@ -1,5 +1,11 @@
# @r-utils/uni-app
## 2.0.2
### Patch Changes
- 修复vue3未导出
## 2.0.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@r-utils/uni-app",
"version": "2.0.1",
"version": "2.0.2",
"private": false,
"description": "uni-app工具库",
"type": "module",
+47
View File
@@ -1,17 +1,56 @@
/*
* @file \src\printer\index.ts
* @description 蓝牙打印工具,最后打印的一步,组合蓝牙信息与打印数据发送
* @author tsl (randy1924@163.com)
* @date 2026-05-28 11:13:40
* @lastModified 2026-06-30 11:02:16
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { wait } from "@r-utils/common";
import { BluetoothUtils, Device } from "@/bluetooth-utils";
/** 蓝牙设备完整数据,包含设备信息和所有必需的特征值 ID */
type DeviceData = Required<Device>;
/**
* 蓝牙打印机工具类
*
* 将蓝牙设备信息与打印数据组合,通过 BLE 写入特征值发送 ESC/POS 打印指令。
* 支持安卓原生发送和通用 BLE 写入两种方式,自动根据平台选择。
* 打印数据会按 size 分包发送,每包之间等待 30ms 以确保数据传输稳定。
*
* @example
* ```ts
* const printer = new Printer(device, { size: 80 })
* await printer.print(escData) // escData 为 ESC/POS 指令的 number 数组
* ```
*/
export class Printer {
/** 蓝牙设备信息,包含 deviceId、服务 ID 和特征值 ID 等 */
device: DeviceData;
/** 每次写入 BLE 的最大字节数,默认 80 */
size = 0;
/**
* @param device 蓝牙设备完整数据(必须包含所有特征值 ID)
* @param size 每次写入的最大字节数,默认 80
*/
constructor(device: DeviceData, { size = 80 } = {}) {
this.device = device;
this.size = size;
}
/**
* 发送打印数据
*
* 根据平台自动选择发送方式:
* - 安卓 App 端:使用 BluetoothUtils.sendDataAndroid 原生发送
* - 其他平台:通过 BLE 写入特征值发送
*
* @param data ESC/POS 打印指令数组
* @returns 发送结果
*/
async print(data: number[]) {
console.log(
data.length,
@@ -32,6 +71,14 @@ export class Printer {
return res;
}
/**
* 通过 BLE 写入特征值分包发送打印数据(内部方法)
*
* 将数据按 size 分包,每包创建 ArrayBuffer 写入 BLE 特征值,
* 包间等待 30ms 确保传输稳定,递归发送直到所有数据写完。
*
* @param data 剩余待发送的打印数据
*/
async _print(data: number[]): Promise<void> {
const size = Math.min(data.length, this.size);
if (size === 0) {
+9
View File
@@ -1,3 +1,12 @@
/*
* @file \src\request\Request.ts
* @description uniapp请求类。建议改用 alova[https://alova.js.org/zh-CN/]
* @author tsl (randy1924@163.com)
* @date 2026-05-28 11:13:40
* @lastModified 2026-06-30 10:14:09
* @lastModifiedBy tsl (randy1924@163.com)
*/
export type DataType = string | AnyObject | ArrayBuffer;
/** 请求配置 */
+9
View File
@@ -1,3 +1,12 @@
/*
* @file \src\upload\index.ts
* @description uniapp上传文件工具
* @author tsl (randy1924@163.com)
* @date 2026-05-28 11:13:40
* @lastModified 2026-06-30 10:54:55
* @lastModifiedBy tsl (randy1924@163.com)
*/
type UploadResponseData = {
originalFileName: string;
url: string;
@@ -1,9 +1,9 @@
/*
* @file /src/vue3/hooks/components.ts
* @description
* @description uniapp组件hook
* @author tsl (randy1924@163.com)
* @date 2026-03-19 11:11:11
* @lastModified 2026-03-26 09:16:44
* @lastModified 2026-06-30 10:55:21
* @lastModifiedBy tsl (randy1924@163.com)
*/
+2 -2
View File
@@ -1,9 +1,9 @@
/*
* @file /src/hooks/list.ts
* @description
* @description 列表加载更多Hook
* @author tsl (randy1924@163.com)
* @date 2026-03-19 11:11:11
* @lastModified 2026-03-20 10:43:38
* @lastModified 2026-06-30 10:57:44
* @lastModifiedBy tsl (randy1924@163.com)
*/
+71 -12
View File
@@ -1,9 +1,37 @@
/*
* @file \src\vue-helper\index.ts
* @description vue3 帮助工具
* @author tsl (randy1924@163.com)
* @date 2026-05-28 11:13:40
* @lastModified 2026-06-30 11:00:29
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { ComponentPublicInstance } from "vue";
/** Vue class 对象格式,键为类名,值为布尔值表示是否生效 */
type CustomClassObj = Record<string, boolean>;
/** Vue class 支持的类型:字符串、字符串数组或对象 */
type CustomClass = string | Array<string> | CustomClassObj;
/** 分解后的 Vue class 对象,所有类名值为 true */
type DistCustomClass = Record<string, true>;
/**
* 将字符串或字符串数组转换为 Vue class 对象
*
* 将类名字符串或数组转换为 `{ className: true }` 格式的对象,
* 适用于 Vue 的 class 绑定。
*
* @example
* ```ts
* createCustomClassObj('foo bar') // { foo: true, bar: true }
* createCustomClassObj(['foo', 'bar']) // { foo: true, bar: true }
* ```
*
* @param customClass 类名字符串或字符串数组
* @returns 类名对象,所有值为 true
* @throws 若 customClass 不是字符串或数组则抛出 TypeError
*/
export function createCustomClassObj(customClass: string): DistCustomClass;
export function createCustomClassObj(
customClass: Array<string>,
@@ -35,9 +63,21 @@ export function createCustomClassObj(
}
/**
* 转换 vue class 为 vue class 对象
* @param sourceClass 原始 class
* @returns
* 转换 Vue class 为 Vue class 对象
*
* 支持字符串、数组和对象三种 Vue class 格式,统一转换为
* `{ className: boolean }` 格式的对象。
*
* @example
* ```ts
* convertCustomClass('foo bar') // { foo: true, bar: true }
* convertCustomClass(['foo', 'bar']) // { foo: true, bar: true }
* convertCustomClass({ foo: true, bar: false }) // { foo: true, bar: false }
* ```
*
* @param sourceClass 原始 Vue class 值,支持字符串、数组或对象
* @returns Vue class 对象
* @throws 若 sourceClass 不是有效的 Vue class 则抛出 TypeError
*/
export function convertCustomClass(sourceClass: CustomClass): CustomClassObj {
let customClassObj = <CustomClassObj>{};
@@ -66,9 +106,19 @@ export function convertCustomClass(sourceClass: CustomClass): CustomClassObj {
}
/**
* 合并 class
* @param customClass vue class
* @returns
* 合并多个 Vue class 值为一个对象
*
* 将多个 Vue class(字符串、数组或对象)统一转换为对象后合并,
* 后面的 class 值会覆盖前面的同名属性。
*
* @example
* ```ts
* mergeClass('foo', ['bar'], { baz: true })
* // { foo: true, bar: true, baz: true }
* ```
*
* @param customClass 一个或多个 Vue class 值
* @returns 合并后的 Vue class 对象
*/
export function mergeClass(...customClass: CustomClass[]) {
return customClass
@@ -77,12 +127,21 @@ export function mergeClass(...customClass: CustomClass[]) {
}
/**
* 发送祖先组件事件
* @param thisArg 调用的源组件实例
* @param componentName 需要触发事件的组件名
* @param eventName 事件名称
* @param params 参数
* @returns
* 向上遍历祖先组件,找到指定名称的组件并触发其事件
*
* 类似 Vue2 的 dispatch 模式,从当前组件开始沿父组件链向上查找,
* 直到找到 componentName 匹配的祖先组件,然后触发其指定事件。
*
* @example
* ```ts
* dispatch(this, 'FormComponent', 'validate', { field: 'name' })
* // 向上查找名为 FormComponent 的祖先组件,触发其 validate 事件
* ```
*
* @param thisArg 调用源的组件实例
* @param componentName 目标祖先组件的 componentName 选项值
* @param eventName 要触发的事件名称
* @param params 传递给事件的参数
*/
export function dispatch(
thisArg: ComponentPublicInstance,