Compare commits
8 Commits
@r-utils/c
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
0e68849e8e | |
|
|
fa3615bbcf | |
|
|
c22b72ac7b | |
|
|
4759dfe6b2 | |
|
|
f9eb267988 | |
|
|
18a037e9b7 | |
|
|
80debf6d6b | |
|
|
3a55edfafc |
|
|
@ -0,0 +1,8 @@
|
|||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "master",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
.turbo
|
||||
.changeset/pre.json
|
||||
*.bak
|
||||
|
|
|
|||
2
.npmrc
2
.npmrc
|
|
@ -1 +1,3 @@
|
|||
registry=http://npm.nps.yunvip123.cn
|
||||
engine-strict=true
|
||||
shamefully-hoist=true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# the name by which the project can be referenced within Serena
|
||||
project_name: "r-util-js"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
|
|
@ -26,7 +25,7 @@ project_name: "r-util-js"
|
|||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- typescript
|
||||
- typescript
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
|
|
@ -67,7 +66,7 @@ read_only: false
|
|||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
#
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"bumpFiles": [
|
||||
{ "filename": "package.json", "type": "json" },
|
||||
{ "filename": "packages/common/package.json", "type": "json" },
|
||||
{ "filename": "packages/vue3/package.json", "type": "json" },
|
||||
{ "filename": "packages/uni-app/package.json", "type": "json" },
|
||||
{ "filename": "packages/vue2/package.json", "type": "json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"psioniq.psi-header",
|
||||
"aaron-bond.better-comments",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
"recommendations": [
|
||||
"psioniq.psi-header",
|
||||
"aaron-bond.better-comments",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,6 @@
|
|||
}
|
||||
],
|
||||
"workbench.editor.limit.value": 3,
|
||||
"cSpell.words": [
|
||||
"Codice",
|
||||
"commitlint",
|
||||
"dcloudio",
|
||||
"Fabbrica"
|
||||
],
|
||||
"cSpell.words": ["Codice", "commitlint", "dcloudio", "Fabbrica"],
|
||||
"js/ts.tsdk.path": "node_modules\\typescript\\lib"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个前端工具库 monorepo,使用 **pnpm workspace + turbo** 管理,包含以下子包:
|
||||
|
||||
| 包名 | 路径 | 描述 |
|
||||
|------|------|------|
|
||||
| `@r-utils/common` | `packages/common` | 通用 JS/TS 工具库(不依赖框架) |
|
||||
| `@r-utils/vue3` | `packages/vue3` | Vue3 专用工具 |
|
||||
| `@r-utils/uni-app` | `packages/uni-app` | uni-app 专用工具 |
|
||||
| `@r-utils/vue2` | `packages/vue2` | Vue2 专用工具 |
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# 构建所有包
|
||||
pnpm build
|
||||
|
||||
# 构建单个包(Turbo filter)
|
||||
pnpm build -- --filter=@r-utils/common
|
||||
|
||||
# 构建单个包(在包目录内)
|
||||
cd packages/common && pnpm build
|
||||
```
|
||||
|
||||
> `@r-utils/common` 同时有 `webpack.config.js` 和 `rollup.config.js`,当前 `build` 脚本使用 webpack;rollup 配置备用。
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 在根目录运行所有测试
|
||||
pnpm test
|
||||
|
||||
# 运行单个测试文件
|
||||
pnpm vitest run packages/common/test/permission.test.ts
|
||||
```
|
||||
|
||||
测试文件位于各包的 `test/` 目录下,使用 Vitest。
|
||||
|
||||
### 代码检查与格式化
|
||||
|
||||
```bash
|
||||
# Lint(从根目录,仅检查 src 目录)
|
||||
pnpm lint
|
||||
|
||||
# 格式化
|
||||
pnpm format
|
||||
```
|
||||
|
||||
ESLint 使用 v9 flat config(`eslint.config.js`),已整合 TypeScript 和 Prettier 规则。
|
||||
|
||||
### Git 提交
|
||||
|
||||
```bash
|
||||
# 使用 commitizen 提交(遵循 conventional commits 规范)
|
||||
pnpm commit
|
||||
```
|
||||
|
||||
**注意**:提交时 husky pre-commit 钩子会自动运行 lint-staged 和 `pnpm test`,commit-msg 钩子会用 commitlint 校验提交信息格式。
|
||||
|
||||
### 发布
|
||||
|
||||
发布分四步:先记录 changeset,再校验与构建、更新版本,最后通过 `package.json` 命令发布。
|
||||
|
||||
```bash
|
||||
# 1. 记录变更
|
||||
pnpm changeset
|
||||
|
||||
# 2. 质量检查与构建
|
||||
pnpm release:check
|
||||
|
||||
# 3. 版本管理(更新受影响包版本号 + 生成 CHANGELOG)
|
||||
pnpm release
|
||||
|
||||
# 4. 发布到 npm
|
||||
pnpm release:publish
|
||||
```
|
||||
|
||||
也可以使用一键命令:
|
||||
|
||||
```bash
|
||||
pnpm release:all
|
||||
```
|
||||
|
||||
changesets 配置见 `.changeset/config.json`,完整发布说明见 `docs/RELEASE.md`。
|
||||
|
||||
## 架构说明
|
||||
|
||||
### Monorepo 结构
|
||||
|
||||
- 根 `package.json` 定义 devDependencies(构建工具、ESLint、Vitest 等),各子包在自身 `package.json` 中声明运行时依赖
|
||||
- `@r-utils/uni-app` 通过 `workspace:^` 依赖 `@r-utils/common`;其他包目前通过 `file:` 本地路径引用
|
||||
- 每个包均输出 ES 模块格式,入口为 `dist/index.js`,类型声明在 `dist/index.d.ts`
|
||||
|
||||
### 构建工具
|
||||
|
||||
- 各包使用 **webpack 5** + **ts-loader** 进行构建,输出 ES module(`experiments.outputModule: true`)
|
||||
- TypeScript 配置:`target: esnext`、`strict: true`、路径别名 `@` 指向 `src/`
|
||||
|
||||
### `@r-utils/common` 模块结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── permission/ Permission 类:权限字符串格式校验(如 "module:action:resource")
|
||||
├── timer/ Countdown(倒计时)、TimeoutTimer
|
||||
├── printer/ ESC 打印工具,PrintUtil 继承自 JpPrinter(esc.js)
|
||||
├── knock-test/ KnockTest:按顺序敲击/点击触发回调的状态机
|
||||
└── time/ 时间工具函数
|
||||
```
|
||||
|
||||
### `@r-utils/uni-app` 主要模块
|
||||
|
||||
- `request/Request.ts`:类 axios 的 uni.request 封装,支持请求/响应拦截器
|
||||
- `uni-helper.ts`:uni-app 常用工具(scanCode、toPromise、rpxToPx、pathToBase64、Canvas 工具等)
|
||||
- `bluetooth-utils/`、`nfc/`、`printer/`:蓝牙、NFC、打印机功能
|
||||
|
||||
### `@r-utils/vue3`
|
||||
|
||||
- `vue-helper.ts`:Vue class 处理工具(`mergeClass`、`createCustomClassObj`)、`dispatch` 祖先组件事件
|
||||
|
||||
### `@r-utils/vue2`
|
||||
|
||||
- `plugins/visibility.js`:Vue2 mixin 插件,为组件提供 `onShow`/`onHide` 生命周期钩子
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
All notable changes to this project will be documented in this file. Versioning is managed by `changesets`.
|
||||
|
||||
## [1.3.0](https://gitee.com/codice_fabbrica/r-util-js/compare/v1.2.1...v1.3.0) (2026-04-20)
|
||||
|
||||
|
|
|
|||
39
CLAUDE.md
39
CLAUDE.md
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## 项目概述
|
||||
|
||||
这是一个前端工具库 monorepo,使用 **pnpm workspace** 管理,包含以下子包:
|
||||
这是一个前端工具库 monorepo,使用 **pnpm workspace + turbo** 管理,包含以下子包:
|
||||
|
||||
| 包名 | 路径 | 描述 |
|
||||
|------|------|------|
|
||||
|
|
@ -21,6 +21,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
# 构建所有包
|
||||
pnpm build
|
||||
|
||||
# 构建单个包(Turbo filter)
|
||||
pnpm build -- --filter=@r-utils/common
|
||||
|
||||
# 构建单个包(在包目录内)
|
||||
cd packages/common && pnpm build
|
||||
```
|
||||
|
|
@ -34,10 +37,10 @@ cd packages/common && pnpm build
|
|||
pnpm test
|
||||
|
||||
# 运行单个测试文件
|
||||
npx jest packages/common/test/permission.test.ts
|
||||
pnpm vitest run packages/common/test/permission.test.ts
|
||||
```
|
||||
|
||||
测试文件位于各包的 `test/` 目录下,使用 Jest + Babel。
|
||||
测试文件位于各包的 `test/` 目录下,使用 Vitest。
|
||||
|
||||
### 代码检查与格式化
|
||||
|
||||
|
|
@ -58,29 +61,39 @@ ESLint 使用 v9 flat config(`eslint.config.js`),已整合 TypeScript 和
|
|||
pnpm commit
|
||||
```
|
||||
|
||||
**注意**:提交时 husky pre-commit 钩子会自动运行 lint-staged 和 jest 测试,commit-msg 钩子会用 commitlint 校验提交信息格式。
|
||||
**注意**:提交时 husky pre-commit 钩子会自动运行 lint-staged 和 `pnpm test`,commit-msg 钩子会用 commitlint 校验提交信息格式。
|
||||
|
||||
### 发布
|
||||
|
||||
发布分两步:先用 `standard-version` 管理版本,再用脚本构建和发布。
|
||||
发布分四步:先记录 changeset,再校验与构建、更新版本,最后通过 `package.json` 命令发布。
|
||||
|
||||
```bash
|
||||
# 1. 版本管理(更新版本号 + 生成 CHANGELOG + git commit/tag)
|
||||
pnpm release # 默认 patch
|
||||
npx standard-version --release-as minor # 指定版本类型
|
||||
# 1. 记录变更
|
||||
pnpm changeset
|
||||
|
||||
# 2. 构建并发布到 npm
|
||||
bash scripts/publish.sh # Linux/macOS
|
||||
powershell scripts/publish.ps1 # Windows
|
||||
# 2. 质量检查与构建
|
||||
pnpm release:check
|
||||
|
||||
# 3. 版本管理(更新受影响包版本号 + 生成 CHANGELOG)
|
||||
pnpm release
|
||||
|
||||
# 4. 发布到 npm
|
||||
pnpm release:publish
|
||||
```
|
||||
|
||||
版本配置见 `.versionrc.json`,会同步更新根包和所有子包的版本号。
|
||||
也可以使用一键命令:
|
||||
|
||||
```bash
|
||||
pnpm release:all
|
||||
```
|
||||
|
||||
changesets 配置见 `.changeset/config.json`,完整发布说明见 `docs/RELEASE.md`。
|
||||
|
||||
## 架构说明
|
||||
|
||||
### Monorepo 结构
|
||||
|
||||
- 根 `package.json` 定义 devDependencies(构建工具、ESLint、Jest 等),各子包在自身 `package.json` 中声明运行时依赖
|
||||
- 根 `package.json` 定义 devDependencies(构建工具、ESLint、Vitest 等),各子包在自身 `package.json` 中声明运行时依赖
|
||||
- `@r-utils/uni-app` 通过 `workspace:^` 依赖 `@r-utils/common`;其他包目前通过 `file:` 本地路径引用
|
||||
- 每个包均输出 ES 模块格式,入口为 `dist/index.js`,类型声明在 `dist/index.d.ts`
|
||||
|
||||
|
|
|
|||
65
README.md
65
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# r-utils
|
||||
|
||||
前端工具库 monorepo,使用 pnpm workspace 管理,Vite 构建。
|
||||
前端工具库 monorepo,使用 pnpm workspace + Turbo 管理任务,Vite 构建。
|
||||
|
||||
## 包列表
|
||||
|
||||
|
|
@ -8,14 +8,14 @@
|
|||
| ---- | ---- | ---- |
|
||||
| `@r-utils/common` | `packages/common` | 通用 JS/TS 工具(权限、倒计时、打印、时间等) |
|
||||
| `@r-utils/vue3` | `packages/vue3` | Vue3 工具(class 处理、dispatch 等) |
|
||||
| `@r-utils/uni-app` | `packages/uni-app` | uni-app 工具(请求封装、蓝牙、NFC、打印等) |
|
||||
| `@r-utils/uni-app` | `packages/uni-app` | uni-app 工具(请求封装、蓝牙、NFC、打印、Vue3 Hooks 等) |
|
||||
| `@r-utils/uview-plus` | `packages/uview-plus` | uview-plus 组合式 API Hooks(Picker、Calendar 等) |
|
||||
| `@r-utils/vue2` | `packages/vue2` | Vue2 工具(visibility 插件) |
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js >= 18.12.0
|
||||
- pnpm >= 8.15.6
|
||||
- pnpm >= 10.0.0
|
||||
|
||||
## 安装依赖
|
||||
|
||||
|
|
@ -31,6 +31,14 @@ pnpm install
|
|||
pnpm build
|
||||
```
|
||||
|
||||
Turbo 会自动按依赖拓扑排序构建(例如 `@r-utils/common` 会先于依赖它的包构建),并复用缓存加速重复任务。
|
||||
|
||||
### 构建单个包(Turbo 过滤)
|
||||
|
||||
```bash
|
||||
pnpm build -- --filter=@r-utils/common
|
||||
```
|
||||
|
||||
### 构建单个包
|
||||
|
||||
```bash
|
||||
|
|
@ -52,8 +60,11 @@ dist/
|
|||
### 监听模式
|
||||
|
||||
```bash
|
||||
cd packages/common
|
||||
pnpm watch
|
||||
# 监听所有包
|
||||
pnpm build:watch
|
||||
|
||||
# 仅监听单个包
|
||||
pnpm build:watch -- --filter=@r-utils/common
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
|
@ -75,6 +86,9 @@ import { mergeClass, dispatch } from '@r-utils/vue3';
|
|||
// uni-app 工具
|
||||
import { Request } from '@r-utils/uni-app';
|
||||
|
||||
// uni-app Vue3 Hooks
|
||||
import { useShare } from '@r-utils/uni-app/vue3';
|
||||
|
||||
// uview-plus Hooks
|
||||
import { usePickerSingle, usePicker, useCalendar } from '@r-utils/uview-plus';
|
||||
|
||||
|
|
@ -114,7 +128,7 @@ const { Permission } = require('@r-utils/common');
|
|||
pnpm test
|
||||
|
||||
# 运行单个测试文件
|
||||
npx jest packages/common/test/permission.test.ts
|
||||
pnpm vitest run packages/common/test/permission.test.ts
|
||||
```
|
||||
|
||||
### 代码检查与格式化
|
||||
|
|
@ -136,50 +150,27 @@ pnpm commit
|
|||
提交时 husky 会自动运行:
|
||||
|
||||
- `lint-staged` - 代码检查和格式化
|
||||
- `jest` - 单元测试
|
||||
- `pnpm test`(Vitest) - 单元测试
|
||||
- `commitlint` - 提交信息格式校验
|
||||
|
||||
详细说明请参考 [提交规范指南](./docs/COMMIT_GUIDE.md)。
|
||||
|
||||
## 发布
|
||||
|
||||
### 1. 版本管理(standard-version)
|
||||
发布流程已提取到独立文档:[发布流程](./docs/RELEASE.md)。
|
||||
|
||||
使用 `standard-version` 更新版本号、生成 CHANGELOG、创建 Git commit 和 tag。版本配置见 `.versionrc.json`,会同步更新根包和所有子包的版本号。
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
# 默认 patch 版本
|
||||
pnpm release
|
||||
|
||||
# 指定版本类型
|
||||
npx standard-version --release-as minor # 1.0.0 -> 1.1.0
|
||||
npx standard-version --release-as major # 1.0.0 -> 2.0.0
|
||||
pnpm changeset # 记录变更
|
||||
pnpm release:check # 测试 + 构建
|
||||
pnpm release # 更新版本号与 CHANGELOG
|
||||
pnpm release:publish # 发布到 npm 仓库
|
||||
pnpm release:all # 一键执行检查 + 版本更新 + 发布
|
||||
```
|
||||
|
||||
### 2. 构建并发布到私有 npm 仓库
|
||||
|
||||
项目已配置发布到私有 npm 仓库 `http://npm.nps.yunvip123.cn`。
|
||||
|
||||
#### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
powershell scripts/publish.ps1
|
||||
```
|
||||
|
||||
#### Linux/macOS (Bash)
|
||||
|
||||
```bash
|
||||
bash scripts/publish.sh
|
||||
```
|
||||
|
||||
发布脚本会自动执行以下步骤:
|
||||
|
||||
1. 检查 Git 工作区状态
|
||||
2. 运行测试(确保代码质量)
|
||||
3. 构建所有子包
|
||||
4. 依次发布 `@r-utils/common`、`@r-utils/vue3`、`@r-utils/uni-app`、`@r-utils/vue2`
|
||||
5. 询问是否推送到远程仓库
|
||||
|
||||
### 安装已发布的包
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -59,8 +59,10 @@ module.exports = {
|
|||
prompt: {
|
||||
useEmoji: true,
|
||||
emojiAlign: "center",
|
||||
allowCustomIssuePrefix: false,
|
||||
allowEmptyIssuePrefix: false,
|
||||
allowCustomIssuePrefix: true,
|
||||
allowEmptyIssuePrefix: true,
|
||||
emptyIssuePrefixAlias: "跳过",
|
||||
customIssuePrefixAlias: "自定义",
|
||||
// 定义 scope 选项
|
||||
scopes: [
|
||||
{ value: "common", name: "common: @r-utils/common 包" },
|
||||
|
|
@ -69,45 +71,74 @@ module.exports = {
|
|||
{ value: "uni-app", name: "uni-app: @r-utils/uni-app 包" },
|
||||
{ value: "deps", name: "deps: 依赖更新" },
|
||||
{ value: "release", name: "release: 版本发布" },
|
||||
{ value: "playground", name: "playground: 示例/调试项目" },
|
||||
],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
// 破坏性变更默认跳过;如需声明可手动在提交信息中添加 BREAKING CHANGE
|
||||
allowBreakingChanges: [],
|
||||
messages: {
|
||||
type: '选择你要提交的类型:',
|
||||
scope: '选择一个提交范围(可选):',
|
||||
customScope: '请输入自定义范围:',
|
||||
subject: '请简短描述这次改动:\n',
|
||||
type: "选择你要提交的类型:",
|
||||
scope: "选择一个提交范围(可选):",
|
||||
customScope: "请输入自定义范围:",
|
||||
subject: "请简短描述这次改动:\n",
|
||||
body: '请详细描述这次改动(可选,用 "|" 换行):\n',
|
||||
breaking: '是否有破坏性变更(可选,用 "|" 换行):\n',
|
||||
footerPrefixesSelect: '选择关联的 ISSUE 类型(可选):',
|
||||
customFooterPrefix: '输入 ISSUE 前缀:',
|
||||
footer: '列出关联的 ISSUE,如: #31, #34:\n',
|
||||
generatingByAI: '正在通过 AI 生成提交信息...',
|
||||
generatedSelectByAI: '选择一个 AI 生成的 subject:',
|
||||
confirmCommit: '是否确认提交以上信息?',
|
||||
footerPrefixesSelect: "选择关联的 ISSUE 类型(可选):",
|
||||
customFooterPrefix: "输入 ISSUE 前缀:",
|
||||
footer: "列出关联的 ISSUE,如: #31, #34:\n",
|
||||
generatingByAI: "正在通过 AI 生成提交信息...",
|
||||
generatedSelectByAI: "选择一个 AI 生成的 subject:",
|
||||
confirmCommit: "是否确认提交以上信息?",
|
||||
},
|
||||
// subject 最小长度提示(中文化 "[x more chars needed]")
|
||||
minSubjectLength: 4,
|
||||
defaultSubject: '',
|
||||
defaultSubject: "",
|
||||
types: [
|
||||
{ value: 'feat', name: 'feat: 新增功能', emoji: ':sparkles:' },
|
||||
{ value: 'fix', name: 'fix: 修复缺陷', emoji: ':bug:' },
|
||||
{ value: 'docs', name: 'docs: 文档变更', emoji: ':memo:' },
|
||||
{ value: 'style', name: 'style: 代码格式(不影响逻辑)', emoji: ':lipstick:' },
|
||||
{ value: 'refactor', name: 'refactor: 重构(既非新功能,也不是修 bug)', emoji: ':recycle:' },
|
||||
{ value: 'perf', name: 'perf: 性能优化', emoji: ':zap:' },
|
||||
{ value: 'test', name: 'test: 添加或修改测试', emoji: ':white_check_mark:' },
|
||||
{ value: 'build', name: 'build: 构建系统或外部依赖变更', emoji: ':package:' },
|
||||
{ value: 'ci', name: 'ci: CI 配置文件和脚本变更', emoji: ':ferris_wheel:' },
|
||||
{ value: 'chore', name: 'chore: 其它不修改 src 或测试的改动', emoji: ':hammer:' },
|
||||
{ value: 'revert', name: 'revert: 回退之前的 commit', emoji: ':rewind:' },
|
||||
{ value: "feat", name: "feat: 新增功能", emoji: ":sparkles:" },
|
||||
{ value: "fix", name: "fix: 修复缺陷", emoji: ":bug:" },
|
||||
{ value: "docs", name: "docs: 文档变更", emoji: ":memo:" },
|
||||
{
|
||||
value: "style",
|
||||
name: "style: 代码格式(不影响逻辑)",
|
||||
emoji: ":lipstick:",
|
||||
},
|
||||
{
|
||||
value: "refactor",
|
||||
name: "refactor: 重构(既非新功能,也不是修 bug)",
|
||||
emoji: ":recycle:",
|
||||
},
|
||||
{ value: "perf", name: "perf: 性能优化", emoji: ":zap:" },
|
||||
{
|
||||
value: "test",
|
||||
name: "test: 添加或修改测试",
|
||||
emoji: ":white_check_mark:",
|
||||
},
|
||||
{
|
||||
value: "build",
|
||||
name: "build: 构建系统或外部依赖变更",
|
||||
emoji: ":package:",
|
||||
},
|
||||
{
|
||||
value: "ci",
|
||||
name: "ci: CI 配置文件和脚本变更",
|
||||
emoji: ":ferris_wheel:",
|
||||
},
|
||||
{
|
||||
value: "chore",
|
||||
name: "chore: 其它不修改 src 或测试的改动",
|
||||
emoji: ":hammer:",
|
||||
},
|
||||
{
|
||||
value: "revert",
|
||||
name: "revert: 回退之前的 commit",
|
||||
emoji: ":rewind:",
|
||||
},
|
||||
],
|
||||
// issue 前缀也可以用中文说明
|
||||
issuePrefixs: [
|
||||
{ value: 'link', name: 'link: 关联 ISSUE' },
|
||||
{ value: 'close', name: 'close: 关闭 ISSUE' },
|
||||
{ value: "link", name: "link: 关联 ISSUE" },
|
||||
{ value: "close", name: "close: 关闭 ISSUE" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
## 开发环境要求
|
||||
|
||||
- Node.js >= 18.12.0
|
||||
- pnpm 8.15.6
|
||||
- pnpm >= 10.0.0
|
||||
|
||||
## 快速开始
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ git checkout -b fix/your-bug-fix
|
|||
### 3. 开发与测试
|
||||
|
||||
```bash
|
||||
# 构建所有包
|
||||
# 构建所有包(Turbo)
|
||||
pnpm build
|
||||
|
||||
# 运行测试
|
||||
|
|
@ -162,7 +162,7 @@ git push origin feat/your-feature-name
|
|||
### JavaScript/TypeScript
|
||||
|
||||
- 使用 ESLint + Prettier 进行代码检查和格式化
|
||||
- 遵循项目的 `.eslintrc` 配置
|
||||
- 遵循项目的 `eslint.config.js`(ESLint Flat Config)
|
||||
- 提交前运行 `pnpm lint` 和 `pnpm format`
|
||||
|
||||
### 测试
|
||||
|
|
@ -170,7 +170,7 @@ git push origin feat/your-feature-name
|
|||
- 为新功能编写单元测试
|
||||
- 确保所有测试通过: `pnpm test`
|
||||
- 测试文件放在各包的 `test/` 目录下
|
||||
- 使用 Jest 作为测试框架
|
||||
- 使用 Vitest 作为测试框架
|
||||
|
||||
### 文档
|
||||
|
||||
|
|
@ -187,20 +187,20 @@ r-util-js/
|
|||
│ ├── vue3/ # Vue3 工具
|
||||
│ ├── vue2/ # Vue2 工具
|
||||
│ └── uni-app/ # uni-app 工具
|
||||
├── scripts/ # 构建和发布脚本
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 发布流程
|
||||
|
||||
发布由维护者负责,使用 `standard-version` 自动生成版本号和 CHANGELOG:
|
||||
发布由维护者负责,详细流程请查看 [发布流程文档](./RELEASE.md)。
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
# 生成版本和 CHANGELOG
|
||||
pnpm changeset
|
||||
pnpm release:check
|
||||
pnpm release
|
||||
|
||||
# 发布到 npm
|
||||
pnpm publish -r
|
||||
pnpm release:publish
|
||||
```
|
||||
|
||||
## 需要帮助?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# 发布流程
|
||||
|
||||
本文档描述维护者发布 `@r-utils` monorepo 包的标准流程。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- Node.js >= 18.12.0
|
||||
- pnpm >= 10.0.0
|
||||
- 已登录目标 npm 仓库(默认使用各包 `publishConfig.registry`)
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## 推荐发布流程(分步)
|
||||
|
||||
### 1. 记录变更(changeset)
|
||||
|
||||
```bash
|
||||
pnpm changeset
|
||||
pnpm changeset:status
|
||||
```
|
||||
|
||||
### 2. 质量检查与构建
|
||||
|
||||
```bash
|
||||
pnpm release:check
|
||||
```
|
||||
|
||||
`release:check` 会依次执行:
|
||||
|
||||
- `pnpm test`
|
||||
- `pnpm build`
|
||||
|
||||
### 3. 更新版本号与 CHANGELOG
|
||||
|
||||
```bash
|
||||
pnpm release
|
||||
```
|
||||
|
||||
该命令等价于 `changeset version`,会更新受影响包版本及 changelog。
|
||||
|
||||
### 4. 发布到 npm 仓库
|
||||
|
||||
```bash
|
||||
pnpm release:publish
|
||||
```
|
||||
|
||||
该命令等价于 `changeset publish`。
|
||||
|
||||
## 一键发布(维护者)
|
||||
|
||||
```bash
|
||||
pnpm release:all
|
||||
```
|
||||
|
||||
`release:all` 会按顺序执行:
|
||||
|
||||
1. `pnpm release:check`
|
||||
2. `pnpm release`
|
||||
3. `pnpm release:publish`
|
||||
|
||||
## 发布后建议
|
||||
|
||||
发布完成后,手动提交并推送版本相关变更与 tag。
|
||||
|
|
@ -4,11 +4,14 @@ import prettier from "eslint-config-prettier";
|
|||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
import globals from "globals";
|
||||
import importPlugin from "eslint-plugin-import-x";
|
||||
import vuePlugin from "eslint-plugin-vue";
|
||||
import vueParser from "vue-eslint-parser";
|
||||
|
||||
export default [
|
||||
// 应用推荐规则
|
||||
// 应用推荐规则
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...vuePlugin.configs["flat/recommended"],
|
||||
prettier,
|
||||
|
||||
// 基础配置
|
||||
|
|
@ -37,14 +40,6 @@ export default [
|
|||
"scripts/",
|
||||
],
|
||||
plugins: { prettier: prettierPlugin, "import-x": importPlugin },
|
||||
settings: {
|
||||
"import-x/resolver": {
|
||||
typescript: {
|
||||
project: "tsconfig.eslint.json",
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
|
|
@ -52,16 +47,36 @@ export default [
|
|||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
parser: ts.parser,
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
},
|
||||
extraFileExtensions: [".vue"],
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
"import-x/resolver": {
|
||||
typescript: {
|
||||
project: [
|
||||
"packages/common/tsconfig.json",
|
||||
"packages/uni-app/tsconfig.json",
|
||||
"packages/uview-plus/tsconfig.json",
|
||||
"packages/vue2/tsconfig.json",
|
||||
"packages/vue3/tsconfig.json",
|
||||
"playground/vue3-app/tsconfig.json",
|
||||
"playground/vue2-app/tsconfig.json",
|
||||
"playground/uniapp-app/tsconfig.json",
|
||||
],
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...importPlugin.flatConfigs.recommended.rules,
|
||||
|
||||
// Prettier 规则
|
||||
// Prettier 规则
|
||||
"prettier/prettier": "warn",
|
||||
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
|
|
@ -110,4 +125,30 @@ export default [
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
// uni-app / App-Plus 运行时全局变量
|
||||
{
|
||||
files: [
|
||||
"packages/uni-app/**/*.{js,ts,vue}",
|
||||
"packages/common/src/printer/**/*.{js,ts}",
|
||||
"playground/uniapp-app/**/*.{js,ts,vue}",
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
uni: "readonly",
|
||||
plus: "readonly",
|
||||
wx: "readonly",
|
||||
getCurrentPages: "readonly",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// playground 项目放宽部分规则
|
||||
{
|
||||
files: ["playground/**/*.{js,ts,vue}"],
|
||||
rules: {
|
||||
"import-x/no-extraneous-dependencies": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
29
package.json
29
package.json
|
|
@ -37,11 +37,23 @@
|
|||
],
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"build": "pnpm -r run build",
|
||||
"build": "turbo run build --filter=./packages/*",
|
||||
"build:playground": "turbo run build --filter=./playground/*",
|
||||
"build:watch": "turbo run watch --filter=./packages/* --parallel",
|
||||
"typecheck": "turbo run typecheck --filter=./packages/* --concurrency=1",
|
||||
"prepare": "husky install",
|
||||
"lint": "eslint --fix packages/*/src",
|
||||
"format": "prettier --write packages/*/src",
|
||||
"release": "standard-version",
|
||||
"lint": "turbo run lint --filter=./packages/*",
|
||||
"lint:playground": "turbo run lint --filter=./playground/*",
|
||||
"format": "turbo run format --filter=./packages/*",
|
||||
"dev:vue3": "pnpm --filter @r-utils/playground-vue3 dev",
|
||||
"dev:vue2": "pnpm --filter @r-utils/playground-vue2 dev",
|
||||
"dev:uniapp": "pnpm --filter @r-utils/playground-uniapp dev:h5",
|
||||
"changeset": "changeset",
|
||||
"release": "changeset version",
|
||||
"changeset:status": "changeset status --verbose",
|
||||
"release:check": "pnpm test && pnpm build",
|
||||
"release:publish": "changeset publish",
|
||||
"release:all": "pnpm release:check && pnpm release && pnpm release:publish",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "vitest run",
|
||||
|
|
@ -49,6 +61,7 @@
|
|||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.31.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
||||
|
|
@ -62,18 +75,20 @@
|
|||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"vue-eslint-parser": "^10.1.3",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"inquirer": "^9.3.8",
|
||||
"lint-staged": "^16.4.0",
|
||||
"vitest": "catalog:",
|
||||
"prettier": "^3.8.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"tslib": "^2.8.1",
|
||||
"turbo": "^2.9.6",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-dts": "^4.5.4"
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/lodash": "4.17.16",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# @r-utils/common
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 添加工具
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
# @r-utils/common
|
||||
|
||||
与框架无关的通用 JS/TS 工具库,适用于任意前端项目,也可在具备对应运行时 API 的环境中使用。
|
||||
|
||||
## 特性
|
||||
|
||||
- 不依赖 Vue、uni-app 或 uview-plus。
|
||||
- 支持 ESM / CJS 双格式产物。
|
||||
- 支持根入口导入和子路径按需导入。
|
||||
- 标记 `sideEffects: false`,方便业务打包器进行 tree-shaking。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @r-utils/common
|
||||
```
|
||||
|
||||
## 导入方式
|
||||
|
||||
### 推荐:根入口导入
|
||||
|
||||
大多数场景推荐从根入口导入,使用心智负担更低,现代打包器仍可结合 ESM 和 `sideEffects: false` 做 tree-shaking。
|
||||
|
||||
```ts
|
||||
import {
|
||||
Permission,
|
||||
wait,
|
||||
Countdown,
|
||||
TimeoutTimer,
|
||||
getValueOfRule,
|
||||
} from "@r-utils/common";
|
||||
```
|
||||
|
||||
### 兼容:子路径按需导入
|
||||
|
||||
如果你希望导入路径更精确,也可以使用子路径导入。两种方式都支持,按团队习惯选择即可。
|
||||
|
||||
```ts
|
||||
import { Permission } from "@r-utils/common/permission";
|
||||
import { wait } from "@r-utils/common/time";
|
||||
import { Countdown, TimeoutTimer } from "@r-utils/common/timer";
|
||||
import { getValueOfRule } from "@r-utils/common/input-rule";
|
||||
```
|
||||
|
||||
## 导出模块
|
||||
|
||||
| 子路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `@r-utils/common/permission` | 权限字符串格式校验 |
|
||||
| `@r-utils/common/time` | 时间相关工具 |
|
||||
| `@r-utils/common/timer` | 倒计时、超时计时器 |
|
||||
| `@r-utils/common/input-rule` | 输入值规则处理 |
|
||||
| `@r-utils/common/knock-test` | 连续敲击/点击触发器 |
|
||||
| `@r-utils/common/ui` | 与框架无关的 UI 计算工具 |
|
||||
| `@r-utils/common/printer` | ESC/TSC 打印相关工具 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 权限字符串校验
|
||||
|
||||
```ts
|
||||
import { Permission } from "@r-utils/common";
|
||||
|
||||
const permission = new Permission([], 3, ":");
|
||||
|
||||
permission.isValid("user:create:button"); // true
|
||||
permission.isValid("user:create"); // false
|
||||
```
|
||||
|
||||
### 等待指定时间
|
||||
|
||||
```ts
|
||||
import { wait } from "@r-utils/common";
|
||||
|
||||
async function submit() {
|
||||
await wait(300);
|
||||
console.log("继续执行");
|
||||
}
|
||||
```
|
||||
|
||||
### 倒计时
|
||||
|
||||
```ts
|
||||
import { Countdown } from "@r-utils/common";
|
||||
|
||||
const countdown = new Countdown(10 * 1000);
|
||||
|
||||
countdown.addStepEventListener((time) => {
|
||||
console.log("剩余时间:", time);
|
||||
});
|
||||
|
||||
countdown.addCountdownEventListener(() => {
|
||||
console.log("倒计时结束");
|
||||
});
|
||||
|
||||
countdown.start();
|
||||
```
|
||||
|
||||
### 简单数值倒计时
|
||||
|
||||
```ts
|
||||
import { TimeoutTimer } from "@r-utils/common";
|
||||
|
||||
const timer = new TimeoutTimer(5000, 0, 1000);
|
||||
|
||||
timer.addStepEventListener((time) => {
|
||||
console.log("剩余毫秒:", time);
|
||||
});
|
||||
|
||||
timer.addCountdownEventListener(() => {
|
||||
console.log("完成");
|
||||
});
|
||||
|
||||
timer.start();
|
||||
```
|
||||
|
||||
### 输入值规则处理
|
||||
|
||||
```ts
|
||||
import { getValueOfRule } from "@r-utils/common";
|
||||
|
||||
const value = getValueOfRule("12.345", {
|
||||
min: 0,
|
||||
max: 99999.99,
|
||||
required: true,
|
||||
digits: 2,
|
||||
keepDecimal: true,
|
||||
});
|
||||
|
||||
console.log(value); // "12.35"
|
||||
```
|
||||
|
||||
### 禁止正负号并转成数字
|
||||
|
||||
```ts
|
||||
import { getValueOfRule } from "@r-utils/common";
|
||||
|
||||
const value = getValueOfRule("-12", {
|
||||
min: 0,
|
||||
required: true,
|
||||
noSign: true,
|
||||
number: true,
|
||||
});
|
||||
|
||||
console.log(value); // 0
|
||||
```
|
||||
|
||||
### 连续敲击触发回调
|
||||
|
||||
```ts
|
||||
import { KnockTest } from "@r-utils/common";
|
||||
|
||||
const knockTest = new KnockTest({
|
||||
maxWaitTime: 5000,
|
||||
operations: [
|
||||
{ times: 3, duration: 1000, delay: 500 },
|
||||
{ times: 2, duration: 1000, delay: 500 },
|
||||
],
|
||||
});
|
||||
|
||||
knockTest.addCallback(() => {
|
||||
console.log("触发隐藏功能");
|
||||
});
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
knockTest.knock();
|
||||
});
|
||||
```
|
||||
|
||||
### 缓动滚动计算
|
||||
|
||||
```ts
|
||||
import { slowlyScroll } from "@r-utils/common";
|
||||
|
||||
await slowlyScroll(0, 300, 500, (value) => {
|
||||
window.scrollTo(0, value);
|
||||
});
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `Countdown`、`TimeoutTimer` 和 `KnockTest` 内部使用 `window.setInterval` / `window.setTimeout`,更适合浏览器或类浏览器环境。
|
||||
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
|
||||
- 打印相关模块通常依赖具体设备和业务场景,建议在真实设备环境中验证。
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@r-utils/common",
|
||||
"version": "1.3.0",
|
||||
"version": "2.0.0",
|
||||
"private": false,
|
||||
"description": "js通用工具库",
|
||||
"type": "module",
|
||||
|
|
@ -8,13 +8,50 @@
|
|||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./input-rule": {
|
||||
"types": "./dist/input-rule/index.d.ts",
|
||||
"import": "./dist/input-rule/index.mjs",
|
||||
"require": "./dist/input-rule/index.cjs"
|
||||
},
|
||||
"./knock-test": {
|
||||
"types": "./dist/knock-test/index.d.ts",
|
||||
"import": "./dist/knock-test/index.mjs",
|
||||
"require": "./dist/knock-test/index.cjs"
|
||||
},
|
||||
"./permission": {
|
||||
"types": "./dist/permission/index.d.ts",
|
||||
"import": "./dist/permission/index.mjs",
|
||||
"require": "./dist/permission/index.cjs"
|
||||
},
|
||||
"./printer": {
|
||||
"types": "./dist/printer/index.d.ts",
|
||||
"import": "./dist/printer/index.mjs",
|
||||
"require": "./dist/printer/index.cjs"
|
||||
},
|
||||
"./time": {
|
||||
"types": "./dist/time/index.d.ts",
|
||||
"import": "./dist/time/index.mjs",
|
||||
"require": "./dist/time/index.cjs"
|
||||
},
|
||||
"./timer": {
|
||||
"types": "./dist/timer/index.d.ts",
|
||||
"import": "./dist/timer/index.mjs",
|
||||
"require": "./dist/timer/index.cjs"
|
||||
},
|
||||
"./ui": {
|
||||
"types": "./dist/ui/index.d.ts",
|
||||
"import": "./dist/ui/index.mjs",
|
||||
"require": "./dist/ui/index.cjs"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"utils",
|
||||
|
|
@ -44,9 +81,9 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint --ext .js,ts --fix src",
|
||||
"format": "prettier --write src",
|
||||
"release": "standard-version",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "vitest run"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,7 +72,10 @@ export function getValueOfRule(
|
|||
/**
|
||||
* 根据输入的获取处理后值-文本类型
|
||||
*/
|
||||
function getTextTargetValue(innerValue: InputValue, options: InputOptions): InputValue {
|
||||
function getTextTargetValue(
|
||||
innerValue: InputValue,
|
||||
options: InputOptions,
|
||||
): InputValue {
|
||||
const { required, init, pattern, number } = options;
|
||||
|
||||
innerValue ??= "";
|
||||
|
|
@ -143,7 +159,10 @@ function applyPrecision(num: number, options: InputOptions): number {
|
|||
/**
|
||||
* 根据输入的获取处理后值-数字类型
|
||||
*/
|
||||
function getNumberTargetValue(innerValue: InputValue, options: InputOptions): InputValue {
|
||||
function getNumberTargetValue(
|
||||
innerValue: InputValue,
|
||||
options: InputOptions,
|
||||
): InputValue {
|
||||
const { number, keepDecimal, noSign, digits } = options;
|
||||
const strValue = String(innerValue ?? "");
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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+?$
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export class JpPrinter {
|
|||
addText(content) {
|
||||
content = String(content);
|
||||
|
||||
let code = [];
|
||||
let code;
|
||||
if (isAndroidApp) {
|
||||
code = plus.android.invoke(content, "getBytes", "gbk");
|
||||
} else {
|
||||
|
|
@ -254,8 +254,8 @@ export class JpPrinter {
|
|||
let code = new TextEncoder("gb18030", {
|
||||
NONSTANDARD_allowLegacyEncoding: true,
|
||||
}).encode(content);
|
||||
const pL = parseInt((code.length + 3) % 256);
|
||||
const pH = parseInt((code.length + 3) / 256);
|
||||
// const pL = parseInt((code.length + 3) % 256);
|
||||
// const pH = parseInt((code.length + 3) / 256);
|
||||
this.data.push(29, 40, 107, 3, 0, 49, 69, ...code);
|
||||
return this;
|
||||
}
|
||||
|
|
@ -740,6 +740,7 @@ export class JpPrinter {
|
|||
let ch = 0;
|
||||
text.split("").forEach((c) => {
|
||||
// 是否是汉字,汉字两倍宽
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const isChinese = /[^\x00-\xff]/.test(c);
|
||||
if (isChinese) {
|
||||
ch += 2;
|
||||
|
|
@ -984,10 +985,12 @@ export class JpPrinter {
|
|||
return this;
|
||||
}
|
||||
|
||||
setBarcodeContent(t, content) {
|
||||
setBarcodeContent(t) {
|
||||
let ty = 73;
|
||||
this.data.push(29);
|
||||
this.data.push(107);
|
||||
const bar = JpPrinter.bar;
|
||||
|
||||
switch (t) {
|
||||
case bar[0]:
|
||||
ty = 65;
|
||||
|
|
@ -1031,12 +1034,16 @@ export class JpPrinter {
|
|||
}
|
||||
|
||||
export class Query {
|
||||
constructor() {
|
||||
this.queryStatus = new Query();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询打印机实时状态
|
||||
* @param {*} n
|
||||
* @param {*} device
|
||||
*/
|
||||
getRealtimeStatusTransmission(n, device) {
|
||||
getRealtimeStatusTransmission(n) {
|
||||
/*
|
||||
n = 1:传送打印机状态
|
||||
n = 2:传送脱机状态
|
||||
|
|
@ -1048,7 +1055,7 @@ export class Query {
|
|||
dateView.setUint8(0, 16);
|
||||
dateView.setUint8(1, 4);
|
||||
dateView.setUint8(2, n);
|
||||
queryStatus.query(buf);
|
||||
this.queryStatus.query(buf);
|
||||
}
|
||||
|
||||
addGeneratePlus(n, m, t, device) {
|
||||
|
|
@ -1059,7 +1066,7 @@ export class Query {
|
|||
dateView.setUint8(2, n);
|
||||
dateView.setUint8(3, m);
|
||||
dateView.setUint8(4, t);
|
||||
queryStatus.query(buf, device);
|
||||
this.queryStatus.query(buf, device);
|
||||
}
|
||||
|
||||
query(buf, device) {
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ export class TSCPlus extends TSC {
|
|||
let ch = 0;
|
||||
text.split("").forEach((c) => {
|
||||
// 是否是汉字,汉字两倍宽
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const isChinese = /[^\x00-\xff]/.test(c);
|
||||
if (isChinese) {
|
||||
ch += 2;
|
||||
|
|
|
|||
|
|
@ -417,7 +417,7 @@ export class TSC {
|
|||
for (x = 0; x < w; x++) {
|
||||
var color = bitmapData[(y * w + x) * 4 + 1];
|
||||
if (color <= 128) {
|
||||
bits[parseInt(y * pitch + x / 8)] |= 0x80 >> x % 8;
|
||||
bits[parseInt(y * pitch + x / 8)] |= 0x80 >> (x % 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ describe("getValueOfRule - 文本模式 (text: true)", () => {
|
|||
});
|
||||
|
||||
test("required + init:非数字文本回退到 init", () => {
|
||||
expect(getValueOfRule("abc", { ...textOpts, required: true, init: "默认" })).toBe("默认");
|
||||
expect(
|
||||
getValueOfRule("abc", { ...textOpts, required: true, init: "默认" }),
|
||||
).toBe("默认");
|
||||
});
|
||||
|
||||
test("required 无 init:非数字文本保持原值", () => {
|
||||
|
|
@ -29,7 +31,9 @@ describe("getValueOfRule - 文本模式 (text: true)", () => {
|
|||
});
|
||||
|
||||
test("pattern 匹配成功返回原值", () => {
|
||||
expect(getValueOfRule("123", { ...textOpts, pattern: "^\\d+$" })).toBe("123");
|
||||
expect(getValueOfRule("123", { ...textOpts, pattern: "^\\d+$" })).toBe(
|
||||
"123",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -107,7 +111,9 @@ describe("getValueOfRule - digits 小数位数", () => {
|
|||
});
|
||||
|
||||
test("keepDecimal + number 返回数字类型", () => {
|
||||
expect(getValueOfRule("3.10", { digits: 2, keepDecimal: true, number: true })).toBe(3.1);
|
||||
expect(
|
||||
getValueOfRule("3.10", { digits: 2, keepDecimal: true, number: true }),
|
||||
).toBe(3.1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -127,11 +133,15 @@ describe("getValueOfRule - integer 整数模式", () => {
|
|||
|
||||
describe("getValueOfRule - noSign 不允许正负号", () => {
|
||||
test("带正号的输入被视为非法", () => {
|
||||
expect(getValueOfRule("+5", { noSign: true, required: true, init: 0 })).toBe("0");
|
||||
expect(
|
||||
getValueOfRule("+5", { noSign: true, required: true, init: 0 }),
|
||||
).toBe("0");
|
||||
});
|
||||
|
||||
test("带负号的输入被视为非法", () => {
|
||||
expect(getValueOfRule("-5", { noSign: true, required: true, init: 0 })).toBe("0");
|
||||
expect(
|
||||
getValueOfRule("-5", { noSign: true, required: true, init: 0 }),
|
||||
).toBe("0");
|
||||
});
|
||||
|
||||
test("无符号数字正常通过", () => {
|
||||
|
|
@ -142,25 +152,45 @@ describe("getValueOfRule - noSign 不允许正负号", () => {
|
|||
describe("getValueOfRule - callback", () => {
|
||||
test("callback 接收最终处理后的值", () => {
|
||||
let received: unknown;
|
||||
getValueOfRule("42", { number: true }, (v) => { received = v; });
|
||||
getValueOfRule("42", { number: true }, (v) => {
|
||||
received = v;
|
||||
});
|
||||
expect(received).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getValueOfRule - 综合场景", () => {
|
||||
test("限制输入 min:0, max:99999.99, required, digits:2", () => {
|
||||
expect(getValueOfRule("100.456", { min: 0, max: 99999.99, required: true, digits: 2 })).toBe("100.46");
|
||||
expect(
|
||||
getValueOfRule("100.456", {
|
||||
min: 0,
|
||||
max: 99999.99,
|
||||
required: true,
|
||||
digits: 2,
|
||||
}),
|
||||
).toBe("100.46");
|
||||
});
|
||||
|
||||
test("超出 max 时截断到 max", () => {
|
||||
expect(getValueOfRule("100000", { min: 0, max: 99999.99, required: true, digits: 2 })).toBe("99999.99");
|
||||
expect(
|
||||
getValueOfRule("100000", {
|
||||
min: 0,
|
||||
max: 99999.99,
|
||||
required: true,
|
||||
digits: 2,
|
||||
}),
|
||||
).toBe("99999.99");
|
||||
});
|
||||
|
||||
test("空输入 required 回退到 min", () => {
|
||||
expect(getValueOfRule("", { min: 0, max: 99999.99, required: true, digits: 2 })).toBe("0");
|
||||
expect(
|
||||
getValueOfRule("", { min: 0, max: 99999.99, required: true, digits: 2 }),
|
||||
).toBe("0");
|
||||
});
|
||||
|
||||
test("负数输入限制到 min:0", () => {
|
||||
expect(getValueOfRule("-5", { min: 0, max: 100, required: true })).toBe("0");
|
||||
expect(getValueOfRule("-5", { min: 0, max: 100, required: true })).toBe(
|
||||
"0",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src/**/*.ts"],
|
||||
"include": ["src/**/*.ts", "types/**/*.d.ts"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,338 @@
|
|||
export function row2col(arr: any): any;
|
||||
/**
|
||||
* NOTE 打印机普通文本最大宽度32个数字,16个汉字超出换行
|
||||
*
|
||||
*/
|
||||
export class JpPrinter {
|
||||
static bar: string[];
|
||||
/**
|
||||
* 获取文本宽度点数
|
||||
* @param {string} text
|
||||
* @returns {number}
|
||||
*/
|
||||
static getTextDots(text: string): number;
|
||||
/**
|
||||
* 根据最大点数获取文本
|
||||
* @param {string} text
|
||||
* @param {number} maxDots
|
||||
* @return {string}
|
||||
*/
|
||||
static getMaxText(text?: string, maxDots?: number): string;
|
||||
/**
|
||||
* 分割字符
|
||||
* "123456789" => ["1234", "5678", "9"]
|
||||
* @param {string} str
|
||||
* @param {number} maxDots
|
||||
* @return {string[]}
|
||||
*/
|
||||
static splitString(str: string, maxDots: number): string[];
|
||||
static createDefaultPrinter(): JpPrinter;
|
||||
static getQuery(): Query;
|
||||
constructor(width?: number, dpi?: number);
|
||||
name: string;
|
||||
data: any[];
|
||||
align: string;
|
||||
bold: boolean;
|
||||
lineSpacing: number;
|
||||
fontSize: number;
|
||||
/** 有效打印宽度 */
|
||||
width: number;
|
||||
dpi: number;
|
||||
_layout: null;
|
||||
storeLayout(): this;
|
||||
restoreLayout(): this;
|
||||
getXDots(): number;
|
||||
/** 初始化打印机 */
|
||||
init(): this;
|
||||
/**
|
||||
* 添加文本内容
|
||||
* @param {string} content
|
||||
*/
|
||||
addText(content: string): this;
|
||||
/**
|
||||
* GS ! 选择字符大小
|
||||
* @param {number} n 放大倍数
|
||||
* */
|
||||
setFontSize(n?: number): this;
|
||||
/**
|
||||
* ESC E 选择/取消加粗模式
|
||||
* @param {boolean} bold
|
||||
* */
|
||||
setBold(bold: boolean): this;
|
||||
/**
|
||||
* ESC – 选择/取消下划线模式
|
||||
* 根据 n 的值选择或取消下划线模式:
|
||||
* • 下划线可加在所有字符下(包括右间距),但不包括 HT 设置的空格。
|
||||
* • 下划线不能作用在顺时针旋转 90°和反色的字符下。
|
||||
* • 当取消下划线模式时,后面的字符不加下划线,下划线的宽度不改变。默认宽度是一点宽。
|
||||
* • 改变字符大小不影响当前下划线宽度。
|
||||
* • 下划线选择取消也可以由 ESC !来设置,最后执行的命令有效。
|
||||
* • 该命令不影响汉字字符的设定。
|
||||
* @param {number} n
|
||||
* 0, 48 取消下划线模式
|
||||
* 1, 49 选择下划线模式(1 点宽)
|
||||
* 2, 50 选择下划线模式(2 点宽)
|
||||
* @returns
|
||||
*/
|
||||
setUnderline(n?: number): this;
|
||||
/**
|
||||
* FS - 选择/取消汉字下划线模式
|
||||
* 根据 n 的值,选择或取消汉字的下划线
|
||||
* • 打印机能对所有字符加下划线,包括左右间距。但不能对由 HT 命令(横向跳格)引起的空格加下划线,也不对顺时针旋转 90 度的字符加下划线。
|
||||
* • 消下划线模式后,不再执行下划线打印,但原先设置的下划线线宽不会改变。默认下划线线宽为 1 点。
|
||||
* • 即使改变字符大小,设定的下划线线宽也不会改变。
|
||||
* • 用 FS !也可选择或取消下划线模式,最后一条命令有效。
|
||||
* @param {*} n
|
||||
* 0, 48 取消汉字下划线
|
||||
* 1, 49 选择汉字下划线(1 点宽)
|
||||
* 2, 50 选择汉字下划线(2 点宽
|
||||
* @returns
|
||||
*/
|
||||
setUnderlineChinese(n?: any): this;
|
||||
/**
|
||||
* 设置二维码大小
|
||||
* @param {*} n
|
||||
* @see {JpPrinter#setQRCodeSize}
|
||||
*/
|
||||
setSelectSizeOfModuleForQRCode(n: any): this;
|
||||
/**
|
||||
* 设置 QRCode 模块大小为 n dot
|
||||
* @param {number} n
|
||||
* @returns
|
||||
*/
|
||||
setQRCodeSize(n: number): this;
|
||||
/**
|
||||
* 选择 QRCode 纠错等级
|
||||
* @see {JpPrinter#setQRCodeErrorCorrectionLevel}
|
||||
*/
|
||||
setSelectErrorCorrectionLevelForQRCode(n: any): this;
|
||||
/**
|
||||
* 选择 QRCode 纠错等级
|
||||
* n 功能 纠错能力
|
||||
* 48 选择纠错等级 L 7
|
||||
* 49 选择纠错等级 M 15
|
||||
* 50 选择纠错等级 Q 25
|
||||
* 51 选择纠错等级 H 30
|
||||
* @see
|
||||
*/
|
||||
setQRCodeErrorCorrectionLevel(n: any): this;
|
||||
/** 设置二维码内容 */
|
||||
setStoreQRCodeData(content: any): this;
|
||||
/**
|
||||
* 打印二维码
|
||||
* @deprecated
|
||||
* @see {JpPrinter#printQRCode}
|
||||
* */
|
||||
setPrintQRCode(): this;
|
||||
/** 打印二维码 */
|
||||
printQRCode(): this;
|
||||
addQrCodeByUrl(url: any, width: any, height: any): this;
|
||||
addImageByUrl(url: any, width: any, height: any): this;
|
||||
addQrCodeByText(contents: any, width: any, height: any): this;
|
||||
/**
|
||||
* HT 水平定位
|
||||
* 移动打印位置到下一个水平定位点的位置。
|
||||
* @deprecated
|
||||
* @see {JpPrinter#addHorTab}
|
||||
*/
|
||||
setHorTab(): this;
|
||||
/**
|
||||
* HT 水平定位
|
||||
* 移动打印位置到下一个水平定位点的位置。
|
||||
*/
|
||||
addHorTab(): this;
|
||||
/**
|
||||
* ESC $ 设置绝对打印位置
|
||||
* @param {number} where
|
||||
* @returns
|
||||
*/
|
||||
setAbsolutePrintPosition(where: number): this;
|
||||
/**
|
||||
* ESC \ 设置相对横向打印位置
|
||||
* 安卓无法传入超过127的数
|
||||
*
|
||||
* @param {number} where
|
||||
* @returns
|
||||
*/
|
||||
setRelativePrintPosition(where: number): this;
|
||||
/**
|
||||
* ESC a 选择对齐方式
|
||||
* @param {number} n
|
||||
* 0, 48 左对齐
|
||||
* 1, 49 中间对齐
|
||||
* 2, 50 右对齐
|
||||
*/
|
||||
setSelectJustification(n: number): this;
|
||||
/**
|
||||
* 设置对齐方式
|
||||
* @param {string} align
|
||||
* "l" 左对齐
|
||||
* "m" 中间对齐
|
||||
* "r" 右对齐
|
||||
* @returns
|
||||
*/
|
||||
setAlign(align: string): this;
|
||||
/**
|
||||
* ESC D 设置横向跳格位
|
||||
* @param {number} n
|
||||
* @deprecated
|
||||
* @see {}
|
||||
*/
|
||||
space(...nk: any[]): this;
|
||||
/**
|
||||
* ESC D 设置横向跳格位
|
||||
* @param {number} n
|
||||
*/
|
||||
addSpace(...nk: any[]): this;
|
||||
/**
|
||||
* GS L 设置左边距
|
||||
* @param {number} n
|
||||
* @returns
|
||||
*/
|
||||
setLeftMargin(n: number): this;
|
||||
textMarginRight(n: any): this;
|
||||
/**
|
||||
* ESC 3 设置行间距
|
||||
* @deprecated
|
||||
* @see {JpPrinter#setLineSpacing}
|
||||
* */
|
||||
rowSpace(n: any): this;
|
||||
/**
|
||||
* ESC 3 设置行间距
|
||||
* */
|
||||
setLineSpacing(n: any): this;
|
||||
/**
|
||||
* GS W 设置打印区域宽度
|
||||
* @param {number} width
|
||||
* @returns
|
||||
*/
|
||||
setPrintingAreaWidth(width: number): this;
|
||||
setSound(n: any, t: any): this;
|
||||
setBitmap(res: any): this;
|
||||
/**
|
||||
* 添加位图 GS v 0
|
||||
* */
|
||||
addBitmap(m: any, xL: any, xH: any, yL: any, yH: any, ...data: any[]): this;
|
||||
/**
|
||||
* 添加位图
|
||||
* @param {Uint8ClampedArray} imgData
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
*/
|
||||
addBitmapHelper(imgData: Uint8ClampedArray, width: number, height: number): this;
|
||||
/**
|
||||
* 打印并换行
|
||||
* */
|
||||
addLF(): this;
|
||||
/**
|
||||
* ESC J 打印并走纸 n 个单位
|
||||
* @param {number} n
|
||||
* @see {JpPrinter#addPrintAndFeed}
|
||||
*/
|
||||
setPrintAndFeed(n: number): this;
|
||||
/**
|
||||
* ESC J 打印并走纸 n 个单位
|
||||
* @param {number} n
|
||||
*/
|
||||
addPrintAndFeed(n: number): this;
|
||||
/**
|
||||
* ESC d 打印并走纸 n 行
|
||||
* @param {number} n
|
||||
* @see {JpPrinter#addPrintAndFeedRow}
|
||||
*/
|
||||
setPrintAndFeedRow(n: number): this;
|
||||
/**
|
||||
* ESC d 打印并走纸 n 行
|
||||
* @param {number} n
|
||||
* @returns
|
||||
*/
|
||||
addPrintAndFeedRow(n: number): this;
|
||||
/**
|
||||
* 添加 n 行
|
||||
* @param {number} n
|
||||
* @see {JpPrinter#addPrintAndFeedRow}
|
||||
*/
|
||||
addRow(n: number): this;
|
||||
/**
|
||||
* 添加打印指令数据
|
||||
* @param {number[]} data
|
||||
* @returns
|
||||
*/
|
||||
addData(...data: number[]): this;
|
||||
/**
|
||||
* 获取打印指令数据
|
||||
*/
|
||||
getData(): any[];
|
||||
/*********************** 自增 ***********************/
|
||||
/**
|
||||
* 计算文字位置
|
||||
* @param {*} value
|
||||
* @param {*} width
|
||||
* @deprecated
|
||||
* @see {JpPrinter#getTextDots}
|
||||
*/
|
||||
siteText(value: any, width: any): number;
|
||||
/**
|
||||
* 计算数字位置
|
||||
* @param {*} value
|
||||
* @param {*} width
|
||||
* @deprecated
|
||||
* @see {JpPrinter#getTextDots}
|
||||
*/
|
||||
siteNumber(value: any, width: any): number;
|
||||
/**
|
||||
* 左对齐文字
|
||||
* @deprecated
|
||||
*/
|
||||
alignLeft(name: any, text: any, arr: any): this;
|
||||
/**
|
||||
* @deprecated
|
||||
* @see {JpPrinter.getTextDots}
|
||||
*/
|
||||
getTextDots(text: any): number;
|
||||
/**
|
||||
* @deprecated
|
||||
* @see {JpPrinter.getMaxText}
|
||||
*/
|
||||
getMaxText(text?: string, maxDots?: number): string;
|
||||
/**
|
||||
* 添加两端对齐的文字
|
||||
*/
|
||||
addTextJustifyAlign(text1: any, text2: any): this;
|
||||
/**
|
||||
* 添加分割线
|
||||
*/
|
||||
addDivider(): this;
|
||||
/**
|
||||
* 添加表格
|
||||
* @param {Object} table
|
||||
* @param {Array} table.header
|
||||
* @param {Array} table.data
|
||||
* @param {number[]} [table.columns] 每列百分比
|
||||
*/
|
||||
addTable({ header, data, columns }: {
|
||||
header: any[];
|
||||
data: any[];
|
||||
columns?: number[] | undefined;
|
||||
}): this;
|
||||
/**
|
||||
* 分割符
|
||||
* @deprecated
|
||||
* @see {JpPrinter#addDivider}
|
||||
*/
|
||||
separator(width: any): this;
|
||||
setBarcodeWidth(width: any): this;
|
||||
setBarcodeHeight(height: any): this;
|
||||
setBarcodeContent(t: any): this;
|
||||
}
|
||||
export class Query {
|
||||
queryStatus: Query;
|
||||
/**
|
||||
* 查询打印机实时状态
|
||||
* @param {*} n
|
||||
* @param {*} device
|
||||
*/
|
||||
getRealtimeStatusTransmission(n: any): void;
|
||||
addGeneratePlus(n: any, m: any, t: any, device: any): void;
|
||||
query(buf: any, device: any): void;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
export class TSCPlus extends TSC {
|
||||
static MILLIMETERS_PER_INCH: number;
|
||||
static getLetterWidthDot(font: any): number;
|
||||
static getFontLineHeightDot(font: any, lineHeight: any): any;
|
||||
/**
|
||||
* 获取文本宽度点数
|
||||
* @param {string} text
|
||||
* @returns {number}
|
||||
*/
|
||||
static getTextDots(text: string, letterDots?: number): number;
|
||||
/**
|
||||
* 根据最大点数获取文本
|
||||
* @param {string} text
|
||||
* @param {number} maxDots
|
||||
* @return {string}
|
||||
*/
|
||||
static getMaxText(text?: string, maxDots?: number): string;
|
||||
/**
|
||||
* 分割字符串
|
||||
* "123456789" => ["1234", "5678", "9"]
|
||||
* @param {string} str
|
||||
* @param {number} maxDots
|
||||
* @return {string[]}
|
||||
*/
|
||||
static splitString(str: string, maxDots: number): string[];
|
||||
font: string;
|
||||
lineHeight: string;
|
||||
dpi: number;
|
||||
nextX: number;
|
||||
nextY: number;
|
||||
widthDot: number;
|
||||
heightDot: number;
|
||||
paddingTopDot: number;
|
||||
paddingLeftDot: number;
|
||||
paddingRightDot: number;
|
||||
paddingBottomDot: number;
|
||||
/**
|
||||
* 设置内边距
|
||||
* @param {number} paddingTopDot
|
||||
* @param {number} paddingRightDot
|
||||
* @param {number} paddingBottomDot
|
||||
* @param {number} paddingLeftDot
|
||||
*/
|
||||
setPadding(paddingTopDot: number, paddingRightDot: number, paddingBottomDot: number, paddingLeftDot: number): void;
|
||||
mmToDot(mm: any): number;
|
||||
setDpi(dpi: any): void;
|
||||
setSize(w: any, h: any): TSC;
|
||||
setSizeMM(w: any, h: any): TSC;
|
||||
setSizeDot(w: any, h: any): TSC;
|
||||
/**
|
||||
* 设置字体
|
||||
* @param {string} font
|
||||
*/
|
||||
setFont(font: string): void;
|
||||
/**
|
||||
* 设置行高
|
||||
* @param {string|number} lineHeight
|
||||
*/
|
||||
setLineHeight(lineHeight: string | number): void;
|
||||
addTextLn(text: any, options?: {}): this;
|
||||
addAddBarCode(content: any, options?: {}): this;
|
||||
}
|
||||
import { TSC } from "./tsc.js";
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
export class TSC {
|
||||
static LINE_BREAK: string;
|
||||
constructor(data?: any[]);
|
||||
data: any[];
|
||||
addData(...data: any[]): void;
|
||||
addDataArray(dataArray: any): void;
|
||||
addCode(code: any): this;
|
||||
getData: () => any;
|
||||
/**
|
||||
* 该指令用于设定卷标纸的宽度和长度
|
||||
* @param {number|string} w 标签宽度 单位英寸inch
|
||||
* @param {number|string} h 标签高度 单位英寸inch
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setSize(w: number | string, h: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于设定卷标纸的宽度和长度
|
||||
* @param {number|string} w 标签宽度 单位毫米mm
|
||||
* @param {number|string} h 标签高度 单位毫米mm
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setSizeMM(w: number | string, h: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于定义两张卷标纸间的垂直间距距离
|
||||
* @param {number|string} m 两标签纸中间的垂直距离 单位英寸inch
|
||||
* @param {number|string} n 垂直间距偏移 单位英寸inch
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setGap(m?: number | string, n?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于定义两张卷标纸间的垂直间距距离
|
||||
* @param {number|string} m 两标签纸中间的垂直距离 单位毫米mm
|
||||
* @param {number|string} n 垂直间距偏移 单位毫米mm
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setGapMM(m?: number | string, n?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于设定黑标高度及定义标签印完后标签额外送出的长度
|
||||
* @param {number|string} m 黑标高度 单位英寸inch
|
||||
* @param {number|string} n 额外送出纸张长度 单位英寸inch
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBLine(m?: number | string, n?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于控制在剥离模式时(peel-off mode)每张卷标停止的位置,
|
||||
* 在打印下一张时打印机会将原先多推出或少推出的部分以回拉方式补偿回来。
|
||||
* 该指令仅适用于剥离模式。
|
||||
* @param {number|string} m 纸张停止的距离 单位英寸inch
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setOffset(offset?: number): TSC;
|
||||
/**
|
||||
* 该指令用于控制打印速度
|
||||
* @param {number|string} speed 打印速度
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setSpeed(speed?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于控制打印时的浓度
|
||||
* @param {number|string} density 打印浓度
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setDensity(density?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于定义打印时出纸和打印字体的方向
|
||||
* @param {number|string} direction
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setDirection(direction?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于定义卷标的参考坐标原点。坐标原点位置和打印方向有关
|
||||
* @param {number|string} x 水平方向的坐标位置,单位dot
|
||||
* @param {number|string} y 垂直方向的坐标位置,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setReference(x?: number | string, y?: number | string): TSC;
|
||||
/**
|
||||
* 该指令表示标签打印偏移量多少设置
|
||||
* @param {number|string} n 打印偏移量
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setShift(n?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于选择对应的国际字符集
|
||||
* 001:USA
|
||||
* 002:French
|
||||
* 003:Latin America
|
||||
* 034:Spanish
|
||||
* 039:Italian
|
||||
* 044:United Kingdom
|
||||
* 046:Swedish
|
||||
* 047:Norwegian
|
||||
* 049:German
|
||||
* @param {number|string} country 字符集
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setCountry(country?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于选择对应的国际代码页
|
||||
* 8-bit codepage 字符集代表
|
||||
* 437:United States
|
||||
* 850:Multilingual
|
||||
* 852:Slavic
|
||||
* 860:Portuguese
|
||||
* 863:Canadian/French
|
||||
* 865:Nordic
|
||||
*
|
||||
* Windows code page
|
||||
* 1250:Central Europe
|
||||
* 1252:Latin I
|
||||
* 1253:Greek
|
||||
* 1254:Turkish
|
||||
*
|
||||
* 以下代码页仅限于12×24 dot 英数字体
|
||||
* WestEurope:WestEurope
|
||||
* Greek:Greek
|
||||
* Hebrew:Hebrew
|
||||
* EastEurope:EastEurope
|
||||
* Iran:Iran
|
||||
* IranII:IranII
|
||||
* Latvian:Latvian
|
||||
* Arabic:Arabic
|
||||
* Vietnam:Vietnam
|
||||
* Uygur:Uygur
|
||||
* Thai:Thai
|
||||
* 1252:Latin I
|
||||
* 1257:WPC1257
|
||||
* 1251:WPC1251
|
||||
* 866:Cyrillic
|
||||
* 858:PC858
|
||||
* 747:PC747
|
||||
* 864:PC864
|
||||
* 1001:PC1001
|
||||
* @param {number|string} n 代码页
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setCodepage(n?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于清除图像缓冲区(image buffer)的数据
|
||||
* 注:此项指令必须置于 SIZE 指令之后
|
||||
* @param {number|string} n 代码页
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setCls(): TSC;
|
||||
/**
|
||||
* 该指令用于将标签纸向前推送指定的长度
|
||||
* 打印机分辨率200 DPI:1 mm = 8 dots
|
||||
* 打印机分辨率300 DPI:1 mm = 12 dots
|
||||
* @param {number|string} n 1≤n≤9999,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setFeed(n: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于将标签纸向前推送指定的长度
|
||||
* 打印机分辨率200 DPI:1 mm = 8 dots
|
||||
* 打印机分辨率300 DPI:1 mm = 12 dots
|
||||
* @param {number|string} n 1≤n≤9999,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBackFeed(n: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于将标签纸向前推送指定的长度
|
||||
* 打印机分辨率200 DPI:1 mm = 8 dots
|
||||
* 打印机分辨率300 DPI:1 mm = 12 dots
|
||||
* @param {number|string} n 1≤n≤9999,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBackUp(n: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于控制打印机进一张标签纸
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setFromFeed(): TSC;
|
||||
/**
|
||||
* 在使用含有间隙或黑标的标签纸时,
|
||||
* 若不能确定第一张标签纸是否在正确打印位置时,
|
||||
* 此指令可将标签纸向前推送至下一张标签纸的起点开始打印。
|
||||
* 标签尺寸和间隙需要在本条指令前设置
|
||||
* 注:使用该指令时,纸张高度大于或等于30 mm
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setHome(): TSC;
|
||||
/**
|
||||
* 该指令用于打印出存储于影像缓冲区内的数据
|
||||
* @param {number|string} m 指定打印的份数(set)
|
||||
* @param {number|string} n 每张标签需重复打印的张数
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setPrint(m?: number | string, n?: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于控制蜂鸣器的频率,可设定10阶的声音,
|
||||
* 频率,可设定10阶的声音,每阶声音的长短由第二个参数控制
|
||||
* @param {number|string} m 指定打印的份数(set)
|
||||
* @param {number|string} n 每张标签需重复打印的张数
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setSound(level?: number, interval?: number): TSC;
|
||||
/**
|
||||
* 该指令用于设定打印机进纸时,若经过所设定的长度仍无法侦测到垂直间距,
|
||||
* 则打印机在连续纸模式工作。
|
||||
* @param {number|string} limit 英制系统(inch)
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setLimitFeed(limit?: number | string): TSC;
|
||||
/**
|
||||
* 不经自测动作,直接打印自检页信息。
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setSelfTest(): TSC;
|
||||
/**
|
||||
* 该指令用于在标签上画线
|
||||
* @param {number|string} x 线条左上角X坐标,单位dot
|
||||
* @param {number|string} y 线条左上角Y坐标,单位dot
|
||||
* @param {number|string} width 线宽,单位dot
|
||||
* @param {number|string} height 线高,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBar(x: number | string, y: number | string, width: number | string, height: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于在标签上画线
|
||||
* @param {number|string} x 线条左上角X坐标,单位dot
|
||||
* @param {number|string} y 线条左上角Y坐标,单位dot
|
||||
* @param {number|string} codeType 线宽,单位dot
|
||||
* @param {number|string} height 条形码高度,以点(dot)表示
|
||||
* @param {number|string} readable 0 表示人眼不可识,1表示人眼可识
|
||||
* @param {number|string} rotation 条形码旋转角度,顺时针方向
|
||||
* @param {number|string} narrow 窄bar宽度,以点(dot)表示
|
||||
* @param {number|string} wide 宽bar宽度,以点(dot)表示
|
||||
* @param {number|string} content 内容
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBarCode(x: number | string, y: number | string, codeType: number | string | undefined, height: number | string | undefined, readable: number | string | undefined, rotation: number | string | undefined, narrow: number | string | undefined, wide: number | string | undefined, content: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于在卷标上绘制矩形方框
|
||||
* @param {number|string} x1 线条左上角X坐标,单位dot
|
||||
* @param {number|string} y1 线条左上角Y坐标,单位dot
|
||||
* @param {number|string} x2 方框右下角X坐标,单位dot
|
||||
* @param {number|string} y2 方框右下角Y坐标,单位dot
|
||||
* @param {number|string} thickness 方框线宽,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBox(x1: number | string, y1: number | string, x2: number | string, y2: number | string, thickness: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于在卷标上绘制位图(非BMP格式图档)
|
||||
* @param {number|string} x 线条左上角X坐标,单位dot
|
||||
* @param {number|string} y 线条左上角Y坐标,单位dot
|
||||
* @param {number|string} width 方框右下角X坐标,单位dot
|
||||
* @param {number|string} height 方框右下角Y坐标,单位dot
|
||||
* @param {number|string} mode 方框线宽,单位dot
|
||||
* @param {number|string} bitmapData 方框线宽,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setBitmap(x: number | string, y: number | string, width: number | string, height: number | string, mode: number | string, bitmapData: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于清除影像缓冲区部分区域的数据
|
||||
* @param {number|string} x 清除区域的左上角X座标,单位dot
|
||||
* @param {number|string} y 清除区域的左上角Y座标,单位dot
|
||||
* @param {number|string} width 清除区域宽度,单位dot
|
||||
* @param {number|string} height 清除区域高度,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setErase(x: number | string, y: number | string, width: number | string, height: number | string): TSC;
|
||||
/**
|
||||
* 将指定的区域反相打印
|
||||
* @param {number|string} x 反相区域的左上角X座标,单位dot
|
||||
* @param {number|string} y 反相区域的左上角Y座标,单位dot
|
||||
* @param {number|string} width 反相区域宽度,单位dot
|
||||
* @param {number|string} height 反相区域高度,单位dot
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setReverse(x: number | string, y: number | string, width: number | string, height: number | string): TSC;
|
||||
/**
|
||||
* 该指令用于打印字符串
|
||||
* @param {number|string} x 文字X方向起始点坐标
|
||||
* @param {number|string} y 文字Y方向起始点坐标
|
||||
* @param {number|string} font 字体名称
|
||||
* @param {number|string} sx X 方向放大倍率1-10
|
||||
* @param {number|string} sy Y 方向放大倍率1-10
|
||||
* @param {number|string} content 内容
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setText(x: number | string, y: number | string, font: number | string, rotation: any, sx: number | string, sy: number | string, content: number | string): TSC;
|
||||
/**
|
||||
* 该指令用来打印二维码
|
||||
* @param {number|string} x 二维码水平方向起始点坐标
|
||||
* @param {number|string} y 反相区域的左上角Y座标,单位dot
|
||||
* @param {number|string} level 选择QRCODE纠错等级
|
||||
* @param {number|string} width 二维码宽度1-10
|
||||
* @param {number|string} mode 手动/自动编码
|
||||
* @param {number|string} rotation 旋转角度(顺时针方向)
|
||||
* @param {number|string} content 内容
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setQrcode(x: number | string, y: number | string, level: number | string, width: number | string, mode: number | string, rotation: number | string, content: number | string): TSC;
|
||||
/**
|
||||
* 该指令用来起动Key1 的预设功能
|
||||
* 预设为进纸功能
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setKey1(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 该指令用来起动Key2 的预设功能
|
||||
* 预设为暂停功能
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setKey2(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 该指令用来启动/关闭剥离模式,默认值为关闭
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setPeel(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 此命令是用来启用/禁用撕纸位置走到撕纸处,此设置关掉电源后将保存在打印机内
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setTear(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 此命令是用来启用/禁用撕纸位置走到撕纸处,此设置关掉电源后将保存在打印机内
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setStripper(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 此设置用于启用/禁用打印头合盖传感器。如果禁用合盖传感器,打印机头被打开时,将不会传回错误信息。
|
||||
* 此设置将保存在打印机内存。
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setHead(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 此设置用于启用/禁用打印头合盖传感器。如果禁用合盖传感器,打印机头被打开时,将不会传回错误信息。
|
||||
* 此设置将保存在打印机内存。
|
||||
* @param {"ON"|"OFF"|"AUTO"|string|number} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setHead2(k: "ON" | "OFF" | "AUTO" | string | number): TSC;
|
||||
/**
|
||||
* 此命令将禁用/启用标签机在无纸或开盖错误发生后,
|
||||
* 上纸或合盖后重新打印一次标签内容
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setReprint(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 设定开启/关闭碳带感应器,即切换热转式/热感印式打印。通常打印机于开启电源时,
|
||||
* 碳带感应器即会自动检测打印机是否已装上碳带,并藉此决定使用热感式或热转式打印。
|
||||
* 此项设定并不会存于打印机中。此方法仅适用于热转式机器。
|
||||
* @param {"ON"|"OFF"} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setRibbon(k: "ON" | "OFF"): TSC;
|
||||
/**
|
||||
* 此命令用于设置切刀状态,关闭打印机电源后,该设置将会被存储在打印机内存中。
|
||||
* @param {"OFF"|"BATCH"|string|number} k 开启按键/关闭按键
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setCutter(k: "OFF" | "BATCH" | string | number): TSC;
|
||||
/**
|
||||
* 此指令用于设置打印机自动返回状态
|
||||
* @param {"ON"|"OFF"|"BATCH"} k 开启按键/关闭按键
|
||||
* @param {string} content
|
||||
* @returns {TSC}
|
||||
*/
|
||||
setResponse(k: "ON" | "OFF" | "BATCH", content: string): TSC;
|
||||
}
|
||||
|
|
@ -5,15 +5,43 @@ import dts from "vite-plugin-dts";
|
|||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
const sharedOutput = {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: resolve(__dirname, "src"),
|
||||
assetFileNames: "assets/[name]-[hash][extname]",
|
||||
exports: "named",
|
||||
} as const;
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format) => `index.${format === "es" ? "mjs" : "cjs"}`,
|
||||
entry: {
|
||||
index: resolve(__dirname, "src/index.ts"),
|
||||
"input-rule/index": resolve(__dirname, "src/input-rule/index.ts"),
|
||||
"knock-test/index": resolve(__dirname, "src/knock-test/index.ts"),
|
||||
"permission/index": resolve(__dirname, "src/permission/index.ts"),
|
||||
"printer/index": resolve(__dirname, "src/printer/index.ts"),
|
||||
"time/index": resolve(__dirname, "src/time/index.ts"),
|
||||
"timer/index": resolve(__dirname, "src/timer/index.ts"),
|
||||
"ui/index": resolve(__dirname, "src/ui/index.ts"),
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
external: ["dayjs", "lodash-es", "text-encoding", "tslib"],
|
||||
output: [
|
||||
{
|
||||
...sharedOutput,
|
||||
format: "es",
|
||||
entryFileNames: "[name].mjs",
|
||||
chunkFileNames: "chunks/[name]-[hash].mjs",
|
||||
},
|
||||
{
|
||||
...sharedOutput,
|
||||
format: "cjs",
|
||||
entryFileNames: "[name].cjs",
|
||||
chunkFileNames: "chunks/[name]-[hash].cjs",
|
||||
},
|
||||
],
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
|
|
@ -24,7 +52,7 @@ export default defineConfig({
|
|||
},
|
||||
plugins: [
|
||||
dts({
|
||||
include: ["src"],
|
||||
include: ["src", "types"],
|
||||
outDir: "dist",
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# @r-utils/uni-app
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 修复vue3未导出
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 修复类型问题
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @r-utils/common@2.0.0
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @r-utils/common@1.4.0
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# @r-utils/uni-app
|
||||
|
||||
仅用于 uni-app 项目的工具包,封装请求、上传、NFC、蓝牙、打印、Vue3 Hooks 和常用 uni API 辅助方法。
|
||||
|
||||
## 适用范围
|
||||
|
||||
- 适用于 uni-app 项目(含 uni 运行时能力)。
|
||||
- 适用于需要复用请求、上传、蓝牙、NFC、打印、Vue3 Hooks 等能力的业务项目。
|
||||
|
||||
## 不适用范围
|
||||
|
||||
- 不适用于普通 Web Vue2/Vue3 项目。
|
||||
- 不适用于没有 `uni` 运行时的纯 Node.js 环境。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @r-utils/uni-app
|
||||
```
|
||||
|
||||
## 导入方式
|
||||
|
||||
### 推荐:根入口导入
|
||||
|
||||
大多数场景推荐从根入口导入,路径简单,使用心智负担更低。
|
||||
|
||||
```ts
|
||||
import { Request, showToast, toPromise, upload, Printer, useShare } from "@r-utils/uni-app";
|
||||
```
|
||||
|
||||
### 兼容:子路径导入
|
||||
|
||||
如果你希望模块边界更清晰,也可以使用子路径导入。两种方式都支持。
|
||||
|
||||
```ts
|
||||
import { Request } from "@r-utils/uni-app/request";
|
||||
import { showToast, toPromise } from "@r-utils/uni-app/uni-helper";
|
||||
import { upload } from "@r-utils/uni-app/upload";
|
||||
import { Printer } from "@r-utils/uni-app/printer";
|
||||
import { nfcScan } from "@r-utils/uni-app/nfc";
|
||||
import { BluetoothUtils } from "@r-utils/uni-app/bluetooth-utils";
|
||||
import { useShare, usePageTab, useTopSticky } from "@r-utils/uni-app/vue3";
|
||||
```
|
||||
|
||||
## 导出模块
|
||||
|
||||
| 子路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `@r-utils/uni-app/request` | 类 axios 的 `uni.request` 封装 |
|
||||
| `@r-utils/uni-app/uni-helper` | 常用 uni API 辅助函数 |
|
||||
| `@r-utils/uni-app/upload` | 上传相关工具 |
|
||||
| `@r-utils/uni-app/printer` | 打印机相关工具 |
|
||||
| `@r-utils/uni-app/nfc` | NFC 相关工具 |
|
||||
| `@r-utils/uni-app/bluetooth-utils` | 蓝牙相关工具 |
|
||||
| `@r-utils/uni-app/vue3` | uni-app Vue3 Hooks |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 请求封装
|
||||
|
||||
```ts
|
||||
import { Request } from "@r-utils/uni-app";
|
||||
|
||||
const request = new Request({
|
||||
baseURL: "https://api.example.com",
|
||||
});
|
||||
|
||||
const res = await request.get("/users", {
|
||||
data: { pageNum: 1, pageSize: 10 },
|
||||
});
|
||||
```
|
||||
|
||||
### 使用 showToast
|
||||
|
||||
```ts
|
||||
import { showToast } from "@r-utils/uni-app";
|
||||
|
||||
showToast("保存成功", "success");
|
||||
```
|
||||
|
||||
### 子路径导入请求模块
|
||||
|
||||
```ts
|
||||
import { Request } from "@r-utils/uni-app/request";
|
||||
|
||||
const request = new Request({
|
||||
baseURL: "https://api.example.com",
|
||||
});
|
||||
```
|
||||
|
||||
### 使用 Vue3 Hooks
|
||||
|
||||
```ts
|
||||
import { useShare } from "@r-utils/uni-app/vue3";
|
||||
|
||||
const { getShareOptions } = useShare({
|
||||
title: "页面标题",
|
||||
query: { id: "1" },
|
||||
});
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
|
||||
- 本包依赖 uni-app 运行时能力,需在真实 uni-app 环境中验证平台相关功能。
|
||||
- NFC、蓝牙、打印功能通常依赖具体平台和设备能力,建议按目标端单独测试。
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@r-utils/uni-app",
|
||||
"version": "1.3.0",
|
||||
"version": "2.0.2",
|
||||
"private": false,
|
||||
"description": "uni-app工具库",
|
||||
"type": "module",
|
||||
|
|
@ -8,13 +8,50 @@
|
|||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./bluetooth-utils": {
|
||||
"types": "./dist/bluetooth-utils/index.d.ts",
|
||||
"import": "./dist/bluetooth-utils/index.mjs",
|
||||
"require": "./dist/bluetooth-utils/index.cjs"
|
||||
},
|
||||
"./nfc": {
|
||||
"types": "./dist/nfc/index.d.ts",
|
||||
"import": "./dist/nfc/index.mjs",
|
||||
"require": "./dist/nfc/index.cjs"
|
||||
},
|
||||
"./printer": {
|
||||
"types": "./dist/printer/index.d.ts",
|
||||
"import": "./dist/printer/index.mjs",
|
||||
"require": "./dist/printer/index.cjs"
|
||||
},
|
||||
"./request": {
|
||||
"types": "./dist/request/index.d.ts",
|
||||
"import": "./dist/request/index.mjs",
|
||||
"require": "./dist/request/index.cjs"
|
||||
},
|
||||
"./uni-helper": {
|
||||
"types": "./dist/uni-helper/index.d.ts",
|
||||
"import": "./dist/uni-helper/index.mjs",
|
||||
"require": "./dist/uni-helper/index.cjs"
|
||||
},
|
||||
"./upload": {
|
||||
"types": "./dist/upload/index.d.ts",
|
||||
"import": "./dist/upload/index.mjs",
|
||||
"require": "./dist/upload/index.cjs"
|
||||
},
|
||||
"./vue3": {
|
||||
"types": "./dist/vue3/index.d.ts",
|
||||
"import": "./dist/vue3/index.mjs",
|
||||
"require": "./dist/vue3/index.cjs"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"uni-app"
|
||||
|
|
@ -43,13 +80,15 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint --ext .js,ts --fix src",
|
||||
"release": "standard-version",
|
||||
"format": "prettier --write src",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jonny1994/qqmap-wx-jssdk": "^1.4.0",
|
||||
"@r-utils/common": "workspace:^",
|
||||
"chroma-js": "catalog:",
|
||||
"lodash-es": "catalog:",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { toPromise, showToast } from "@/uni-helper";
|
||||
/* eslint-disable no-useless-assignment, @typescript-eslint/no-unused-vars */
|
||||
import { showToast } from "@/uni-helper";
|
||||
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
const isAndroidApp =
|
||||
|
|
@ -237,7 +238,7 @@ export class BluetoothUtils {
|
|||
res = await cb();
|
||||
} else {
|
||||
// 失败重连5次
|
||||
for (var i = 1; i <= 5; i++) {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
try {
|
||||
await uni.createBLEConnection({
|
||||
deviceId: device.deviceId,
|
||||
|
|
@ -397,12 +398,12 @@ export class BluetoothUtils {
|
|||
private static async _getBLEMTU(device: Device) {
|
||||
console.log("_getBLEMTU", device);
|
||||
// return new Promise<number>(async (resolve, reject) => {
|
||||
return uni.getBLEMTU({
|
||||
deviceId: device.deviceId,
|
||||
// success: (res) => {
|
||||
// resolve(res.mtu);
|
||||
// },
|
||||
});
|
||||
return uni.getBLEMTU({
|
||||
deviceId: device.deviceId,
|
||||
// success: (res) => {
|
||||
// resolve(res.mtu);
|
||||
// },
|
||||
});
|
||||
// });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export * from "./printer";
|
|||
export * from "./request";
|
||||
export * from "./uni-helper";
|
||||
export * from "./upload";
|
||||
export * from "./vue3";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-useless-assignment, @typescript-eslint/no-unused-vars */
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
const isAndroid =
|
||||
systemInfo.uniPlatform === "app" && systemInfo.osName === "android";
|
||||
|
|
@ -195,12 +196,16 @@ export class AndroidNfcUtil {
|
|||
|
||||
static async scan() {
|
||||
const androidNfcUtil = new AndroidNfcUtil();
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
await androidNfcUtil.startNfcScan();
|
||||
androidNfcUtil.addDiscoveredListener((res) => {
|
||||
androidNfcUtil.stopNfcScan();
|
||||
resolve(res);
|
||||
});
|
||||
return await new Promise((resolve, reject) => {
|
||||
androidNfcUtil
|
||||
.startNfcScan()
|
||||
.then(() => {
|
||||
androidNfcUtil.addDiscoveredListener((res) => {
|
||||
androidNfcUtil.stopNfcScan();
|
||||
resolve(res);
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function isNfcEnabled() {
|
|||
}
|
||||
}
|
||||
|
||||
export function getNFCUtil(){
|
||||
export function getNFCUtil() {
|
||||
if (isAndroidApp) {
|
||||
return new AndroidNfcUtil();
|
||||
} else if (isAndroidWeixin) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ export async function startNfcScan() {
|
|||
await nfcAdapter.startDiscovery();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
nfcAdapter.onDiscovered((res) => {
|
||||
console.log("onDiscovered", res);
|
||||
const arr = Array.from(new Int8Array(res.id));
|
||||
resolve(arr);
|
||||
});
|
||||
try {
|
||||
nfcAdapter.onDiscovered((res) => {
|
||||
console.log("onDiscovered", res);
|
||||
const arr = Array.from(new Int8Array(res.id));
|
||||
resolve(arr);
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -67,12 +71,16 @@ export class WeixinNfcUtil {
|
|||
|
||||
static async scan() {
|
||||
const weixinNfcUtil = new WeixinNfcUtil();
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
await weixinNfcUtil.startNfcScan();
|
||||
weixinNfcUtil.addDiscoveredListener((res) => {
|
||||
weixinNfcUtil.stopNfcScan();
|
||||
resolve(res);
|
||||
});
|
||||
return await new Promise((resolve, reject) => {
|
||||
weixinNfcUtil
|
||||
.startNfcScan()
|
||||
.then(() => {
|
||||
weixinNfcUtil.addDiscoveredListener((res) => {
|
||||
weixinNfcUtil.stopNfcScan();
|
||||
resolve(res);
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,63 @@
|
|||
import { BluetoothUtils, Device } from "@/bluetooth-utils";
|
||||
/*
|
||||
* @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,
|
||||
data.slice(0, 100),
|
||||
data.slice(data.length - 100, data.length)
|
||||
data.slice(data.length - 100, data.length),
|
||||
);
|
||||
let res = null;
|
||||
let res;
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
if (systemInfo.uniPlatform === "app" && systemInfo.osName === "android") {
|
||||
res = await BluetoothUtils.sendDataAndroid(this.device, data);
|
||||
|
|
@ -33,7 +71,15 @@ export class Printer {
|
|||
return res;
|
||||
}
|
||||
|
||||
async _print(data: number[]):Promise<any> {
|
||||
/**
|
||||
* 通过 BLE 写入特征值分包发送打印数据(内部方法)
|
||||
*
|
||||
* 将数据按 size 分包,每包创建 ArrayBuffer 写入 BLE 特征值,
|
||||
* 包间等待 30ms 确保传输稳定,递归发送直到所有数据写完。
|
||||
*
|
||||
* @param data 剩余待发送的打印数据
|
||||
*/
|
||||
async _print(data: number[]): Promise<void> {
|
||||
const size = Math.min(data.length, this.size);
|
||||
if (size === 0) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,37 +1,66 @@
|
|||
/*
|
||||
* @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;
|
||||
|
||||
/** 请求配置 */
|
||||
export interface Config<T extends DataType>
|
||||
extends Partial<UniApp.RequestOptions> {
|
||||
export interface Config<
|
||||
T extends DataType,
|
||||
> extends Partial<UniApp.RequestOptions> {
|
||||
baseURL?: string;
|
||||
data?: T;
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** 响应 */
|
||||
export interface Response<
|
||||
T extends DataType,
|
||||
D extends DataType,
|
||||
C extends Config<D> = Config<D>
|
||||
> extends UniApp.RequestSuccessCallbackResult {
|
||||
C extends Config<D> = Config<D>,
|
||||
>
|
||||
extends UniApp.RequestSuccessCallbackResult {
|
||||
data: T;
|
||||
errMsg?: string;
|
||||
config: C;
|
||||
}
|
||||
|
||||
/** 判断是否为绝对地址 */
|
||||
const isAbsoluteUrl = (url: string): boolean =>
|
||||
/^(?:[a-z][a-z\d+\-.]*:)?\/\//i.test(url);
|
||||
|
||||
/** 拼接基础地址 */
|
||||
const joinBaseURL = (baseURL: string, url: string): string => {
|
||||
if (!baseURL || isAbsoluteUrl(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const normalizedBaseURL = baseURL.endsWith("/")
|
||||
? baseURL.slice(0, -1)
|
||||
: baseURL;
|
||||
const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
|
||||
|
||||
return `${normalizedBaseURL}${normalizedUrl}`;
|
||||
};
|
||||
|
||||
/** 成功拦截器 */
|
||||
type FulfilledInterceptor<R, T> = (res: R) => T | Promise<T>;
|
||||
/** 失败拦截器 */
|
||||
type RejectedInterceptor = (error: any) => any;
|
||||
type RejectedInterceptor = (error: unknown) => unknown | Promise<unknown>;
|
||||
/** 拦截器管理 */
|
||||
class InterceptorManager<R> {
|
||||
/** 拦截器列表 */
|
||||
handlers: [FulfilledInterceptor<R, any>?, RejectedInterceptor?][] = [];
|
||||
handlers: [FulfilledInterceptor<R, unknown>?, RejectedInterceptor?][] = [];
|
||||
|
||||
/** 添加拦截器 */
|
||||
add<T = R>(
|
||||
onFulfilled?: FulfilledInterceptor<R, T>,
|
||||
onRejected?: RejectedInterceptor
|
||||
onRejected?: RejectedInterceptor,
|
||||
): number {
|
||||
this.handlers.push([onFulfilled, onRejected]);
|
||||
return this.handlers.length - 1;
|
||||
|
|
@ -44,9 +73,9 @@ class InterceptorManager<R> {
|
|||
|
||||
forEach(
|
||||
fn: (
|
||||
onFulfilled?: FulfilledInterceptor<R, any>,
|
||||
onRejected?: RejectedInterceptor
|
||||
) => void
|
||||
onFulfilled?: FulfilledInterceptor<R, unknown>,
|
||||
onRejected?: RejectedInterceptor,
|
||||
) => void,
|
||||
) {
|
||||
this.handlers.forEach(([onFulfilled, onRejected]) => {
|
||||
fn(onFulfilled, onRejected);
|
||||
|
|
@ -59,16 +88,16 @@ class InterceptorManager<R> {
|
|||
*/
|
||||
export class Request {
|
||||
/** 请求配置 */
|
||||
config: Config<any>;
|
||||
config: Config<DataType>;
|
||||
/** 拦截器 */
|
||||
interceptors: {
|
||||
/** 请求拦截器 */
|
||||
request: InterceptorManager<Config<any>>;
|
||||
request: InterceptorManager<Config<DataType>>;
|
||||
/** 响应拦截器 */
|
||||
response: InterceptorManager<any>;
|
||||
response: InterceptorManager<unknown>;
|
||||
};
|
||||
|
||||
constructor(config: Config<any> = { url: "" }) {
|
||||
constructor(config: Config<DataType> = { url: "" }) {
|
||||
this.config = config;
|
||||
this.interceptors = {
|
||||
request: new InterceptorManager(),
|
||||
|
|
@ -76,79 +105,86 @@ export class Request {
|
|||
};
|
||||
}
|
||||
|
||||
/** 依次执行请求拦截器 */
|
||||
private async runRequestInterceptors<REQD extends DataType>(
|
||||
config: Config<REQD>,
|
||||
): Promise<Config<REQD>> {
|
||||
let currentConfig = config;
|
||||
|
||||
for (const [onFulfilled, onRejected] of this.interceptors.request
|
||||
.handlers) {
|
||||
try {
|
||||
if (onFulfilled) {
|
||||
currentConfig = (await onFulfilled(currentConfig)) as Config<REQD>;
|
||||
}
|
||||
} catch (error) {
|
||||
if (onRejected) {
|
||||
currentConfig = (await onRejected(error)) as Config<REQD>;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
/** 依次执行响应拦截器 */
|
||||
private async runResponseInterceptors<R>(response: R): Promise<R> {
|
||||
let currentResponse = response;
|
||||
|
||||
for (const [onFulfilled, onRejected] of this.interceptors.response
|
||||
.handlers) {
|
||||
try {
|
||||
if (onFulfilled) {
|
||||
currentResponse = (await onFulfilled(currentResponse)) as R;
|
||||
}
|
||||
} catch (error) {
|
||||
if (onRejected) {
|
||||
currentResponse = (await onRejected(error)) as R;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求主方法
|
||||
*/
|
||||
async request<
|
||||
RESPD extends DataType,
|
||||
REQD extends DataType,
|
||||
R = Response<RESPD, REQD>
|
||||
R = Response<RESPD, REQD>,
|
||||
>(config: Config<REQD>): Promise<R> {
|
||||
// 合并方法配置 与 实例配置
|
||||
let newConfig = Object.assign({}, this.config, config);
|
||||
let newConfig = Object.assign({}, this.config, config) as Config<REQD>;
|
||||
|
||||
// 赋值默认URL
|
||||
newConfig.url = newConfig.url ?? "";
|
||||
// 如果设置了 baseURL 并且 url 不是绝对路径,则拼接 baseURL
|
||||
if (newConfig.baseURL && !/https?\/\//.test(newConfig.url)) {
|
||||
newConfig.url = newConfig.baseURL + newConfig.url;
|
||||
}
|
||||
|
||||
// 执行请求拦截器
|
||||
this.interceptors.request.forEach((onFulfilled, onRejected) => {
|
||||
try {
|
||||
if (onFulfilled) {
|
||||
newConfig = onFulfilled(newConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onRejected) {
|
||||
throw onRejected(error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
newConfig = await this.runRequestInterceptors(newConfig);
|
||||
|
||||
// 如果设置了 baseURL 并且 url 不是绝对路径,则拼接 baseURL
|
||||
newConfig.url = joinBaseURL(newConfig.baseURL ?? "", newConfig.url ?? "");
|
||||
|
||||
// 发送请求
|
||||
// let responsePromise = await new Promise<Response<RESPD>>((resolve, reject) => {
|
||||
// uni.request({
|
||||
// ...newConfig,
|
||||
// success(result) {
|
||||
// resolve(result);
|
||||
// },
|
||||
// fail(result) {
|
||||
// reject(result);
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
let responsePromise = await (<Promise<R>>uni.request({
|
||||
let responsePromise = (await (uni.request({
|
||||
...newConfig,
|
||||
url: newConfig.url,
|
||||
}));
|
||||
}) as Promise<R>)) as R;
|
||||
|
||||
// 替换新请求的配置
|
||||
responsePromise = {
|
||||
...responsePromise,
|
||||
config: newConfig,
|
||||
};
|
||||
config: newConfig as Config<REQD>,
|
||||
} as R;
|
||||
|
||||
// 执行响应拦截器
|
||||
this.interceptors.response.forEach((onFulfilled, onRejected) => {
|
||||
// this.interceptors?.response.handlers.forEach((handler) => {
|
||||
// const [onFulfilled, onRejected] = handler;
|
||||
// responsePromise = responsePromise.then(onFulfilled, onRejected);
|
||||
try {
|
||||
if (typeof onFulfilled !== "undefined") {
|
||||
responsePromise = onFulfilled(responsePromise);
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof onRejected !== "undefined") {
|
||||
onRejected(error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
responsePromise = await this.runResponseInterceptors(responsePromise);
|
||||
|
||||
return responsePromise;
|
||||
}
|
||||
|
|
@ -156,7 +192,7 @@ export class Request {
|
|||
/** GET 请求 */
|
||||
get<RESPD extends DataType, REQD extends DataType, R = Response<RESPD, REQD>>(
|
||||
url: string,
|
||||
config?: Config<REQD>
|
||||
config?: Config<REQD>,
|
||||
): Promise<R> {
|
||||
return this.request({ ...config, url, method: "GET" });
|
||||
}
|
||||
|
|
@ -165,17 +201,17 @@ export class Request {
|
|||
post<
|
||||
RESPD extends DataType,
|
||||
REQD extends DataType,
|
||||
R = Response<RESPD, REQD>
|
||||
R = Response<RESPD, REQD>,
|
||||
>(url: string, data?: REQD, config?: Config<REQD>): Promise<R> {
|
||||
return this.request({ ...config, url, method: "POST", data });
|
||||
}
|
||||
|
||||
/** PUT 请求 */
|
||||
put<
|
||||
RESPD extends DataType,
|
||||
REQD extends DataType,
|
||||
R = Response<RESPD, REQD>
|
||||
>(url: string, data?: REQD, config?: Config<REQD>): Promise<R> {
|
||||
put<RESPD extends DataType, REQD extends DataType, R = Response<RESPD, REQD>>(
|
||||
url: string,
|
||||
data?: REQD,
|
||||
config?: Config<REQD>,
|
||||
): Promise<R> {
|
||||
return this.request({ ...config, url, method: "PUT", data });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
/*
|
||||
* @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;
|
||||
|
|
@ -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)
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@
|
|||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,45 @@
|
|||
export class AndroidNfcUtil {
|
||||
/**
|
||||
* 是否支持NFC
|
||||
* @returns
|
||||
*/
|
||||
static isNfcSupported(): boolean;
|
||||
/**
|
||||
* 是否开启NFC
|
||||
* @returns {boolean} true 已开启
|
||||
*/
|
||||
static isNfcEnabled(): Promise<boolean>;
|
||||
static scan(): Promise<number[]>;
|
||||
main: PlusAndroidInstanceObject | null;
|
||||
nfcAdapter: PlusAndroidInstanceObject | null;
|
||||
pendingIntent: PlusAndroidInstanceObject | null;
|
||||
intentFiltersArray: PlusAndroidInstanceObject[] | null;
|
||||
techListsArray: string[][] | null;
|
||||
discoveredListenerList: Array<(id: number[]) => void>;
|
||||
/**
|
||||
* 内部发现处理器
|
||||
*/
|
||||
_discoveredHandler: (() => void) | null;
|
||||
/**
|
||||
* 内部恢复处理器
|
||||
*/
|
||||
_resumeHandler: (() => void) | null;
|
||||
/**
|
||||
* 添加NFC发现监听器
|
||||
* @param cb 回调函数,接收读取到的NFC ID
|
||||
*/
|
||||
addDiscoveredListener(cb: (id: number[]) => void): void;
|
||||
/**
|
||||
* 开始NFC扫描
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
startNfcScan(): Promise<number[]>;
|
||||
stopNfcScan(): Promise<void>;
|
||||
/**
|
||||
* 初始化NFC并开启监听
|
||||
*/
|
||||
init(): Promise<void>;
|
||||
resumeHandler(): void;
|
||||
discoveredHandler(): void;
|
||||
readId(): number[];
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { AndroidNfcUtil } from "./android";
|
||||
import { WeixinNfcUtil } from "./weixin";
|
||||
|
||||
export function nfcScan(): Promise<number[] | undefined>;
|
||||
export function isNfcEnabled(): Promise<boolean | undefined>;
|
||||
export function getNFCUtil(): AndroidNfcUtil | WeixinNfcUtil | undefined;
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
declare namespace WechatMiniprogram {
|
||||
/** 微信小程序 NFC 适配器 */
|
||||
interface NFCAdapter {
|
||||
/** 开始监听 NFC 标签发现事件 */
|
||||
startDiscovery(options?: {
|
||||
/** 启动成功回调 */
|
||||
success?: (res: NFCError) => void;
|
||||
/** 启动失败回调 */
|
||||
fail?: (res: NFCError) => void;
|
||||
}): void;
|
||||
/** 停止监听 NFC 标签发现事件 */
|
||||
stopDiscovery(options?: {
|
||||
/** 停止成功回调 */
|
||||
success?: (res: NFCError) => void;
|
||||
/** 停止失败回调 */
|
||||
fail?: (res: NFCError) => void;
|
||||
}): void;
|
||||
/** 注册 NFC 标签发现回调 */
|
||||
onDiscovered(callback: (res: OnDiscoveredCallbackResult) => void): void;
|
||||
/** 取消注册 NFC 标签发现回调 */
|
||||
offDiscovered(callback?: (res: OnDiscoveredCallbackResult) => void): void;
|
||||
}
|
||||
|
||||
/** 微信小程序 NFC 操作结果 */
|
||||
interface NFCError {
|
||||
/** 错误码 */
|
||||
errCode?: number;
|
||||
/** 错误信息 */
|
||||
errMsg?: string;
|
||||
}
|
||||
|
||||
/** 微信小程序 NFC 标签发现结果 */
|
||||
interface OnDiscoveredCallbackResult {
|
||||
/** NFC 标签 ID */
|
||||
id: ArrayBuffer;
|
||||
/** NFC 标签消息列表 */
|
||||
messages?: unknown[];
|
||||
/** NFC 支持的技术列表 */
|
||||
techs?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
export function startNfcScan(): Promise<number[]>;
|
||||
|
||||
export class WeixinNfcUtil {
|
||||
/**
|
||||
* 是否开启NFC
|
||||
* NOTE 微信无法判断是否开启
|
||||
* @returns {boolean} true 已开启
|
||||
*/
|
||||
static isNfcEnabled(): Promise<boolean>;
|
||||
static scan(): Promise<number[]>;
|
||||
nfcAdapter: WechatMiniprogram.NFCAdapter | null;
|
||||
discoveredListenerList: Array<(id: number[]) => void>;
|
||||
/**
|
||||
* 内部发现处理器
|
||||
*/
|
||||
_discoveredHandler:
|
||||
| ((res: WechatMiniprogram.OnDiscoveredCallbackResult) => void)
|
||||
| null;
|
||||
discoveredHandler(res: WechatMiniprogram.OnDiscoveredCallbackResult): void;
|
||||
/**
|
||||
* 添加NFC发现监听器
|
||||
* @param cb 回调函数,接收读取到的NFC ID
|
||||
*/
|
||||
addDiscoveredListener(cb: (id: number[]) => void): void;
|
||||
startNfcScan(): Promise<WechatMiniprogram.NFCError>;
|
||||
stopNfcScan(): Promise<WechatMiniprogram.NFCError>;
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import * as QQMapWX from "@jonny1994/qqmap-wx-jssdk";
|
||||
|
||||
export * from "@jonny1994/qqmap-wx-jssdk";
|
||||
/**
|
||||
* 行政区划列表
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/// <reference types="@dcloudio/types" />
|
||||
|
||||
declare namespace UniNamespace {
|
||||
type LocaldataItem<T=string> = {
|
||||
type LocaldataItem<T = string> = {
|
||||
text: string;
|
||||
value: T;
|
||||
children?: LocaldataItem<T>[];
|
||||
|
|
@ -111,7 +111,7 @@ interface PlusIo {
|
|||
resolveLocalFileSystemURL(
|
||||
url?: string,
|
||||
succesCB?: (result: PlusIoFileEntry) => void,
|
||||
errorCB?: (result: any) => void,
|
||||
errorCB?: (result: PlusIoDirectoryEntry) => void,
|
||||
): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,48 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
|
||||
entry: {
|
||||
index: resolve(__dirname, "src/index.ts"),
|
||||
"bluetooth-utils/index": resolve(
|
||||
__dirname,
|
||||
"src/bluetooth-utils/index.ts",
|
||||
),
|
||||
"nfc/index": resolve(__dirname, "src/nfc/index.js"),
|
||||
"printer/index": resolve(__dirname, "src/printer/index.ts"),
|
||||
"request/index": resolve(__dirname, "src/request/index.ts"),
|
||||
"uni-helper/index": resolve(__dirname, "src/uni-helper/index.ts"),
|
||||
"upload/index": resolve(__dirname, "src/upload/index.ts"),
|
||||
"vue3/index": resolve(__dirname, "src/vue3/index.ts"),
|
||||
},
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format, entryName) =>
|
||||
`${entryName}.${format === "es" ? "mjs" : "cjs"}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
// uni / plus / wx 是 uni-app 运行时注入的全局变量,无需 external
|
||||
external: ['vue', 'lodash', /^@r-utils\/.*/],
|
||||
external: ["vue", "lodash", /^@r-utils\/.*/],
|
||||
output: {
|
||||
chunkFileNames: "chunks/[name]-[hash].js",
|
||||
assetFileNames: "assets/[name]-[hash][extname]",
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
include: ['src', 'types'],
|
||||
outDir: 'dist',
|
||||
include: ["src", "types"],
|
||||
outDir: "dist",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# @r-utils/uview-plus
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 添加工具
|
||||
|
|
@ -1,6 +1,16 @@
|
|||
# @r-utils/uview-plus
|
||||
|
||||
基于 [uview-plus](https://uview-plus.jiangruyi.com/) 的 Vue3 组合式 API 工具 Hooks,适用于 uni-app 项目。
|
||||
在项目已使用 [uview-plus](https://uview-plus.jiangruyi.com/) 的前提下,提供适用于 uview-plus 组件的组合式 API Hooks。
|
||||
|
||||
## 适用范围
|
||||
|
||||
- 适用于已接入 `uview-plus` 的项目。
|
||||
- 典型场景是 uni-app + Vue3 + uview-plus。
|
||||
|
||||
## 不适用范围
|
||||
|
||||
- 未使用 `uview-plus` 的项目不建议使用。
|
||||
- 不适用于 Vue2 项目。
|
||||
|
||||
## 安装
|
||||
|
||||
|
|
@ -8,102 +18,69 @@
|
|||
pnpm add @r-utils/uview-plus
|
||||
```
|
||||
|
||||
## 使用
|
||||
## 导入方式
|
||||
|
||||
### 推荐:根入口导入
|
||||
|
||||
大多数场景推荐从根入口导入,路径简单,使用心智负担更低。
|
||||
|
||||
```ts
|
||||
import { usePickerSingle, usePicker, useCalendar } from '@r-utils/uview-plus'
|
||||
import {
|
||||
usePickerSingle,
|
||||
usePicker,
|
||||
useDateTimePicker,
|
||||
useCalendar,
|
||||
} from "@r-utils/uview-plus";
|
||||
```
|
||||
|
||||
## API
|
||||
### 兼容:子路径导入
|
||||
|
||||
### `usePickerSingle(options)`
|
||||
|
||||
单列 Picker 封装。
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `value` | `unknown \| Ref<unknown>` | `null` | 选中的值 |
|
||||
| `show` | `boolean \| Ref<boolean>` | `false` | 是否显示 |
|
||||
| `indexes` | `Array<number \| null> \| Ref<...>` | `[null]` | 选中的索引 |
|
||||
| `list` | `PickerColumns[0] \| Ref<...>` | `[]` | 列数据 |
|
||||
| `textName` | `string` | `'text'` | 显示字段名 |
|
||||
| `valueName` | `string` | `'value'` | 值字段名 |
|
||||
| `placeholder` | `string` | `'请选择'` | 占位文本 |
|
||||
|
||||
**返回值:**
|
||||
如果你希望模块边界更清晰,也可以使用子路径导入。两种方式都支持。
|
||||
|
||||
```ts
|
||||
{
|
||||
value, show, indexes, columns, text, defaultIndex,
|
||||
showPicker, hidePicker, handleConfirm, handleClose
|
||||
}
|
||||
import { usePickerSingle } from "@r-utils/uview-plus/picker-single";
|
||||
import { usePicker } from "@r-utils/uview-plus/picker";
|
||||
import { useDateTimePicker } from "@r-utils/uview-plus/datetime-picker";
|
||||
import { useCalendar } from "@r-utils/uview-plus/calendar";
|
||||
```
|
||||
|
||||
---
|
||||
## 导出模块
|
||||
|
||||
### `usePicker(options)`
|
||||
| 子路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `@r-utils/uview-plus/picker-single` | 单列 picker Hook |
|
||||
| `@r-utils/uview-plus/picker` | 多列 picker Hook |
|
||||
| `@r-utils/uview-plus/datetime-picker` | 时间选择器 Hook |
|
||||
| `@r-utils/uview-plus/calendar` | 日历选择器 Hook |
|
||||
|
||||
多列 Picker 封装。
|
||||
## 使用示例
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `value` | `unknown[] \| Ref<unknown[]>` | `[]` | 选中的值数组 |
|
||||
| `show` | `boolean \| Ref<boolean>` | `false` | 是否显示 |
|
||||
| `indexes` | `Array<number \| null> \| Ref<...>` | `[]` | 选中的索引数组 |
|
||||
| `columns` | `PickerColumns \| Ref<PickerColumns>` | `[]` | 列数据 |
|
||||
| `textName` | `string` | `'text'` | 显示字段名 |
|
||||
| `valueName` | `string` | `'value'` | 值字段名 |
|
||||
| `placeholder` | `string` | `'请选择'` | 占位文本 |
|
||||
| `separator` | `string` | `' '` | 多列值拼接分隔符 |
|
||||
|
||||
**返回值:**
|
||||
### 单列选择器
|
||||
|
||||
```ts
|
||||
{
|
||||
value, show, indexes, columns, text, defaultIndex,
|
||||
showPicker, hidePicker, handleConfirm, handleClose
|
||||
}
|
||||
import { usePickerSingle } from "@r-utils/uview-plus";
|
||||
|
||||
const picker = usePickerSingle({
|
||||
list: [
|
||||
{ text: "启用", value: 1 },
|
||||
{ text: "禁用", value: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
picker.showPicker();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `useCalendar(options)`
|
||||
|
||||
日历选择封装。
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `value` | `string \| string[] \| Ref<...>` | `null` | 选中的日期 |
|
||||
| `show` | `boolean \| Ref<boolean>` | `false` | 是否显示 |
|
||||
| `mode` | `'single' \| 'multiple' \| 'range' \| Ref<...>` | `'single'` | 日历模式 |
|
||||
| `placeholder` | `string` | `'请选择'` | 占位文本 |
|
||||
|
||||
**返回值:**
|
||||
### 子路径导入
|
||||
|
||||
```ts
|
||||
{
|
||||
value, show, text,
|
||||
showCalendar, hideCalendar, handleConfirm, handleClose
|
||||
}
|
||||
import { usePicker } from "@r-utils/uview-plus/picker";
|
||||
|
||||
const picker = usePicker({
|
||||
columns: [[{ text: "浙江", value: "zhejiang" }]],
|
||||
});
|
||||
```
|
||||
|
||||
## 类型声明
|
||||
## 注意事项
|
||||
|
||||
包内置了 `UViewPlus` namespace 类型声明,无需额外引入。
|
||||
|
||||
```ts
|
||||
declare namespace UViewPlus {
|
||||
type PickerColumns = any[][];
|
||||
type PickerValue<T extends PickerColumns = PickerColumns> = T[number][number][];
|
||||
type PickerConfirmEvent<T extends PickerColumns = PickerColumns> = {
|
||||
indexs: number[];
|
||||
value: PickerValue<T>;
|
||||
values: T;
|
||||
};
|
||||
type CalendarConfirmEvent = string[];
|
||||
}
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
ISC
|
||||
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
|
||||
- 本包依赖 Vue3 和 uview-plus,请确保业务项目已安装并正确配置。
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@r-utils/uview-plus",
|
||||
"version": "1.2.1",
|
||||
"version": "2.0.0",
|
||||
"private": false,
|
||||
"description": "uview-plus 组合式 API Hooks",
|
||||
"type": "module",
|
||||
|
|
@ -8,13 +8,35 @@
|
|||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./calendar": {
|
||||
"types": "./dist/calendar/index.d.ts",
|
||||
"import": "./dist/calendar/index.mjs",
|
||||
"require": "./dist/calendar/index.cjs"
|
||||
},
|
||||
"./datetime-picker": {
|
||||
"types": "./dist/datetime-picker/index.d.ts",
|
||||
"import": "./dist/datetime-picker/index.mjs",
|
||||
"require": "./dist/datetime-picker/index.cjs"
|
||||
},
|
||||
"./picker-single": {
|
||||
"types": "./dist/picker-single/index.d.ts",
|
||||
"import": "./dist/picker-single/index.mjs",
|
||||
"require": "./dist/picker-single/index.cjs"
|
||||
},
|
||||
"./picker": {
|
||||
"types": "./dist/picker/index.d.ts",
|
||||
"import": "./dist/picker/index.mjs",
|
||||
"require": "./dist/picker/index.cjs"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"vue3",
|
||||
|
|
@ -46,11 +68,12 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint --ext .js,ts --fix src",
|
||||
"release": "standard-version",
|
||||
"format": "prettier --write src",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"uview-plus": ">=3.0.0",
|
||||
|
|
@ -62,9 +85,11 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@jonny1994/qqmap-wx-jssdk": "^1.4.0",
|
||||
"lodash-es": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.14",
|
||||
"@types/lodash-es": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-dts": "catalog:",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ describe("useCalendar", () => {
|
|||
test("单选模式:text 显示 value,无值时显示 placeholder", () => {
|
||||
const scope = effectScope();
|
||||
scope.run(() => {
|
||||
const { value, text } = useCalendar({ mode: "single", placeholder: "请选日期" });
|
||||
const { value, text } = useCalendar({
|
||||
mode: "single",
|
||||
placeholder: "请选日期",
|
||||
});
|
||||
expect(text.value).toBe("请选日期");
|
||||
value.value = "2024-01-15";
|
||||
expect(text.value).toBe("2024-01-15");
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ describe("useDateTimePicker", () => {
|
|||
test("handleConfirm 关闭弹窗(不更新 value)", () => {
|
||||
const scope = effectScope();
|
||||
scope.run(() => {
|
||||
const { value, show, handleConfirm } = useDateTimePicker({ value: "2024-01-01" });
|
||||
const { value, show, handleConfirm } = useDateTimePicker({
|
||||
value: "2024-01-01",
|
||||
});
|
||||
show.value = true;
|
||||
const event = { indexs: [], value: [], values: [] } as any;
|
||||
handleConfirm(event);
|
||||
|
|
|
|||
|
|
@ -60,7 +60,11 @@ describe("usePicker", () => {
|
|||
test("indexes 含 null 时 text 显示 placeholder", async () => {
|
||||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const { text } = usePicker({ indexes: [null, 1], columns, placeholder: "请选择地区" });
|
||||
const { text } = usePicker({
|
||||
indexes: [null, 1],
|
||||
columns,
|
||||
placeholder: "请选择地区",
|
||||
});
|
||||
await nextTick();
|
||||
expect(text.value).toBe("请选择地区");
|
||||
});
|
||||
|
|
@ -71,7 +75,11 @@ describe("usePicker", () => {
|
|||
const scope = effectScope();
|
||||
await scope.run(async () => {
|
||||
const { indexes, value, handleConfirm } = usePicker({ columns });
|
||||
handleConfirm({ indexs: [1, 0], value: ["pb", "ca"], values: columns } as any);
|
||||
handleConfirm({
|
||||
indexs: [1, 0],
|
||||
value: ["pb", "ca"],
|
||||
values: columns,
|
||||
} as any);
|
||||
expect(indexes.value).toEqual([1, 0]);
|
||||
await nextTick();
|
||||
expect(value.value).toEqual(["pb", "ca"]);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@
|
|||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import * as QQMapWX from "@jonny1994/qqmap-wx-jssdk";
|
||||
export * from "@jonny1994/qqmap-wx-jssdk";
|
||||
|
||||
/**
|
||||
* 行政区划列表
|
||||
* @example
|
||||
* cidx: [103, 118]
|
||||
* fullname: "张家口市"
|
||||
* id: "130700"
|
||||
* location: {lat: 40.82444, lng: 114.88755}
|
||||
* name: "张家口"
|
||||
* pinyin: ["zhang", "jia", "kou"]
|
||||
*/
|
||||
export interface GetCityListSuccessResultResult {
|
||||
/**
|
||||
* 行政区划唯一标识
|
||||
* @example "110000"
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* 简称,如“内蒙古”
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* 全称,如“内蒙古自治区”
|
||||
* @example "北京市"
|
||||
*/
|
||||
fullname: string;
|
||||
/**
|
||||
* 中心点坐标
|
||||
* @example {lat: 39.90469, lng: 116.40717}
|
||||
*/
|
||||
location: QQMapWX.ResultLocation;
|
||||
/**
|
||||
* 行政区划拼音,每一下标为一个字的全拼,如:[“nei”,“meng”,“gu”]
|
||||
*/
|
||||
pinyin: string[];
|
||||
/**
|
||||
* 子级行政区划在下级数组中的下标位置
|
||||
* @example [0, 15]
|
||||
*/
|
||||
cidx?: number[];
|
||||
}
|
||||
|
||||
export interface GetCityListSuccessResult extends QQMapWX.CommonResult {
|
||||
/**
|
||||
* 结果数组,第0项,代表一级行政区划,第1项代表二级行政区划,以此类推;使用getchildren接口时,仅为指定父级行政区划的子级
|
||||
*/
|
||||
result: GetCityListSuccessResultResult[];
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
|
||||
|
||||
/// <reference types="uview-plus/types" />
|
||||
|
||||
declare namespace UViewPlus {
|
||||
type PickerColumns = never[][];
|
||||
type PickerValue<T extends PickerColumns = PickerColumns> = T[number][number][];
|
||||
type PickerValue<T extends PickerColumns = PickerColumns> =
|
||||
T[number][number][];
|
||||
|
||||
type PickerConfirmEvent<T extends PickerColumns = PickerColumns> = {
|
||||
indexs: number[];
|
||||
|
|
|
|||
|
|
@ -1,31 +1,48 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
|
||||
entry: {
|
||||
index: resolve(__dirname, "src/index.ts"),
|
||||
"calendar/index": resolve(__dirname, "src/calendar/index.ts"),
|
||||
"datetime-picker/index": resolve(
|
||||
__dirname,
|
||||
"src/datetime-picker/index.ts",
|
||||
),
|
||||
"picker-single/index": resolve(
|
||||
__dirname,
|
||||
"src/picker-single/index.ts",
|
||||
),
|
||||
"picker/index": resolve(__dirname, "src/picker/index.ts"),
|
||||
},
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format, entryName) =>
|
||||
`${entryName}.${format === "es" ? "mjs" : "cjs"}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'lodash-es'],
|
||||
external: ["vue", "lodash-es"],
|
||||
output: {
|
||||
chunkFileNames: "chunks/[name]-[hash].js",
|
||||
assetFileNames: "assets/[name]-[hash][extname]",
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
include: ['src', 'types'],
|
||||
outDir: 'dist',
|
||||
include: ["src", "types"],
|
||||
outDir: "dist",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# @r-utils/vue2
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 修复类型问题
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 添加工具
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# @r-utils/vue2
|
||||
|
||||
仅用于 Vue2 项目的工具包。
|
||||
|
||||
## 适用范围
|
||||
|
||||
- 适用于 Vue2 项目。
|
||||
- 适用于需要根据页面可见性触发组件 `onShow` / `onHide` 的场景。
|
||||
|
||||
## 不适用范围
|
||||
|
||||
- 不适用于 Vue3 项目。
|
||||
- 如果是 uni-app 专用能力,建议优先使用 `@r-utils/uni-app`。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @r-utils/vue2
|
||||
```
|
||||
|
||||
## 导入方式
|
||||
|
||||
### 推荐:根入口导入
|
||||
|
||||
大多数场景推荐从根入口导入,路径简单,使用心智负担更低。
|
||||
|
||||
```ts
|
||||
import Vue from "vue";
|
||||
import { VisibilityPlugin } from "@r-utils/vue2";
|
||||
|
||||
Vue.use(VisibilityPlugin);
|
||||
```
|
||||
|
||||
也可以使用默认导出:
|
||||
|
||||
```ts
|
||||
import Vue from "vue";
|
||||
import VisibilityPlugin from "@r-utils/vue2";
|
||||
|
||||
Vue.use(VisibilityPlugin);
|
||||
```
|
||||
|
||||
### 兼容:子路径导入
|
||||
|
||||
如果你希望模块边界更清晰,也可以使用子路径导入。两种方式都支持。
|
||||
|
||||
```ts
|
||||
import Vue from "vue";
|
||||
import VisibilityPlugin from "@r-utils/vue2/plugins/visibility";
|
||||
|
||||
Vue.use(VisibilityPlugin);
|
||||
```
|
||||
|
||||
## 当前能力
|
||||
|
||||
| 子路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `@r-utils/vue2/plugins/visibility` | 基于页面可见性变更触发 `onShow` / `onHide` |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```ts
|
||||
export default {
|
||||
onShow() {
|
||||
console.log("页面显示");
|
||||
},
|
||||
onHide() {
|
||||
console.log("页面隐藏");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
|
||||
- 本包依赖 Vue2,请确保业务项目已安装兼容版本的 `vue`。
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@r-utils/vue2",
|
||||
"version": "1.3.0",
|
||||
"version": "2.0.1",
|
||||
"private": false,
|
||||
"description": "Vue2工具库",
|
||||
"type": "module",
|
||||
|
|
@ -8,13 +8,20 @@
|
|||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./plugins/visibility": {
|
||||
"types": "./dist/plugins/visibility/index.d.ts",
|
||||
"import": "./dist/plugins/visibility/index.mjs",
|
||||
"require": "./dist/plugins/visibility/index.cjs"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"vue2"
|
||||
|
|
@ -43,8 +50,9 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint --ext .js,ts --fix src",
|
||||
"release": "standard-version",
|
||||
"format": "prettier --write src",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "vitest run"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import type Vue from "vue";
|
||||
import type { PluginObject, VueConstructor } from "vue";
|
||||
|
||||
declare module "vue/types/options" {
|
||||
interface ComponentOptions<V extends Vue> {
|
||||
/** 页面变为可见时触发的生命周期钩子 */
|
||||
onShow?: (this: V) => void;
|
||||
/** 页面变为隐藏时触发的生命周期钩子 */
|
||||
onHide?: (this: V) => void;
|
||||
}
|
||||
}
|
||||
|
||||
/** 安装页面可见性生命周期插件 */
|
||||
export declare function install(Vue: VueConstructor): void;
|
||||
|
||||
/** 页面可见性生命周期插件 */
|
||||
declare const VisibilityPlugin: PluginObject<unknown> & {
|
||||
/** 安装页面可见性生命周期插件 */
|
||||
install: typeof install;
|
||||
};
|
||||
|
||||
export default VisibilityPlugin;
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
/**
|
||||
* 安装页面可见性生命周期插件
|
||||
* @param {import("vue").VueConstructor} Vue Vue2 构造器
|
||||
*/
|
||||
function install(Vue) {
|
||||
Vue.mixin({
|
||||
created() {
|
||||
|
|
@ -8,14 +12,14 @@ function install(Vue) {
|
|||
this.visibilitychangeCallback();
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
this.visibilitychangeCallback
|
||||
this.visibilitychangeCallback,
|
||||
);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this.visibilitychangeCallback
|
||||
this.visibilitychangeCallback,
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -6,6 +6,6 @@
|
|||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,43 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
|
||||
entry: {
|
||||
index: resolve(__dirname, "src/index.ts"),
|
||||
"plugins/visibility/index": resolve(
|
||||
__dirname,
|
||||
"src/plugins/visibility/index.js",
|
||||
),
|
||||
},
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format, entryName) =>
|
||||
`${entryName}.${format === "es" ? "mjs" : "cjs"}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'lodash'],
|
||||
external: ["vue", "lodash"],
|
||||
output: {
|
||||
chunkFileNames: "chunks/[name]-[hash].js",
|
||||
assetFileNames: "assets/[name]-[hash][extname]",
|
||||
exports: "named",
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
include: ["src"],
|
||||
outDir: "dist",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# @r-utils/vue3
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @r-utils/common@2.0.0
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 添加工具
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @r-utils/common@1.4.0
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# @r-utils/vue3
|
||||
|
||||
仅用于 Vue3 项目的工具包,提供 class 处理、组件事件派发和常用组合式 Hooks。
|
||||
|
||||
## 适用范围
|
||||
|
||||
- 适用于 Vue3 项目。
|
||||
- 适用于需要复用 Vue3 class 处理、列表加载、表单输入值处理等能力的项目。
|
||||
|
||||
## 不适用范围
|
||||
|
||||
- 不适用于 Vue2 项目。
|
||||
- 如果是 uni-app 专用能力,建议优先使用 `@r-utils/uni-app`。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @r-utils/vue3
|
||||
```
|
||||
|
||||
## 导入方式
|
||||
|
||||
### 推荐:根入口导入
|
||||
|
||||
大多数场景推荐从根入口导入,路径简单,使用心智负担更低。
|
||||
|
||||
```ts
|
||||
import {
|
||||
mergeClass,
|
||||
dispatch,
|
||||
useLoadMore,
|
||||
useTimer,
|
||||
useValueOfRule,
|
||||
} from "@r-utils/vue3";
|
||||
```
|
||||
|
||||
### 兼容:子路径导入
|
||||
|
||||
如果你希望模块边界更清晰,也可以使用子路径导入。两种方式都支持。
|
||||
|
||||
```ts
|
||||
import { mergeClass } from "@r-utils/vue3/vue-helper";
|
||||
import { useLoadMore } from "@r-utils/vue3/hooks/list";
|
||||
import { useTimer, useValueOfRule } from "@r-utils/vue3/hooks/utils";
|
||||
```
|
||||
|
||||
## 导出模块
|
||||
|
||||
| 子路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `@r-utils/vue3/vue-helper` | Vue class 合并、转换和祖先组件事件派发 |
|
||||
| `@r-utils/vue3/hooks/list` | 列表分页加载 Hook |
|
||||
| `@r-utils/vue3/hooks/utils` | 常用组合式工具 Hook |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 合并 class
|
||||
|
||||
```ts
|
||||
import { mergeClass } from "@r-utils/vue3";
|
||||
|
||||
const cls = mergeClass("btn", ["btn-primary"], { active: true });
|
||||
```
|
||||
|
||||
### 列表加载
|
||||
|
||||
```ts
|
||||
import { useLoadMore } from "@r-utils/vue3";
|
||||
|
||||
const listState = useLoadMore(async (pageNum, pageSize) => {
|
||||
return {
|
||||
total: 100,
|
||||
list: await fetchList(pageNum, pageSize),
|
||||
};
|
||||
});
|
||||
|
||||
await listState.loadMore();
|
||||
```
|
||||
|
||||
### 子路径导入 Hook
|
||||
|
||||
```ts
|
||||
import { useTimer } from "@r-utils/vue3/hooks/utils";
|
||||
|
||||
const timer = useTimer(60);
|
||||
timer.start();
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
|
||||
- 本包依赖 Vue3,请确保业务项目已安装兼容版本的 `vue`。
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@r-utils/vue3",
|
||||
"version": "1.3.0",
|
||||
"version": "2.0.0",
|
||||
"private": false,
|
||||
"description": "Vue3 工具",
|
||||
"type": "module",
|
||||
|
|
@ -8,13 +8,30 @@
|
|||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./hooks/list": {
|
||||
"types": "./dist/hooks/list/index.d.ts",
|
||||
"import": "./dist/hooks/list/index.mjs",
|
||||
"require": "./dist/hooks/list/index.cjs"
|
||||
},
|
||||
"./hooks/utils": {
|
||||
"types": "./dist/hooks/utils/index.d.ts",
|
||||
"import": "./dist/hooks/utils/index.mjs",
|
||||
"require": "./dist/hooks/utils/index.cjs"
|
||||
},
|
||||
"./vue-helper": {
|
||||
"types": "./dist/vue-helper/index.d.ts",
|
||||
"import": "./dist/vue-helper/index.mjs",
|
||||
"require": "./dist/vue-helper/index.cjs"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"vue3"
|
||||
|
|
@ -42,8 +59,9 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint --ext .js,ts --fix src",
|
||||
"release": "standard-version",
|
||||
"format": "prettier --write src",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "vitest run"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
*/
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ import type { Ref } from "vue";
|
|||
*/
|
||||
export function useTimer(initTime = 0) {
|
||||
const time = ref(initTime);
|
||||
const timeInterval = ref();
|
||||
const timeInterval = ref<ReturnType<typeof setInterval>>();
|
||||
|
||||
const start = () => {
|
||||
timeInterval.value = setInterval(() => {
|
||||
|
|
@ -1 +1,3 @@
|
|||
export * from "./vue-helper"
|
||||
export * from "./vue-helper";
|
||||
export * from "./hooks/list";
|
||||
export * from "./hooks/utils";
|
||||
|
|
|
|||
|
|
@ -1,14 +1,32 @@
|
|||
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>
|
||||
customClass: Array<string>,
|
||||
): DistCustomClass;
|
||||
export function createCustomClassObj(
|
||||
customClass: string | Array<string>
|
||||
customClass: string | Array<string>,
|
||||
): DistCustomClass {
|
||||
let customClassObj = <DistCustomClass>{};
|
||||
if (typeof customClass === "string") {
|
||||
|
|
@ -26,7 +44,7 @@ export function createCustomClassObj(
|
|||
}
|
||||
} else {
|
||||
throw new TypeError(
|
||||
`customClass只能是字符串或数组类型,customClass: ${customClass}`
|
||||
`customClass只能是字符串或数组类型,customClass: ${customClass}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -34,9 +52,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>{};
|
||||
|
|
@ -57,7 +87,7 @@ export function convertCustomClass(sourceClass: CustomClass): CustomClassObj {
|
|||
customClassObj = sourceClass;
|
||||
} else {
|
||||
throw new TypeError(
|
||||
`sourceClass不是有效的vue class,sourceClass: ${sourceClass}`
|
||||
`sourceClass不是有效的vue class,sourceClass: ${sourceClass}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -65,46 +95,22 @@ 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
|
||||
.map((cc) => convertCustomClass(cc))
|
||||
.reduce((a, b) => Object.assign(a, b),{});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送祖先组件事件
|
||||
* @param thisArg 调用的源组件实例
|
||||
* @param componentName 需要触发事件的组件名
|
||||
* @param eventName 事件名称
|
||||
* @param params 参数
|
||||
* @returns
|
||||
*/
|
||||
export function dispatch(
|
||||
thisArg: ComponentPublicInstance,
|
||||
componentName: ComponentPublicInstance,
|
||||
eventName: string,
|
||||
params: unknown
|
||||
): void {
|
||||
let parent = thisArg.$parent || thisArg.$root;
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = parent.$options.componentName;
|
||||
|
||||
while (parent && (!name || name !== componentName)) {
|
||||
parent = parent.$parent;
|
||||
|
||||
if (parent) {
|
||||
name = parent.$options.componentName;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent.$emit(eventName, params);
|
||||
}
|
||||
.reduce((a, b) => Object.assign(a, b), {});
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* @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";
|
||||
|
||||
export * from "./class-helper";
|
||||
export * from "./style-helper";
|
||||
|
||||
/**
|
||||
* 向上遍历祖先组件,找到指定名称的组件并触发其事件
|
||||
*
|
||||
* 类似 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,
|
||||
componentName: ComponentPublicInstance,
|
||||
eventName: string,
|
||||
params: unknown,
|
||||
): void {
|
||||
let parent = thisArg.$parent || thisArg.$root;
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = parent.$options.componentName;
|
||||
|
||||
while (parent && (!name || name !== componentName)) {
|
||||
parent = parent.$parent;
|
||||
|
||||
if (parent) {
|
||||
name = parent.$options.componentName;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent.$emit(eventName, params);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import type { CSSProperties, StyleValue } from "vue";
|
||||
|
||||
/**
|
||||
* 将合法的 Vue 动态 style 值统一转换为 CSSProperties 对象,
|
||||
* 并可选择性合并覆盖样式。
|
||||
*
|
||||
* - value:要转换的 style 值(string / CSSProperties / Array<StyleValue>)
|
||||
* - overrides:可选,合并时优先级更高的 style 值
|
||||
*
|
||||
* 合并规则:
|
||||
* 1. 两个 style 分别 normalize 后,以 overrides 优先
|
||||
* 2. 如果 overrides 中某属性值为空(null / undefined / "" / false),则使用 value 的值
|
||||
* 3. 如果两者都为空,则过滤掉该属性
|
||||
*
|
||||
* 逻辑与 Vue 内部 normalizeStyle 一致,可直接用于组件 props 的 customStyle 转换。
|
||||
*/
|
||||
export function normalizeStyle(
|
||||
value: StyleValue,
|
||||
overrides?: StyleValue,
|
||||
): CSSProperties {
|
||||
const base = toCSSProperties(value);
|
||||
|
||||
if (overrides == null) return base;
|
||||
|
||||
const over = toCSSProperties(overrides);
|
||||
const result: Record<string, string | number> = {};
|
||||
const allKeys = new Set([
|
||||
...Object.keys(base as Record<string, unknown>),
|
||||
...Object.keys(over),
|
||||
]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const overVal = (over as Record<string, unknown>)[key];
|
||||
const baseVal = (base as Record<string, unknown>)[key];
|
||||
|
||||
if (!isEmptyStyleValue(overVal)) {
|
||||
// overrides 有值,优先使用
|
||||
result[key] = overVal as string | number;
|
||||
} else if (!isEmptyStyleValue(baseVal)) {
|
||||
// overrides 为空,base 有值,使用 base
|
||||
result[key] = baseVal as string | number;
|
||||
}
|
||||
// 两者都为空,跳过
|
||||
}
|
||||
|
||||
return result as CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断样式属性值是否为空(null / undefined / "" / false)。
|
||||
*/
|
||||
function isEmptyStyleValue(val: unknown): boolean {
|
||||
return val == null || val === "" || val === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 StyleValue 转为 CSSProperties,纯转换不做合并。
|
||||
*/
|
||||
function toCSSProperties(value: StyleValue): CSSProperties {
|
||||
if (Array.isArray(value)) {
|
||||
const res: CSSProperties = {};
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const item = value[i];
|
||||
const normalized =
|
||||
typeof item === "string"
|
||||
? parseStringStyle(item)
|
||||
: toCSSProperties(item as StyleValue);
|
||||
if (normalized) {
|
||||
const target = res as Record<string, string | number>;
|
||||
const source = normalized as Record<string, string | number>;
|
||||
for (const key in source) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return parseStringStyle(value);
|
||||
}
|
||||
|
||||
if (value != null && typeof value === "object") {
|
||||
return value as CSSProperties;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/** 分号分隔正则,排除括号内的分号 */
|
||||
const listDelimiterRE = /;(?![^(]*\))/;
|
||||
/** 属性值分隔正则 */
|
||||
const propertyDelimiterRE = /:([\s\S]+)/;
|
||||
/** CSS 注释正则 */
|
||||
const styleCommentRE = /\/\*[\s\S]*?\*\//g;
|
||||
|
||||
/**
|
||||
* 将 CSS 字符串解析为 CSSProperties 对象。
|
||||
* 逻辑与 Vue 内部 parseStringStyle 一致。
|
||||
*/
|
||||
function parseStringStyle(cssText: string): CSSProperties {
|
||||
const ret: Record<string, string> = {};
|
||||
cssText
|
||||
.replace(styleCommentRE, "")
|
||||
.split(listDelimiterRE)
|
||||
.forEach((item) => {
|
||||
if (item) {
|
||||
const tmp = item.split(propertyDelimiterRE);
|
||||
if (tmp.length > 1) {
|
||||
ret[tmp[0].trim()] = tmp[1].trim();
|
||||
}
|
||||
}
|
||||
});
|
||||
return ret as CSSProperties;
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { dispatch } from "../src/vue-helper";
|
||||
|
||||
/**
|
||||
* 创建模拟的 Vue ComponentPublicInstance
|
||||
*/
|
||||
function createMockInstance(
|
||||
componentName?: string,
|
||||
parent?: any,
|
||||
root?: any,
|
||||
) {
|
||||
const instance = {
|
||||
$parent: parent ?? null,
|
||||
$root: root ?? null,
|
||||
$options: { componentName: componentName ?? null },
|
||||
$emit: vi.fn(),
|
||||
};
|
||||
return instance;
|
||||
}
|
||||
|
||||
describe("dispatch", () => {
|
||||
it("应在祖先组件中找到匹配的 componentName 并触发事件", () => {
|
||||
const targetComponentName = "FormComponent";
|
||||
const grandParent = createMockInstance(targetComponentName);
|
||||
const parent = createMockInstance(undefined, grandParent, grandParent);
|
||||
const thisArg = createMockInstance(undefined, parent, grandParent);
|
||||
|
||||
// thisArg.$parent → parent, parent.$parent → grandParent
|
||||
parent.$parent = grandParent;
|
||||
|
||||
dispatch(thisArg, targetComponentName as any, "validate", { field: "name" });
|
||||
|
||||
expect(grandParent.$emit).toHaveBeenCalledWith("validate", { field: "name" });
|
||||
});
|
||||
|
||||
it("应从直接父组件开始查找", () => {
|
||||
const targetComponentName = "DirectParent";
|
||||
const parent = createMockInstance(targetComponentName);
|
||||
const thisArg = createMockInstance(undefined, parent, parent);
|
||||
|
||||
dispatch(thisArg, targetComponentName as any, "change", null);
|
||||
|
||||
expect(parent.$emit).toHaveBeenCalledWith("change", null);
|
||||
});
|
||||
|
||||
it("当没有父组件且有根组件时,应从根组件开始查找", () => {
|
||||
const targetComponentName = "RootComponent";
|
||||
const root = createMockInstance(targetComponentName);
|
||||
const thisArg = createMockInstance(undefined, null, root);
|
||||
|
||||
dispatch(thisArg, targetComponentName as any, "init", undefined);
|
||||
|
||||
expect(root.$emit).toHaveBeenCalledWith("init", undefined);
|
||||
});
|
||||
|
||||
it("当 thisArg.$parent 和 thisArg.$root 都为 null 时,应直接返回", () => {
|
||||
const thisArg = createMockInstance(undefined, null, null);
|
||||
const targetComponentName = "FormComponent" as any;
|
||||
|
||||
// 不应抛出错误
|
||||
expect(() => dispatch(thisArg, targetComponentName, "validate", {})).not.toThrow();
|
||||
});
|
||||
|
||||
it("当找不到匹配的祖先组件时,不应触发任何事件", () => {
|
||||
const grandParent = createMockInstance("OtherComponent");
|
||||
const parent = createMockInstance("AnotherComponent", grandParent, grandParent);
|
||||
const thisArg = createMockInstance(undefined, parent, grandParent);
|
||||
|
||||
dispatch(thisArg, "NonExistentComponent" as any, "validate", {});
|
||||
|
||||
expect(grandParent.$emit).not.toHaveBeenCalled();
|
||||
expect(parent.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("应在多级父组件链中找到匹配的祖先", () => {
|
||||
const targetComponentName = "GrandGrandParent";
|
||||
const grandGrandParent = createMockInstance(targetComponentName);
|
||||
const grandParent = createMockInstance("GrandParent", grandGrandParent, grandGrandParent);
|
||||
const parent = createMockInstance("Parent", grandParent, grandGrandParent);
|
||||
const thisArg = createMockInstance(undefined, parent, grandGrandParent);
|
||||
|
||||
dispatch(thisArg, targetComponentName as any, "submit", { data: "test" });
|
||||
|
||||
expect(grandGrandParent.$emit).toHaveBeenCalledWith("submit", { data: "test" });
|
||||
expect(grandParent.$emit).not.toHaveBeenCalled();
|
||||
expect(parent.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("找到的第一个匹配组件后,不应继续向上查找", () => {
|
||||
const targetComponentName = "Parent";
|
||||
const grandParent = createMockInstance(targetComponentName); // 更上层也有同名组件
|
||||
const parent = createMockInstance(targetComponentName, grandParent, grandParent);
|
||||
const thisArg = createMockInstance(undefined, parent, grandParent);
|
||||
|
||||
dispatch(thisArg, targetComponentName as any, "click", true);
|
||||
|
||||
// 应该触发 parent 的事件,而非 grandParent 的
|
||||
expect(parent.$emit).toHaveBeenCalledWith("click", true);
|
||||
expect(grandParent.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("应支持传递各种类型的参数", () => {
|
||||
const parent = createMockInstance("Target");
|
||||
const thisArg = createMockInstance(undefined, parent, parent);
|
||||
|
||||
// 字符串
|
||||
dispatch(thisArg, "Target" as any, "event1", "hello");
|
||||
expect(parent.$emit).toHaveBeenCalledWith("event1", "hello");
|
||||
|
||||
// 数字
|
||||
dispatch(thisArg, "Target" as any, "event2", 42);
|
||||
expect(parent.$emit).toHaveBeenCalledWith("event2", 42);
|
||||
|
||||
// 数组
|
||||
dispatch(thisArg, "Target" as any, "event3", [1, 2, 3]);
|
||||
expect(parent.$emit).toHaveBeenCalledWith("event3", [1, 2, 3]);
|
||||
|
||||
// undefined
|
||||
dispatch(thisArg, "Target" as any, "event4", undefined);
|
||||
expect(parent.$emit).toHaveBeenCalledWith("event4", undefined);
|
||||
});
|
||||
|
||||
it("当祖先组件的 componentName 为 undefined 时,应跳过继续查找", () => {
|
||||
const targetComponentName = "TargetComponent";
|
||||
const grandParent = createMockInstance(targetComponentName);
|
||||
// parent 没有 componentName
|
||||
const parent = createMockInstance(undefined, grandParent, grandParent);
|
||||
const thisArg = createMockInstance(undefined, parent, grandParent);
|
||||
|
||||
dispatch(thisArg, targetComponentName as any, "save", {});
|
||||
|
||||
expect(grandParent.$emit).toHaveBeenCalledWith("save", {});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeStyle } from "../src/vue-helper/style-helper";
|
||||
|
||||
describe("normalizeStyle", () => {
|
||||
// ========== 单参数:纯转换 ==========
|
||||
|
||||
// ---------- 空值 / falsy ----------
|
||||
it("应将 undefined 转为空对象", () => {
|
||||
expect(normalizeStyle(undefined as any)).toEqual({});
|
||||
});
|
||||
|
||||
it("应将 null 转为空对象", () => {
|
||||
expect(normalizeStyle(null as any)).toEqual({});
|
||||
});
|
||||
|
||||
it("应将 false 转为空对象", () => {
|
||||
expect(normalizeStyle(false as any)).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- CSSProperties 对象 ----------
|
||||
it("应直接返回 CSSProperties 对象", () => {
|
||||
const style = { color: "red", fontSize: "14px" };
|
||||
expect(normalizeStyle(style)).toEqual(style);
|
||||
});
|
||||
|
||||
it("应保留 CSSProperties 中的数字值", () => {
|
||||
const style = { zIndex: 10, opacity: 0.5 };
|
||||
expect(normalizeStyle(style)).toEqual({ zIndex: 10, opacity: 0.5 });
|
||||
});
|
||||
|
||||
it("应保留 CSS 变量", () => {
|
||||
const style = { "--custom-color": "red" } as any;
|
||||
expect(normalizeStyle(style)).toEqual({ "--custom-color": "red" });
|
||||
});
|
||||
|
||||
it("空对象应返回空对象", () => {
|
||||
expect(normalizeStyle({})).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 字符串 ----------
|
||||
it("应解析简单的 CSS 字符串", () => {
|
||||
expect(normalizeStyle("color: red")).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("应解析多条声明的 CSS 字符串", () => {
|
||||
expect(normalizeStyle("color: red; font-size: 14px")).toEqual({
|
||||
color: "red",
|
||||
"font-size": "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应解析带空格的 CSS 字符串", () => {
|
||||
expect(normalizeStyle(" color : red ; font-size : 14px ")).toEqual({
|
||||
color: "red",
|
||||
"font-size": "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应忽略空声明", () => {
|
||||
expect(normalizeStyle("color: red;; font-size:14px;")).toEqual({
|
||||
color: "red",
|
||||
"font-size": "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应忽略尾部分号后的空项", () => {
|
||||
expect(normalizeStyle("color: red;")).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("应去除 CSS 注释", () => {
|
||||
expect(normalizeStyle("color: red; /* 注释 */ font-size: 14px")).toEqual({
|
||||
color: "red",
|
||||
"font-size": "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应解析含 url() 的值(不拆分括号内的分号)", () => {
|
||||
expect(normalizeStyle("background: url(data:image/png;base64,abc)")).toEqual({
|
||||
background: "url(data:image/png;base64,abc)",
|
||||
});
|
||||
});
|
||||
|
||||
it("应解析含冒号的值(如 url 中的协议)", () => {
|
||||
expect(normalizeStyle("background: url(http://example.com/img.png)")).toEqual({
|
||||
background: "url(http://example.com/img.png)",
|
||||
});
|
||||
});
|
||||
|
||||
it("空字符串应返回空对象", () => {
|
||||
expect(normalizeStyle("")).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 数组 ----------
|
||||
it("应合并对象数组", () => {
|
||||
expect(normalizeStyle([{ color: "red" }, { fontSize: "14px" }])).toEqual({
|
||||
color: "red",
|
||||
fontSize: "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应合并字符串数组", () => {
|
||||
expect(normalizeStyle(["color: red", "font-size: 14px"])).toEqual({
|
||||
color: "red",
|
||||
"font-size": "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应合并混合类型数组", () => {
|
||||
expect(normalizeStyle(["color: red", { fontSize: "14px" }])).toEqual({
|
||||
color: "red",
|
||||
fontSize: "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("应处理嵌套数组", () => {
|
||||
expect(normalizeStyle([["color: red", { fontSize: "14px" }], { fontWeight: "bold" }])).toEqual({
|
||||
color: "red",
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
});
|
||||
});
|
||||
|
||||
it("应跳过数组中的 falsy 值", () => {
|
||||
expect(normalizeStyle([null, false, undefined, { color: "red" }] as any)).toEqual({
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
|
||||
it("数组中后面的属性应覆盖前面的", () => {
|
||||
expect(normalizeStyle([{ color: "red" }, { color: "blue" }])).toEqual({
|
||||
color: "blue",
|
||||
});
|
||||
});
|
||||
|
||||
it("空数组应返回空对象", () => {
|
||||
expect(normalizeStyle([])).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 实际使用场景 ----------
|
||||
it("应处理 AppButton 典型的 customStyle 用法", () => {
|
||||
// 字符串形式(旧用法兼容)
|
||||
expect(normalizeStyle("color: red; font-size: 28rpx")).toEqual({
|
||||
color: "red",
|
||||
"font-size": "28rpx",
|
||||
});
|
||||
// 对象形式(推荐用法)
|
||||
expect(normalizeStyle({ color: "red", fontSize: "28rpx" })).toEqual({
|
||||
color: "red",
|
||||
fontSize: "28rpx",
|
||||
});
|
||||
// 数组形式
|
||||
expect(normalizeStyle([{ color: "red" }, { fontSize: "28rpx" }])).toEqual({
|
||||
color: "red",
|
||||
fontSize: "28rpx",
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 双参数:合并覆盖 ==========
|
||||
|
||||
describe("合并覆盖(第二个参数)", () => {
|
||||
it("不传第二个参数时,行为与单参数一致", () => {
|
||||
expect(normalizeStyle({ color: "red" })).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("第二个参数为 undefined 时,行为与单参数一致", () => {
|
||||
expect(normalizeStyle({ color: "red" }, undefined)).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("第二个参数为 null 时,行为与单参数一致", () => {
|
||||
expect(normalizeStyle({ color: "red" }, null as any)).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("overrides 有值的属性应覆盖 base", () => {
|
||||
expect(normalizeStyle({ color: "red" }, { color: "blue" })).toEqual({ color: "blue" });
|
||||
});
|
||||
|
||||
it("overrides 新增的属性应合并进来", () => {
|
||||
expect(normalizeStyle({ color: "red" }, { fontSize: "14px" })).toEqual({
|
||||
color: "red",
|
||||
fontSize: "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides 和 base 各有不同的属性时,应全部合并", () => {
|
||||
expect(
|
||||
normalizeStyle({ color: "red", padding: "8px" }, { fontSize: "14px", margin: "4px" }),
|
||||
).toEqual({
|
||||
color: "red",
|
||||
padding: "8px",
|
||||
fontSize: "14px",
|
||||
margin: "4px",
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- overrides 为空值时 fallback 到 base ----------
|
||||
it("overrides 属性为 undefined 时应使用 base 的值", () => {
|
||||
expect(normalizeStyle({ color: "red" }, { color: undefined } as any)).toEqual({
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides 属性为 null 时应使用 base 的值", () => {
|
||||
expect(normalizeStyle({ color: "red" }, { color: null } as any)).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("overrides 属性为空字符串时应使用 base 的值", () => {
|
||||
expect(normalizeStyle({ color: "red" }, { color: "" })).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
it("overrides 属性为 false 时应使用 base 的值", () => {
|
||||
expect(normalizeStyle({ color: "red" }, { color: false } as any)).toEqual({ color: "red" });
|
||||
});
|
||||
|
||||
// ---------- 两者都为空时过滤属性 ----------
|
||||
it("base 和 overrides 都为空时,应过滤掉该属性", () => {
|
||||
expect(normalizeStyle({ color: "" }, { color: undefined } as any)).toEqual({});
|
||||
});
|
||||
|
||||
it("base 和 overrides 都为 null 时,应过滤掉该属性", () => {
|
||||
expect(normalizeStyle({ color: null } as any, { color: null } as any)).toEqual({});
|
||||
});
|
||||
|
||||
it("base 有值但属性为空、overrides 未指定该属性时,应过滤掉", () => {
|
||||
// base 有 color:"" (空值), overrides 没有 color → both empty → filtered
|
||||
expect(normalizeStyle({ color: "" }, {})).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 字符串形式的 overrides ----------
|
||||
it("overrides 为字符串时应正常解析并合并", () => {
|
||||
expect(normalizeStyle({ color: "red" }, "font-size: 14px")).toEqual({
|
||||
color: "red",
|
||||
"font-size": "14px",
|
||||
});
|
||||
});
|
||||
|
||||
it("base 为字符串、overrides 为对象时应正常合并", () => {
|
||||
// 字符串解析出 kebab-case 键,对象用 camelCase 键,两者独立
|
||||
expect(normalizeStyle("color: red", { fontSize: "14px" })).toEqual({
|
||||
color: "red",
|
||||
fontSize: "14px",
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- 数组形式的 overrides ----------
|
||||
it("overrides 为数组时应正常解析并合并", () => {
|
||||
expect(normalizeStyle({ color: "red" }, [{ fontSize: "14px" }])).toEqual({
|
||||
color: "red",
|
||||
fontSize: "14px",
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- 0 是有效值,不算空 ----------
|
||||
it("数字 0 应视为有效值,不当作空值", () => {
|
||||
expect(normalizeStyle({ opacity: 1 }, { opacity: 0 })).toEqual({ opacity: 0 });
|
||||
});
|
||||
|
||||
it("overrides 为 0 时不应 fallback 到 base", () => {
|
||||
expect(normalizeStyle({ zIndex: 10 }, { zIndex: 0 })).toEqual({ zIndex: 0 });
|
||||
});
|
||||
|
||||
// ---------- 综合场景 ----------
|
||||
it("应正确处理混合覆盖和 fallback", () => {
|
||||
const base = { color: "red", fontSize: "14px", padding: "8px", margin: "" };
|
||||
const overrides = { color: "blue", fontSize: undefined, padding: null, margin: "4px" } as any;
|
||||
expect(normalizeStyle(base, overrides)).toEqual({
|
||||
color: "blue", // overrides 有值,覆盖
|
||||
fontSize: "14px", // overrides 为 undefined,fallback 到 base
|
||||
padding: "8px", // overrides 为 null,fallback 到 base
|
||||
margin: "4px", // base 为空,overrides 有值,使用 overrides
|
||||
});
|
||||
});
|
||||
|
||||
it("appButton 场景:customStyle 覆盖内部样式", () => {
|
||||
// 模拟 AppButton 内部样式 + customStyle
|
||||
const internal = { width: "200rpx", backgroundColor: "#3adceb", color: "#111" };
|
||||
const custom = { width: "128rpx", backgroundColor: undefined, color: "" } as any;
|
||||
expect(normalizeStyle(internal, custom)).toEqual({
|
||||
width: "128rpx", // overrides 有值,覆盖
|
||||
backgroundColor: "#3adceb", // overrides 为 undefined,fallback
|
||||
color: "#111", // overrides 为空字符串,fallback
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createCustomClassObj,
|
||||
convertCustomClass,
|
||||
mergeClass,
|
||||
} from "../src/vue-helper";
|
||||
|
||||
// ========== createCustomClassObj ==========
|
||||
|
||||
describe("createCustomClassObj", () => {
|
||||
// ---------- 字符串输入 ----------
|
||||
it("应将单个类名字符串转为对象", () => {
|
||||
expect(createCustomClassObj("foo")).toEqual({ foo: true });
|
||||
});
|
||||
|
||||
it("应将空格分隔的类名字符串转为对象", () => {
|
||||
expect(createCustomClassObj("foo bar")).toEqual({ foo: true, bar: true });
|
||||
});
|
||||
|
||||
it("应处理前后有空格的字符串", () => {
|
||||
// trim 后 "foo bar",replace(" +"," ") 是字符串匹配不会替换连续空格
|
||||
// split(" ") 产生 ["foo", "", "bar"],空字符串作为键
|
||||
expect(createCustomClassObj(" foo bar ")).toEqual({ foo: true, "": true, bar: true });
|
||||
});
|
||||
|
||||
it("应处理多个连续空格", () => {
|
||||
// replace(" +", " ") 是字符串匹配而非正则,不会替换连续空格
|
||||
// split(" ") 产生 ["foo", "", "", "", "bar"],重复的空字符串键只保留最后一个
|
||||
expect(createCustomClassObj("foo bar")).toEqual({ foo: true, "": true, bar: true });
|
||||
});
|
||||
|
||||
it("空字符串应返回空对象", () => {
|
||||
expect(createCustomClassObj("")).toEqual({});
|
||||
});
|
||||
|
||||
it("仅空格的字符串应返回空对象", () => {
|
||||
expect(createCustomClassObj(" ")).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 数组输入 ----------
|
||||
it("应将类名数组转为对象", () => {
|
||||
expect(createCustomClassObj(["foo", "bar"])).toEqual({ foo: true, bar: true });
|
||||
});
|
||||
|
||||
it("应将单个元素的数组转为对象", () => {
|
||||
expect(createCustomClassObj(["foo"])).toEqual({ foo: true });
|
||||
});
|
||||
|
||||
it("空数组应返回空对象", () => {
|
||||
expect(createCustomClassObj([])).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 异常输入 ----------
|
||||
it("传入对象应抛出 TypeError", () => {
|
||||
expect(() => createCustomClassObj({} as any)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("传入数字应抛出 TypeError", () => {
|
||||
expect(() => createCustomClassObj(123 as any)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("传入 null 应抛出 TypeError", () => {
|
||||
expect(() => createCustomClassObj(null as any)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("传入 undefined 应抛出 TypeError", () => {
|
||||
expect(() => createCustomClassObj(undefined as any)).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== convertCustomClass ==========
|
||||
|
||||
describe("convertCustomClass", () => {
|
||||
// ---------- 字符串输入 ----------
|
||||
it("应将字符串转为类名对象", () => {
|
||||
expect(convertCustomClass("foo bar")).toEqual({ foo: true, bar: true });
|
||||
});
|
||||
|
||||
it("应将单个类名字符串转为对象", () => {
|
||||
expect(convertCustomClass("foo")).toEqual({ foo: true });
|
||||
});
|
||||
|
||||
it("空字符串应返回空对象", () => {
|
||||
expect(convertCustomClass("")).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 数组输入 ----------
|
||||
it("应将类名数组转为对象", () => {
|
||||
expect(convertCustomClass(["foo", "bar"])).toEqual({ foo: true, bar: true });
|
||||
});
|
||||
|
||||
it("空数组应返回空对象", () => {
|
||||
expect(convertCustomClass([])).toEqual({});
|
||||
});
|
||||
|
||||
// ---------- 对象输入 ----------
|
||||
it("应直接返回对象", () => {
|
||||
const obj = { foo: true, bar: false };
|
||||
expect(convertCustomClass(obj)).toEqual(obj);
|
||||
});
|
||||
|
||||
it("应保留对象中值为 false 的属性", () => {
|
||||
const obj = { foo: true, bar: false };
|
||||
const result = convertCustomClass(obj);
|
||||
expect(result).toHaveProperty("bar", false);
|
||||
});
|
||||
|
||||
// ---------- 异常输入 ----------
|
||||
it("传入数字应抛出 TypeError", () => {
|
||||
expect(() => convertCustomClass(123 as any)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("传入 null 应抛出 TypeError", () => {
|
||||
expect(() => convertCustomClass(null as any)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("传入 undefined 应抛出 TypeError", () => {
|
||||
expect(() => convertCustomClass(undefined as any)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("传入布尔值应抛出 TypeError", () => {
|
||||
expect(() => convertCustomClass(true as any)).toThrow(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== mergeClass ==========
|
||||
|
||||
describe("mergeClass", () => {
|
||||
it("应合并多个字符串类名", () => {
|
||||
expect(mergeClass("foo", "bar")).toEqual({ foo: true, bar: true });
|
||||
});
|
||||
|
||||
it("应合并字符串和数组", () => {
|
||||
expect(mergeClass("foo", ["bar"])).toEqual({ foo: true, bar: true });
|
||||
});
|
||||
|
||||
it("应合并字符串、数组和对象", () => {
|
||||
expect(mergeClass("foo", ["bar"], { baz: true })).toEqual({
|
||||
foo: true,
|
||||
bar: true,
|
||||
baz: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("后面的值应覆盖前面的同名属性", () => {
|
||||
expect(mergeClass({ foo: true }, { foo: false })).toEqual({ foo: false });
|
||||
});
|
||||
|
||||
it("字符串形式的类名应覆盖前面的", () => {
|
||||
// "foo" → { foo: true },后者覆盖前者
|
||||
expect(mergeClass("foo", "foo")).toEqual({ foo: true });
|
||||
});
|
||||
|
||||
it("单个参数应正常工作", () => {
|
||||
expect(mergeClass("foo")).toEqual({ foo: true });
|
||||
});
|
||||
|
||||
it("应处理空字符串参数", () => {
|
||||
expect(mergeClass("")).toEqual({});
|
||||
});
|
||||
|
||||
it("应处理空数组参数", () => {
|
||||
expect(mergeClass([])).toEqual({});
|
||||
});
|
||||
|
||||
it("应处理空对象参数", () => {
|
||||
expect(mergeClass({})).toEqual({});
|
||||
});
|
||||
|
||||
it("应合并多个对象类名", () => {
|
||||
expect(
|
||||
mergeClass({ color: true }, { size: true }, { weight: true }),
|
||||
).toEqual({ color: true, size: true, weight: true });
|
||||
});
|
||||
|
||||
it("实际使用场景:Vue 组件中合并 class", () => {
|
||||
// 模拟组件内部 class + 传入 class
|
||||
const internal = "btn primary";
|
||||
const custom = { active: true, disabled: false };
|
||||
expect(mergeClass(internal, custom)).toEqual({
|
||||
btn: true,
|
||||
primary: true,
|
||||
active: true,
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,6 +6,6 @@
|
|||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import * as QQMapWX from "@jonny1994/qqmap-wx-jssdk";
|
||||
|
||||
export * from "@jonny1994/qqmap-wx-jssdk";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,31 +1,41 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
|
||||
entry: {
|
||||
index: resolve(__dirname, "src/index.ts"),
|
||||
"hooks/list/index": resolve(__dirname, "src/hooks/list/index.ts"),
|
||||
"hooks/utils/index": resolve(__dirname, "src/hooks/utils/index.ts"),
|
||||
"vue-helper/index": resolve(__dirname, "src/vue-helper/index.ts"),
|
||||
},
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format, entryName) =>
|
||||
`${entryName}.${format === "es" ? "mjs" : "cjs"}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'lodash'],
|
||||
external: ["vue", "lodash"],
|
||||
output: {
|
||||
chunkFileNames: "chunks/[name]-[hash].js",
|
||||
assetFileNames: "assets/[name]-[hash][extname]",
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
include: ["src"],
|
||||
outDir: "dist",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
public-hoist-pattern[]=*dcloudio*
|
||||
shamefully-hoist=false
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "@r-utils/playground-uniapp",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev:custom": "uni -p",
|
||||
"dev:h5": "uni",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-jd": "uni -p mp-jd",
|
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||
"dev:mp-lark": "uni -p mp-lark",
|
||||
"dev:mp-qq": "uni -p mp-qq",
|
||||
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||
"dev:mp-harmony": "uni -p mp-harmony",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"dev:mp-xhs": "uni -p mp-xhs",
|
||||
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||
"build:custom": "uni build -p",
|
||||
"build:h5": "uni build",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-jd": "uni build -p mp-jd",
|
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||
"build:mp-lark": "uni build -p mp-lark",
|
||||
"build:mp-qq": "uni build -p mp-qq",
|
||||
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||
"build:mp-harmony": "uni build -p mp-harmony",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"build:mp-xhs": "uni build -p mp-xhs",
|
||||
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-components": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-harmony": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
|
||||
"@r-utils/uni-app": "workspace:^",
|
||||
"@r-utils/uview-plus": "workspace:^",
|
||||
"@r-utils/common": "workspace:^",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vue-router": "^4.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4080420251103001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4080420251103001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4080420251103001",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "5.2.8",
|
||||
"vue-tsc": "^1.0.24"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/// <reference types='@dcloudio/types' />
|
||||
import 'vue'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
|
||||
interface ComponentCustomOptions extends Hooks {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
|
||||
onLaunch(() => {
|
||||
console.log("App Launch");
|
||||
});
|
||||
onShow(() => {
|
||||
console.log("App Show");
|
||||
});
|
||||
onHide(() => {
|
||||
console.log("App Hide");
|
||||
});
|
||||
</script>
|
||||
<style></style>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name" : "",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue