Compare commits

...

10 Commits

181 changed files with 19631 additions and 17523 deletions

8
.changeset/README.md Normal file
View File

@ -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).

11
.changeset/config.json Normal file
View File

@ -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": []
}

View File

@ -11,8 +11,9 @@
"Bash(npm run build *)",
"Bash(npm view *)",
"Bash(npm list *)",
"Bash(pnpm install)",
"Bash(pnpm build)",
"Bash(pnpm install *)",
"Bash(pnpm add *)",
"Bash(pnpm build *)",
"Bash(pnpm run *)",
"Bash(pnpm test *)",
"Bash(pnpm run build *)",

View File

@ -1,26 +0,0 @@
node_modules/
.vscode/
.idea/
.husky/
Dockerfile
.hbuilderx/
.vite/
/dist/
/public/
/docs/
bin/
pnpm-lock.yaml
uni_modules/
*.md
libs/
src/libs/
src/static/
/patches/
wxcomponents/
lib/

View File

@ -1,40 +0,0 @@
// 配置参考https://eslint.bootcss.com/docs/user-guide/configuring
// import { defineConfig } from 'eslint-define-config';
const { defineConfig } = require("eslint-define-config");
module.exports = defineConfig({
root: true,
env: {
node: true,
browser: true,
},
globals: {},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
impliedStrict: true,
},
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
overrides: [
{
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
env: {
mocha: true,
jest: true,
},
},
],
});

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
node_modules
dist
coverage
.turbo
.changeset/pre.json
*.bak

View File

@ -1,3 +1,3 @@
{
"src/*.{js,jsx,ts,tsx,vue}": "npm run lint"
"packages/*/src/**/*.{js,jsx,ts,tsx,vue}": "eslint --fix"
}

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
registry=http://npm.nps.yunvip123.cn
engine-strict=true
shamefully-hoist=true

2
.serena/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/cache
/project.local.yml

151
.serena/project.yml Normal file
View File

@ -0,0 +1,151 @@
# 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
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# 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
# 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
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# 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,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []

View File

@ -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" }
]
}

View File

@ -1,8 +1,10 @@
{
"recommendations": [
"psioniq.psi-header",
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint"
]
"recommendations": [
"psioniq.psi-header",
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig"
]
}

21
.vscode/settings.json vendored
View File

@ -7,9 +7,9 @@
"psi-header.changes-tracking": {
"isActive": true,
"autoHeader": "autoSave",
"modDate": "@lastModified:",
"modDate": "@lastModified",
"modDateFormat": "YYYY-MM-DD HH:mm:ss",
"modAuthor": "@lastModifiedBy:",
"modAuthor": "@lastModifiedBy",
"exclude": ["markdown", "json"],
"excludeGlob": ["**/settings.json"]
},
@ -17,14 +17,17 @@
{
"language": "*",
"template": [
"@file: <<filerelativepath>>",
"@description: ",
"@author: <<author>> (<<authoremail>>)",
"@date: <<filecreated('YYYY-MM-DD HH:mm:ss')>>",
"@lastModified: <<dateformat('YYYY-MM-DD HH:mm:ss')>>",
"@lastModifiedBy: <<author>> (<<authoremail>>)"
"@file <<filerelativepath>>",
"@description ",
"@author <<author>> (<<authoremail>>)",
"@date <<filecreated('YYYY-MM-DD HH:mm:ss')>>",
"@lastModified <<dateformat('YYYY-MM-DD HH:mm:ss')>>",
"@lastModifiedBy <<author>> (<<authoremail>>)"
],
"changeLogNaturalOrder": true
}
]
],
"workbench.editor.limit.value": 3,
"cSpell.words": ["Codice", "commitlint", "dcloudio", "Fabbrica"],
"js/ts.tsdk.path": "node_modules\\typescript\\lib"
}

128
AGENTS.md Normal file
View File

@ -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` 脚本使用 webpackrollup 配置备用。
### 测试
```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 继承自 JpPrinteresc.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` 生命周期钩子

View File

@ -1,6 +1,13 @@
# 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)
### Features
* **common:** :sparkles: 添加工具 ([b10d613](https://gitee.com/codice_fabbrica/r-util-js/commit/b10d6139877a1d9f7bb4483b4c2cd442bbc695a5))
### [1.2.1](https://gitee.com/codice_fabbrica/r-util-js/compare/v1.2.0...v1.2.1) (2026-03-26)

View File

@ -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`

View File

@ -1,6 +1,6 @@
# r-utils
前端工具库 monorepo使用 pnpm workspace 管理Vite 构建。
前端工具库 monorepo使用 pnpm workspace + Turbo 管理任务Vite 构建。
## 包列表
@ -8,13 +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 HooksPicker、Calendar 等) |
| `@r-utils/vue2` | `packages/vue2` | Vue2 工具visibility 插件) |
## 环境要求
- Node.js >= 18.12.0
- pnpm >= 8.15.6
- pnpm >= 10.0.0
## 安装依赖
@ -30,6 +31,14 @@ pnpm install
pnpm build
```
Turbo 会自动按依赖拓扑排序构建(例如 `@r-utils/common` 会先于依赖它的包构建),并复用缓存加速重复任务。
### 构建单个包Turbo 过滤)
```bash
pnpm build -- --filter=@r-utils/common
```
### 构建单个包
```bash
@ -51,8 +60,11 @@ dist/
### 监听模式
```bash
cd packages/common
pnpm watch
# 监听所有包
pnpm build:watch
# 仅监听单个包
pnpm build:watch -- --filter=@r-utils/common
```
## 使用方式
@ -74,6 +86,12 @@ 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';
// Vue2 插件
import { VisibilityPlugin } from '@r-utils/vue2';
Vue.use(VisibilityPlugin);
@ -110,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
```
### 代码检查与格式化
@ -132,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
@ -189,6 +184,7 @@ echo "registry=http://npm.nps.yunvip123.cn" > .npmrc
pnpm add @r-utils/common
pnpm add @r-utils/vue3
pnpm add @r-utils/uni-app
pnpm add @r-utils/uview-plus
pnpm add @r-utils/vue2
```
@ -208,6 +204,7 @@ r-util-js/
│ │ └── tsconfig.json
│ ├── vue3/ # @r-utils/vue3
│ ├── uni-app/ # @r-utils/uni-app
│ ├── uview-plus/ # @r-utils/uview-plus
│ └── vue2/ # @r-utils/vue2
├── eslint.config.js # ESLint v9 flat config
├── prettier.config.js

View File

@ -1,6 +0,0 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
};

View File

@ -33,12 +33,8 @@ module.exports = {
"revert",
],
],
// scope 可选,但如果提供必须是以下之一
"scope-enum": [
2,
"always",
["common", "vue3", "vue2", "uni-app", "deps", "release"],
],
// scope 可自定义(保留 prompt 的推荐列表,不做白名单限制)
"scope-enum": [0],
// subject 不能为空
"subject-empty": [2, "never"],
// subject 不能以句号结尾
@ -63,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 包" },
@ -73,43 +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: false,
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" },
],
},
};

View File

@ -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
```
## 需要帮助?

65
docs/RELEASE.md Normal file
View File

@ -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。

View File

@ -1,9 +1,19 @@
import js from "@eslint/js";
import ts from "typescript-eslint";
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,
// 基础配置
{
files: ["**/*.{js,mjs,cjs,ts,vue}"],
@ -29,6 +39,7 @@ export default [
"lib/",
"scripts/",
],
plugins: { prettier: prettierPlugin, "import-x": importPlugin },
languageOptions: {
globals: {
...globals.node,
@ -36,23 +47,74 @@ 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": "warn",
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// Import 规则
"import-x/no-self-import": "error",
"import-x/no-cycle": "error",
"import-x/no-useless-path-segments": "error",
"import-x/no-extraneous-dependencies": "error",
"import-x/no-mutable-exports": "error",
"import-x/no-named-as-default": "error",
"import-x/no-named-as-default-member": "error",
"import-x/no-duplicates": "warn",
"import-x/no-deprecated": "warn",
"import-x/newline-after-import": "warn",
"import-x/first": "warn",
"import-x/order": [
"warn",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type",
],
alphabetize: {
order: "asc",
},
},
],
},
},
// 应用推荐规则
js.configs.recommended,
...ts.configs.recommended,
prettier,
// 测试文件覆盖配置
{
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
@ -63,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",
},
},
];

View File

@ -1,198 +0,0 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\Administrator\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
transform: {
'\\.[jt]sx?$': 'babel-jest',
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: [
"\\\\node_modules\\\\",
"\\.pnp\\.[^\\\\]+$"
],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
extensionsToTreatAsEsm: ['.ts']
};

10730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@r-utils/root",
"version": "1.2.1",
"version": "1.3.0",
"private": true,
"description": "前端工具库",
"keywords": [
@ -37,25 +37,36 @@
],
"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": "jest"
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-typescript": "^7.28.5",
"@changesets/cli": "^2.31.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.3.0",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"babel-jest": "^30.3.0",
"@vitest/coverage-v8": "catalog:",
"commitizen": "^4.3.1",
"cz-git": "^1.12.0",
"eslint": "^10.0.3",
@ -64,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",
"jest": "^30.3.0",
"lint-staged": "^16.4.0",
"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",

View File

@ -0,0 +1,13 @@
# @r-utils/common
## 2.0.0
### Major Changes
- 添加工具
## 1.4.0
### Minor Changes
- 添加工具

184
packages/common/README.md Normal file
View File

@ -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`,更适合浏览器或类浏览器环境。
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
- 打印相关模块通常依赖具体设备和业务场景,建议在真实设备环境中验证。

View File

@ -1,3 +0,0 @@
import rootConfig from "../../eslint.config.js";
export default [...rootConfig];

View File

@ -1,6 +1,6 @@
{
"name": "@r-utils/common",
"version": "1.2.1",
"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,23 +81,24 @@
"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": "jest"
"test": "vitest run"
},
"dependencies": {
"dayjs": "^1.11.13",
"js-easing-functions": "^1.0.3",
"lodash-es": "^4.17.21",
"lodash-es": "catalog:",
"text-encoding": "^0.7.0"
},
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/lodash-es": "^4.17.12",
"@types/lodash-es": "catalog:",
"vite": "catalog:",
"vite-plugin-dts": "catalog:"
"vite-plugin-dts": "catalog:",
"vitest": "catalog:"
}
}

View File

@ -4,3 +4,4 @@ export * from "./printer";
export * from "./time";
export * from "./timer";
export * from "./input-rule";
export * from "./ui";

View File

@ -9,19 +9,32 @@
export type InputValue = string | number | null;
export interface InputOptions {
min?: number; // 最小值
max?: number; // 最大值
init?: string | number; // 初始值
digits?: number; // 小数位数
pattern?: string | RegExp; // 匹配模式
required?: boolean; // 是否必填
integer?: boolean; // 是否整数
number?: boolean; // 是否转成数字
text?: boolean; // 是否使用文本模式
floor?: boolean; // 是否向下取数
ceil?: boolean; // 是否向上取数
keepDecimal?: boolean; // 是否保留小数即使是小数位是0
noSign?: boolean; // 不允许输入正负号
/** 最小值 */
min?: number;
/** 最大值 */
max?: number;
/** 初始值,当输入非法且 required 为 true 时回退到此值 */
init?: string | number;
/** 小数位数,设置后会对结果进行精度处理 */
digits?: number;
/** 匹配模式,文本模式下用于校验输入是否匹配该正则 */
pattern?: string | RegExp;
/** 是否必填,为 true 时非法输入将回退到 init 或 min */
required?: boolean;
/** 是否取整,为 true 时结果将取整 */
integer?: boolean;
/** 是否将结果转为数字类型 */
number?: boolean;
/** 是否使用文本模式,文本模式只做 pattern 校验和必填回退 */
text?: boolean;
/** 是否向下取整 */
floor?: boolean;
/** 是否向上取整 */
ceil?: boolean;
/** 是否保留小数位,为 true 时即使小数位为 0 也会保留指定位数(需配合 digits 使用) */
keepDecimal?: boolean;
/** 是否不允许输入正负号 */
noSign?: boolean;
}
/**
@ -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 ?? "");

View File

@ -1,31 +1,80 @@
/*
* @file \src\knock-test\KnockTest.ts
* @description
* @author tsl (randy1924@163.com)
* @date 2026-02-09 18:26:16
* @lastModified 2026-06-30 10:05:44
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { debounce } from "lodash-es";
/** 操作项,定义一次敲击阶段的参数 */
export class Operation {
/** 敲击有效时长ms超过该时长未达到指定次数则重置 */
duration = 1000;
/** 当前操作完成后进入下一操作前的等待时长ms等待期间点击会重置 */
delay = 1000;
/** 当前操作需要敲击的次数 */
times = 1;
}
/** KnockTest 配置项 */
export class Config {
/** 最大等待时间ms从首次敲击开始计算超时未完成所有操作则自动重置 */
maxWaitTime = 5000;
/** 操作项列表,按顺序执行 */
operations: Operation[] = [];
}
/**
*
*
*
*
*
* - idle knocking
* - knocking wait
* - wait idle
*
* @example
* ```ts
* const kt = new KnockTest({
* maxWaitTime: 10000,
* operations: [
* { duration: 1000, delay: 500, times: 3 }, // 1秒内敲3次
* { duration: 1000, delay: 500, times: 2 }, // 然后等待0.5秒后1秒内敲2次
* ]
* })
* kt.addCallback(() => console.log('解锁成功'))
* kt.knock() // 在UI点击事件中调用
* ```
*/
export class KnockTest {
/** 配置项 */
config: Config;
/** 当前操作项索引 */
index = 0;
/** 当前阶段已敲击次数 */
times = 0;
/**
* "idle"times从0开始计数duration期
* duration期间"knocking"times达到Operation的指定次数后进入delay期
* delay期间"wait"
*
* - "idle" times从0开始计数duration期
* - "knocking" times达到Operation的指定次数后进入delay期
* - "wait"
*/
status: "idle" | "knocking" | "wait" = "idle";
/** duration 定时器 ID */
durationTimerId: number | null = null;
/** delay 定时器 ID */
delayTimerId: number | null = null;
/** 成功回调列表,所有操作完成后依次执行 */
callbackList: (() => void)[] = [];
/** 最大等待防抖函数,超时自动重置 */
maxWaitFun: () => void;
/**
* @param config 使
*/
constructor(config: Config) {
const c = new Config();
this.config = Object.assign(c, config);
@ -36,10 +85,22 @@ export class KnockTest {
}, this.config.maxWaitTime);
}
/**
*
* @param cb
*/
addCallback(cb: () => void) {
this.callbackList.push(cb);
}
/**
* UI
*
* 使 times + 1
* - idle
* - knocking
* - wait
*/
knock() {
console.log("knock");
this.maxWaitFun();
@ -60,6 +121,7 @@ export class KnockTest {
}
}
/** 重置所有状态,回到初始空闲状态并清除所有定时器 */
private reset() {
this.status = "idle";
this.times = 0;
@ -75,6 +137,11 @@ export class KnockTest {
}
}
/**
* duration
*
* @throws
*/
private handleIdle() {
if (this.config.operations.length === 0) {
throw new Error("至少添加一个操作项");
@ -92,10 +159,17 @@ export class KnockTest {
this.checkKnock();
}
/**
*
*/
private handleKnocking() {
this.checkKnock();
}
/**
*
*
*/
private checkKnock() {
const operation = this.config.operations[this.index];
@ -127,6 +201,9 @@ export class KnockTest {
}
}
/**
*
*/
private handleWait() {
if (this.delayTimerId != null) {
clearTimeout(this.delayTimerId);

View File

@ -1,17 +1,54 @@
/*
* @file \src\permission\Permission.ts
* @description
* @author tsl (randy1924@163.com)
* @date 2023-11-27 13:35:18
* @lastModified 2026-06-30 10:06:50
* @lastModifiedBy tsl (randy1924@163.com)
*/
/**
*
*
*
* "module:action:resource"使 ":" 3
*
* @example
* ```ts
* const permission = new Permission(['user:read:info', 'admin:write:config'], 3, ':')
* permission.isValid('user:read:info') // true
* permission.isValid('user:read') // false级数不足
* permission.isValid('user read info') // false分隔符不匹配
* ```
*/
export class Permission {
/**
*
*/
/** 权限字符串列表 */
list: string[];
/** 权限分隔符,默认为 ":" */
separator: string;
/** 权限级数,默认为 3如 "module:action:resource" */
level: number;
/**
* @param list
* @param level 3
* @param separator ":"
*/
constructor(list: string[], level = 3, separator = ":") {
this.list = list;
this.level = level;
this.separator = separator;
}
/**
*
*
*
* level=3separator=":" "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+?$

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -3,20 +3,47 @@ import { Duration } from "dayjs/plugin/duration";
type TimerCallback = (time: string) => void;
/**
*
*
* dayjs duration
* stepEventListener
* countdownEventListener
*
* @example
* ```ts
* const countdown = new Countdown(60000) // 60秒倒计时
* countdown.format = 'mm:ss'
* countdown.addStepEventListener((time) => console.log('剩余:', time))
* countdown.addCountdownEventListener((time) => console.log('倒计时结束:', time))
* countdown.start()
* ```
*/
export default class Countdown {
/** 定时器 ID-1 表示未运行 */
id = -1;
/** dayjs duration 对象,表示剩余时长 */
duration: Duration;
/** 输出时间格式,默认 "HH:mm:ss" */
format = "HH:mm:ss";
/**
*
*
*
*
*/
stepEventListenerList = <TimerCallback[]>[];
/**
*
*
*
*
*/
countdownEventListenerList = <TimerCallback[]>[];
/** 初始时间(毫秒),用于 restart 时重置 */
initialTime = 0;
/**
* @param time
*/
constructor(time: number) {
this.initialTime = time;
this.duration = dayjs.duration(time);
@ -24,8 +51,11 @@ export default class Countdown {
/**
*
* @param cb
* @returns
*
*
*
* @param cb
* @returns cb TypeError
*/
addStepEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@ -35,9 +65,12 @@ export default class Countdown {
}
/**
*
* @param cb
* @returns
*
*
*
*
* @param cb
* @returns cb TypeError
*/
addCountdownEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@ -47,7 +80,7 @@ export default class Countdown {
}
/**
*
*
*/
start() {
this.id = window.setInterval(() => {
@ -63,7 +96,7 @@ export default class Countdown {
}
/**
*
*
*/
stop() {
console.log("stop", this.id);
@ -75,7 +108,7 @@ export default class Countdown {
}
/**
*
*
*/
restart() {
clearInterval(this.id);

View File

@ -1,19 +1,49 @@
type TimerCallback = (time: number) => void;
/**
*
*
* startTime endTime
* interval endTime
*
* Countdown TimeoutTimer 使 dayjs duration
*
*
* @example
* ```ts
* // 实例方式
* const timer = new TimeoutTimer(10, 0, 1000)
* timer.addStepEventListener((time) => console.log('剩余:', time))
* timer.addCountdownEventListener((time) => console.log('结束:', time))
* timer.start()
*
* // 静态工厂方式
* const timer2 = TimeoutTimer.start(10, 0, 1000,
* (time) => console.log('剩余:', time),
* (time) => console.log('结束:', time),
* )
* ```
*/
export default class TimeoutTimer {
/** 起始时间(数值) */
startTime = 0;
/** 结束时间(数值),倒计时到此值停止 */
endTime = 0;
/** 递减间隔(毫秒) */
interval = 0;
/** 当前剩余时间 */
time = 0;
/** 定时器 ID-1 表示未运行 */
id = -1;
/** 步骤监听器列表,每次递减时触发,参数为当前剩余时间 */
stepEventListenerList = <TimerCallback[]>[];
/** 完成监听器列表,倒计时结束时触发,参数为结束时间 */
countdownEventListenerList = <TimerCallback[]>[];
/**
*
* @param startTime
* @param endTime
* @param interval (ms)
* @param startTime
* @param endTime 0
* @param interval 1000
*/
constructor(startTime = 5, endTime = 0, interval = 1000) {
this.startTime = startTime;
@ -22,9 +52,12 @@ export default class TimeoutTimer {
}
/**
*
* @param cb
* @returns
*
*
*
*
* @param cb
* @returns cb TypeError
*/
addStepEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@ -34,9 +67,12 @@ export default class TimeoutTimer {
}
/**
*
* @param cb
* @returns
*
*
* endTime
*
* @param cb
* @returns cb TypeError
*/
addCountdownEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
@ -46,13 +82,14 @@ export default class TimeoutTimer {
}
/**
*
* @param startTime
* @param endTime
* @param interval
* @param stepEventListener
* @param countdownEventListener
* @returns
*
*
* @param startTime
* @param endTime 0
* @param interval 1000
* @param stepEventListener
* @param countdownEventListener
* @returns TimeoutTimer start
*/
static start(
startTime: number,
@ -73,7 +110,7 @@ export default class TimeoutTimer {
}
/**
*
* startTime endTime
*/
start() {
this.time = this.startTime;
@ -88,7 +125,7 @@ export default class TimeoutTimer {
}
/**
*
*
*/
stop() {
console.log("stop", this.id);

View File

@ -0,0 +1,47 @@
/*
* @file: /src/ui/function.ts
* @description:
* @author: tsl (randy1924@163.com)
* @date: 2026-03-19 11:50:59
* @lastModified: 2026-03-19 15:51:48
* @lastModifiedBy: tsl (randy1924@163.com)
*/
import { easeOutSine } from "js-easing-functions";
/**
* 使
* @param start px
* @param end px
* @param duration ms
* @param cb px
*/
export async function slowlyScroll(
start: number,
end: number,
duration: number,
cb?: (value: number) => void,
): Promise<void> {
return new Promise<void>((resolve) => {
const startTime = Date.now();
const distance = end - start;
const intervalId = setInterval(() => {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
// 使用 easeOutSine 缓动函数
// 参数:已过去时间、初始值、改变量、总持续时间
const currentValue = easeOutSine(elapsed, start, distance, duration);
cb?.(currentValue);
if (elapsed >= duration) {
// 清除定时器
clearInterval(intervalId);
// 确保最后一次调用使用准确的结束值
cb?.(end);
resolve();
}
}, 16); // 约60fps
});
}

View File

@ -0,0 +1,10 @@
/*
* @file: /src/ui/index.ts
* @description:
* @author: tsl (randy1924@163.com)
* @date: 2026-03-19 15:32:51
* @lastModified: 2026-03-19 15:46:00
* @lastModifiedBy: tsl (randy1924@163.com)
*/
export * from "./function";

View File

@ -1,4 +1,4 @@
import { test, expect, describe } from "@jest/globals";
import { test, expect, describe } from "vitest";
import { getValueOfRule, InputOptions } from "../src/input-rule";
describe("getValueOfRule - 文本模式 (text: true)", () => {
@ -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",
);
});
});

View File

@ -1,4 +1,4 @@
import { test, expect } from "@jest/globals";
import { test, expect } from "vitest";
import { Permission } from "../src/permission";
test("测试 Permission#isValid()", () => {

View File

@ -1,12 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "types/**/*.d.ts"],
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"outDir": "./dist"
}
}
}

338
packages/common/types/printer/esc.d.ts vendored Normal file
View File

@ -0,0 +1,338 @@
export function row2col(arr: any): any;
/**
* NOTE 3216
*
*/
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;
}

View File

@ -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";

369
packages/common/types/printer/tsc.d.ts vendored Normal file
View File

@ -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 1n9999dot
* @returns {TSC}
*/
setFeed(n: number | string): TSC;
/**
*
* 200 DPI:1 mm = 8 dots
* 300 DPI:1 mm = 12 dots
* @param {number|string} n 1n9999dot
* @returns {TSC}
*/
setBackFeed(n: number | string): TSC;
/**
*
* 200 DPI:1 mm = 8 dots
* 300 DPI:1 mm = 12 dots
* @param {number|string} n 1n9999dot
* @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;
}

View File

@ -1,31 +1,59 @@
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
const __dirname = fileURLToPath(new URL('.', import.meta.url));
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: {
external: ['dayjs', 'lodash-es', 'text-encoding', 'tslib'],
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,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
"@": resolve(__dirname, "src"),
},
},
plugins: [
dts({
include: ['src'],
outDir: 'dist',
include: ["src", "types"],
outDir: "dist",
}),
],
});

View File

@ -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

106
packages/uni-app/README.md Normal file
View File

@ -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、蓝牙、打印功能通常依赖具体平台和设备能力建议按目标端单独测试。

View File

@ -1,6 +1,6 @@
{
"name": "@r-utils/uni-app",
"version": "1.2.1",
"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,17 +80,19 @@
"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"
},
"dependencies": {
"@jonny1994/qqmap-wx-jssdk": "^1.4.0",
"@r-utils/common": "workspace:^",
"chroma-js": "^3.2.0",
"lodash-es": "^4.17.21",
"qs": "^6.15.0"
"chroma-js": "catalog:",
"lodash-es": "catalog:",
"qs": "catalog:"
},
"peerDependencies": {
"@dcloudio/uni-app": ">=3.0.0",
@ -67,9 +106,9 @@
"devDependencies": {
"@dcloudio/types": "^3.0.7",
"@dcloudio/uni-app": "3.0.0-4070520250711001",
"@types/chroma-js": "^3.1.2",
"@types/lodash-es": "^4.17.12",
"@types/qs": "^6.15.0",
"@types/chroma-js": "catalog:",
"@types/lodash-es": "catalog:",
"@types/qs": "catalog:",
"vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vue": "^3.3.0"

View File

@ -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);
// },
});
// });
}

View File

@ -4,3 +4,4 @@ export * from "./printer";
export * from "./request";
export * from "./uni-helper";
export * from "./upload";
export * from "./vue3";

View File

@ -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);
});
}
}

View File

@ -27,7 +27,7 @@ export async function isNfcEnabled() {
}
}
export function getNFCUtil(){
export function getNFCUtil() {
if (isAndroidApp) {
return new AndroidNfcUtil();
} else if (isAndroidWeixin) {

View File

@ -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);
});
}
}

View File

@ -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;

View File

@ -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 });
}
}

View File

@ -24,7 +24,6 @@ export function getCurrentPage() {
return pages[pages.length - 1];
}
/**
* 使 TypeScript toPromise
*
@ -39,10 +38,17 @@ export function getCurrentPage() {
// type ExtractFirstParameter<T> = Parameters<T>[0]; // 这样也可以但需要T是函数类型
// 方法2使用条件类型实现更灵活等价于 Parameters<T>[0]
type ExtractFirstParameter<T> = T extends (arg: infer P, ...args: unknown[]) => unknown ? P : never;
type ExtractFirstParameter<T> = T extends (
arg: infer P,
...args: unknown[]
) => unknown
? P
: never;
// 提取 success 回调的参数类型(类似 ReturnType 的思路,但提取回调参数)
type ExtractSuccessResult<T> = T extends { success?: (result: infer R) => void } ? R : never;
type ExtractSuccessResult<T> = T extends { success?: (result: infer R) => void }
? R
: never;
// 提取函数参数类型并排除回调函数用于args参数
type ExtractOptions<T> = T extends (options: infer P) => unknown
@ -103,7 +109,6 @@ export function toPromise<F extends (options: Record<string, unknown>) => void>(
});
}
export function showToast(
title: string,
icon: "success" | "loading" | "error" | "none" = "none",
@ -139,12 +144,9 @@ export async function getCanvasImageData(
) {
const context = uni.createCanvasContext(canvasId, thisArg);
const imgRes = await toPromise(
uni.downloadFile,
{
url,
},
);
const imgRes = await toPromise(uni.downloadFile, {
url,
});
console.log(imgRes);
context.drawImage(imgRes.tempFilePath, x, y, width, height);
@ -243,3 +245,45 @@ export function checkAndroidPermission(permissionList: string[]) {
});
// #endif
}
/**
*
*/
export async function calculateDistanceBySelectorAsync(
scrollViewSelector: string,
selector: string,
offsetTop = 0,
): Promise<number> {
const scrollViewScrollOffsetPromise = new Promise<UniApp.NodeInfo>(
(resolve) => {
const query = uni.createSelectorQuery();
query.select(scrollViewSelector).scrollOffset((res) => {
resolve(res as UniApp.NodeInfo);
});
query.exec();
},
);
const targetElementPromise = new Promise<UniApp.NodeInfo>((resolve) => {
const query = uni.createSelectorQuery();
query.select(selector).boundingClientRect((rect) => {
resolve(rect as UniApp.NodeInfo);
});
query.exec();
});
const [scrollViewScrollOffset, targetElementRect] = await Promise.all([
scrollViewScrollOffsetPromise,
targetElementPromise,
]);
if (!scrollViewScrollOffset || !targetElementRect) {
return 0;
}
const distance =
(targetElementRect.top ?? 0) +
(scrollViewScrollOffset.scrollTop ?? 0) -
offsetTop;
return Math.max(0, Math.round(distance));
}

View File

@ -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;

View File

@ -0,0 +1,450 @@
/*
* @file /src/vue3/hooks/components.ts
* @description uniapp组件hook
* @author tsl (randy1924@163.com)
* @date 2026-03-19 11:11:11
* @lastModified 2026-06-30 10:55:21
* @lastModifiedBy tsl (randy1924@163.com)
*/
import chroma from "chroma-js";
import { computed, ref, onMounted } from "vue";
import type { GetCityListSuccessResultResult } from "types/qqmap-wx-jssdk";
/** UniSwiperDot组件hook */
export function useUniSwiperDot() {
/** 当前索引 */
const current = ref(0);
/** 当Swiper组件发生变化时改变当前索引 */
const handleSwiperChange = (e: UniApp.SwiperEvent) => {
// #ifndef MP-WEIXIN
// 微信使用 handleSwiperAnimationfinish 切换
current.value = e.detail.current;
// #endif
};
/** 当Swiper组件动画结束时改变当前索引 */
const handleSwiperAnimationfinish = (e: UniApp.SwiperEvent) => {
// #ifdef MP-WEIXIN
// 微信使用 handleSwiperAnimationfinish 切换
current.value = e.detail.current;
// #endif
};
return { current, handleSwiperChange, handleSwiperAnimationfinish };
}
/** UniSegmentedControl组件hook */
export function useUniSegmentedControl() {
/** 当前索引 */
const current = ref(0);
/** 当当前索引被点击时,改变当前索引 */
const handleClickItem = (e: { currentIndex: number }) => {
console.log(e);
current.value = e.currentIndex;
};
return { current, handleClickItem };
}
/**
* UniDataPicker组件的时间偏移hook
*/
export function useUniDataPickerForHourOffset() {
/** 当前所选小时 */
const hour = ref(0);
/** 当前所选持续小时 */
const offset = ref(0);
/** 最多持续几个小时 */
const maxOffset = 4;
/** 小时列表 */
const hourList: UniApp.LocaldataItem<string>[] = [];
// 填充小时列表
for (let i = 1, h = 6; h < 24; i++, h++) {
// 持续时间列表
const offsetList: UniApp.LocaldataItem<string>[] = [];
// 填充持续时间列表
for (let j = 1, o = 1; o <= Math.min(24 - h, maxOffset); j++, o++) {
offsetList.push({ text: `${o}小时`, value: `${i}-${j}` });
}
hourList.push({ text: `${h}:00`, value: `${i}`, children: offsetList });
}
console.log(hourList);
/** UniDataPicker 的 localdata */
const localdata = ref<UniApp.LocaldataItem<string>[]>(hourList);
/** 当 uni-data-picker 的选项改变时,设置 hour 与 offset */
const handleChange = (e: UniApp.UniDataPickerEvent) => {
console.log(e);
hour.value = parseInt(e.detail.value[0].text.split(":")[0]);
offset.value = parseInt(e.detail.value[1].text);
console.log(hour.value, offset.value);
};
return { localdata, handleChange, hour, offset };
}
/**
* AppDataPicker组件的时间偏移hook
*/
export function useAppDataPickerForHourOffset() {
/**
*
* @example "14:00-16:00 2小时"
*/
const timeFormatter = (e: UniApp.UniDataPickerEvent) => {
console.log(e);
if (!e) {
return "";
}
const [hour, offset] = e.detail.value.map((v) => v.text);
const [h, m] = hour.split(":");
const hInt = parseInt(h);
const oInt = parseInt(offset);
return `${hour}-${hInt + oInt}:${m} ${offset}`;
};
return { ...useUniDataPickerForHourOffset(), timeFormatter };
}
/**
* UniDataPicker组件的地址hook
*/
export function useUniDataPickerForLocation() {
/**
* uni-data-picker数据格式的地址列表
*/
const localdata = ref<UniApp.LocaldataItem<number>[]>([]);
const province = ref("");
const city = ref("");
/**
* qqmap uni-data-picker
* @param qqMapList qqmap
*/
const setLocaldata = (qqMapList: GetCityListSuccessResultResult[][]) => {
console.log(qqMapList);
const list = qqMapList[0].map((p) => {
const cityList = qqMapList[1]
.slice(p.cidx?.[0] ?? 0, p.cidx?.[1] ?? 0)
.map((v) => {
return {
text: v.fullname,
value: v.id,
};
});
return {
// ...p,
text: p.fullname,
value: p.id,
children: cityList,
};
});
localdata.value = list;
console.log(localdata);
};
/** 当 uni-data-picker 的选项改变时,设置 省 与 市 */
const handleChange = (e: UniApp.UniDataPickerEvent) => {
console.log({ e });
province.value = e.detail.value[0].text;
city.value = e.detail.value[1].text;
console.log(province.value, city.value);
};
return { localdata, setLocaldata, province, city, handleChange };
}
/**
* UniDataPicker组件的hook
*
* @param type
* @see useUniDataPickerForLocation
* @see useUniDataPickerForHourOffset
*/
export function useUniDataPicker(type: "hourOffset" | "location") {
console.log(type);
let data;
switch (type) {
case "hourOffset": {
data = useUniDataPickerForHourOffset();
break;
}
case "location": {
data = useUniDataPickerForLocation();
break;
}
default:
throw new Error("参数错误");
}
return data;
}
/** Picker 组件 hook */
export function usePicker({ list } = { list: [] }) {
/** 当前索引 */
const value = ref("0");
const handleChange = (e: UniApp.PickerEvent) => {
value.value = e.detail.value;
};
const item = computed(() => {
return list[parseInt(value.value)];
});
return { value, handleChange, item };
}
/** input 组件 hook */
export function useInput() {
const value = ref("");
const handleChange = (e: UniApp.InputEvent) => {
value.value = e.detail.value;
};
const handleInput = (e: UniApp.InputEvent) => {
value.value = e.detail.value;
};
return { value, handleChange, handleInput };
}
/** 头部根据滚动高度设置背景色透明度 */
export function useHeader(finalHeight = 32, finalColor = "#fff") {
const {
color,
scrollTop,
end,
handlePageScroll: _handlePageScroll,
} = useNavbar(finalHeight, finalColor);
const handlePageScroll = (e: UniApp.ScrollEvent) => {
_handlePageScroll(e.detail.scrollTop);
};
return { color, scrollTop, end, handlePageScroll };
}
/** 头部根据滚动高度设置背景色透明度 */
export function useNavbar(
finalHeight = 32,
finalColor = "#fff",
endChange?: (end: boolean) => void,
) {
const end = ref(false);
const scrollTop = ref(0);
const color = computed(() => {
return chroma(finalColor)
.alpha(scrollTop.value / finalHeight)
.css();
});
const handlePageScroll = (st: number) => {
scrollTop.value = st;
if (scrollTop.value > finalHeight) {
end.value = true;
} else {
end.value = false;
}
endChange?.(end.value);
};
return { color, scrollTop, end, handlePageScroll };
}
// export function usePickerView(){
// const value = ref("");
// const handleChange = (e: UniApp.PickerViewEvent) => {
// value.value = e.detail.value;
// };
// return { value, handleChange };
// }
/** PickerView 的 日期 hook */
export function usePickerViewForDate() {
const date = new Date();
/** 是否可见 */
const visible = ref(false);
const value = ref<number[]>([0, 0, 0]);
const years = ref<number[]>([]);
const months = ref<number[]>([]);
const days = ref<number[]>([]);
const year = ref(date.getFullYear());
const month = ref(date.getMonth() + 1);
const day = ref(date.getDate());
/** 起始年 */
const startYear = ref(date.getFullYear() - 50);
for (let i = startYear.value; i <= date.getFullYear(); i++) {
years.value.push(i);
}
for (let i = 1; i <= 12; i++) {
months.value.push(i);
}
for (let i = 1; i <= 31; i++) {
days.value.push(i);
}
const handleChange = (e: UniApp.PickerViewEvent<number>) => {
const [newYear, newMonth, newDay] = e.detail.value;
year.value = years.value[newYear];
month.value = months.value[newMonth];
day.value = days.value[newDay];
};
onMounted(() => {
// NOTE 只能在组件挂载完成后才能响应式修改值
// 初始化日期为今天
const currentYear = years.value.findIndex((v: number) => v === year.value);
const currentMonth = months.value.findIndex(
(v: number) => v === month.value,
);
const currentDay = days.value.findIndex((v: number) => v === day.value);
value.value = [currentYear, currentMonth, currentDay];
});
return {
value,
handleChange,
visible,
years,
months,
days,
year,
month,
day,
};
}
/**
*
* @param oldArr
* @param newArr
* @returns -1
*/
function comparisonChangedIndex(
oldArr: unknown[],
newArr: unknown[],
fun?: (newValue: unknown, oldValue: unknown) => boolean,
): number {
for (let i = 0; i < oldArr.length; i++) {
if (fun ? fun(oldArr[i], newArr[i]) : oldArr[i] !== newArr[i]) {
return i;
}
}
if (oldArr.length !== newArr.length) {
return (
Math.max(oldArr.length, newArr.length) -
Math.abs(oldArr.length - newArr.length)
);
}
return -1;
}
/** PickerView 的 qqmap hook */
export function usePickerViewForQqMapList(
qqmapList: GetCityListSuccessResultResult[][],
) {
/** 是否可见 */
const visible = ref(false);
const value = ref<number[]>([0, 0, 0]);
const [allProvinceList, allCityList, allDistrictList] = qqmapList;
const provinceList = ref(allProvinceList);
const cityList = ref(allCityList);
const districtList = ref(allDistrictList);
const province = ref(provinceList.value[0]);
const city = ref(cityList.value[0]);
const district = ref(districtList.value[0]);
let lastSelect = [0, 0, 0];
const handleChange = (e: UniApp.PickerViewEvent<number>) => {
const changedIndex = comparisonChangedIndex(lastSelect, e.detail.value);
lastSelect = e.detail.value;
const [newProvinceIndex, newCityIndex, newDistrictIndex] = lastSelect;
if (changedIndex === 0) {
// 变化了省,更新市和区
// provinceList.value = allProvinceList;
province.value = provinceList.value[newProvinceIndex];
cityList.value = allCityList.slice(
province.value.cidx?.[0] ?? 0,
province.value.cidx?.[1] ?? 0,
);
city.value = cityList.value[0];
districtList.value = allDistrictList.slice(
city.value.cidx?.[0] ?? 0,
city.value.cidx?.[1] ?? 0,
);
district.value = districtList.value[0];
value.value = [newProvinceIndex, 0, 0];
} else if (changedIndex === 1) {
// 变化了市,则只变化区
city.value = cityList.value[newCityIndex];
districtList.value = allDistrictList.slice(
city.value?.cidx?.[0] ?? 0,
city.value?.cidx?.[1] ?? 0,
);
district.value = districtList.value[0];
value.value = [newProvinceIndex, newCityIndex, 0];
} else if (changedIndex === 2) {
district.value = districtList.value[newDistrictIndex];
value.value = [newProvinceIndex, newCityIndex, newDistrictIndex];
}
};
onMounted(() => {
// NOTE 只能在组件挂载完成后才能响应式修改值
const [newProvinceIndex, newCityIndex, newDistrictIndex] = lastSelect;
provinceList.value = allProvinceList;
province.value = provinceList.value[newProvinceIndex];
cityList.value = allCityList.slice(
province.value.cidx?.[0],
province.value.cidx?.[1],
);
city.value = cityList.value?.[newCityIndex];
districtList.value = allDistrictList.slice(
city.value?.cidx?.[0] ?? 0,
city.value?.cidx?.[1] ?? 0,
);
district.value = districtList.value?.[newDistrictIndex];
value.value = [newProvinceIndex, newCityIndex, newDistrictIndex];
});
return {
value,
handleChange,
visible,
provinceList,
cityList,
districtList,
province,
city,
district,
};
}

View File

@ -0,0 +1,13 @@
/*
* @file /src/vue3/hooks/index.ts
* @description
* @author tsl (randy1924@163.com)
* @date 2026-03-20 09:09:42
* @lastModified 2026-03-20 10:42:54
* @lastModifiedBy tsl (randy1924@163.com)
*/
export * from "./components";
export * from "./page";
export * from "./page-tab";
export * from "./top-sticky";

View File

@ -0,0 +1,280 @@
/*
* @file /src/vue3/hooks/page-tab.ts
* @description tab点击自动滚动到对应位置
* @author tsl (randy1924@163.com)
* @date 2026-03-26 10:12:41
* @lastModified 2026-04-20 14:08:14
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { slowlyScroll } from "@r-utils/common";
import { debounce } from "lodash-es";
import { ref, reactive, toRef } from "vue";
import { calculateDistanceBySelectorAsync } from "../../uni-helper";
import type { Ref } from "vue";
/**
* Tab
* @since 1.2.2
*/
export type TabConfig<
T extends Record<string, unknown> = Record<string, unknown>,
> = {
/** Tab 名称(显示文本) */
name: string;
/** Tab 值(唯一标识) */
value: string;
/** 对应的选择器 */
selector: string;
/** 锚点偏移量,用于微调滚动位置 */
offset?: number;
} & T;
/**
* Tab
* @since 1.2.2
*/
export interface PageTabOptions {
/** 当前激活的 tab */
currentPageTab?: Ref<string>;
/** 滚动容器的选择器 */
scrollViewSelector?: string;
/** 基础偏移高度(导航栏高度等) */
baseOffsetTop?: number;
/** 防抖延迟时间(毫秒) */
debounceDelay?: number;
/** 切换偏移阈值 */
switchOffset?: number;
/** Tab 值的键名 */
tabValueKey?: string;
}
/**
* Tab
* @since 1.2.2
*/
export interface PageTabReturn<
T extends Record<string, unknown> = Record<string, unknown>,
> {
/** 当前滚动位置,<scroll-view> 的 scroll-top */
scrollTop: ReturnType<typeof ref<number>>;
/** 实时滚动位置 */
currentScrollTop: ReturnType<typeof ref<number>>;
/** 当前激活的 tab */
currentPageTab: ReturnType<typeof ref<string>>;
/** Tab 锚点选择器映射 */
pageTabAnchorSelectorMap: Record<string, string>;
/** Tab 锚点偏移映射 */
pageTabAnchorOffsetMap: Record<string, number>;
/** Tab 锚点距离映射(计算后的实际距离) */
pageTabAnchorDistanceMap: Record<string, number>;
/** 是否正在进行 tab 滚动 */
isPageTabScrolling: ReturnType<typeof ref<boolean>>;
/** 滚动事件处理函数 */
handlePageScroll: (e: number) => void;
/** 更新所有锚点位置 */
updatePageTabAnchorDistance: (tabConfigs?: TabConfig<T>[]) => Promise<void>;
/** 滚动到指定 tab */
scrollToPageTab: (tabValue: string) => Promise<void>;
/** Tab 切换事件处理 */
tabChange: (tabValue: string) => void;
/** 页面 Tab 滚动处理(防抖) */
handlePageTabScroll: (scrollTopValue: number) => void;
/** 设置基础偏移高度 */
setBaseOffsetTop: (offsetTop: number) => void;
}
/**
* 使 Tab
*
* @param tabConfigs - Tab
* @param options -
* @param onPageScroll -
* @returns Tab
*
* @example
* ```ts
* const tabConfigs = [
* { name: "活动详情", value: "活动详情", selector: ".activity-desc-wrapper", offset: 45 },
* { name: "俱乐部评价", value: "俱乐部评价", selector: ".club-comment-list", offset: 50 },
* ];
*
* const {
* scrollTop,
* currentPageTab,
* handlePageScroll,
* updatePageTabAnchorDistance,
* tabChange,
* } = usePageTab(tabConfigs, {
* baseOffsetTop: 92,
* debounceDelay: 50,
* }, (scrollTop) => {
* // 处理导航栏渐变等额外逻辑
* handleNavbarPageScroll(scrollTop);
* });
* ```
* @since 1.2.2
*/
export function usePageTab<
T extends Record<string, unknown> = Record<string, unknown>,
>(tabConfigs: TabConfig<T>[], options: PageTabOptions = {}): PageTabReturn<T> {
const {
scrollViewSelector = ".scroll-view",
debounceDelay = 50,
switchOffset = 5,
} = options;
const tabValueKey = options.tabValueKey ?? "value";
let baseOffsetTop: number = options.baseOffsetTop ?? 80 + 12;
// 响应式状态
const scrollTop = ref(0);
const currentScrollTop = ref(0);
const currentPageTab = toRef(options.currentPageTab);
const isPageTabScrolling = ref(false);
// 初始化映射对象
const pageTabAnchorSelectorMap: Record<string, string> = reactive({});
const pageTabAnchorOffsetMap: Record<string, number> = reactive({});
const pageTabAnchorDistanceMap: Record<string, number> = reactive({});
setTabConfigs(tabConfigs);
function setTabConfigs(configs: TabConfig<T>[]): void {
for (const config of configs) {
const tabValue = config[tabValueKey] as string;
pageTabAnchorSelectorMap[tabValue] = config.selector;
pageTabAnchorOffsetMap[tabValue] = config.offset ?? 0;
pageTabAnchorDistanceMap[tabValue] = 0;
}
}
/**
* Tab
*/
async function updatePageTabAnchorDistance(
configs?: TabConfig<T>[],
): Promise<void> {
const _configs = configs ?? tabConfigs;
for (const config of _configs) {
const tabValue = config[tabValueKey] as string;
try {
const offset = config.offset ?? pageTabAnchorOffsetMap[tabValue] ?? 0;
const distance = await calculateDistanceBySelectorAsync(
scrollViewSelector,
config.selector,
offset + baseOffsetTop,
);
pageTabAnchorDistanceMap[tabValue] = distance;
} catch (error) {
console.warn(
`Failed to calculate distance for tab: ${tabValue}`,
error,
);
pageTabAnchorDistanceMap[tabValue] = 0;
}
}
}
/**
* Tab
*/
async function scrollToPageTab(tabValue: string): Promise<void> {
console.log("scrollToPageTab", tabValue);
// 避免点击 tab 时触发 handlePageScroll 再反向修改 currentPageTab
isPageTabScrolling.value = true;
try {
const target = pageTabAnchorDistanceMap[tabValue] || 0;
if (Math.abs(target - currentScrollTop.value) < 1) {
return;
}
await slowlyScroll(currentScrollTop.value, target, 500, (value) => {
scrollTop.value = value;
});
} finally {
// 给 scroll-view 一点时间完成滚动
setTimeout(() => {
isPageTabScrolling.value = false;
}, 100);
}
}
/**
* Tab
*/
function tabChange(tabValue: string): void {
console.log("tabChange", tabValue);
scrollToPageTab(tabValue);
}
/**
* Tab
*/
const handlePageTabScroll = debounce((scrollTopValue: number) => {
if (isPageTabScrolling.value) {
return;
}
// 获取所有 tab 的值,并按距离从大到小排序
const sortedTabs = Object.keys(pageTabAnchorDistanceMap)
.map((tabValue) => ({
value: tabValue,
distance: pageTabAnchorDistanceMap[tabValue],
}))
.filter((tab) => tab.distance > 0)
.sort((a, b) => b.distance - a.distance);
// 从下往上检查各个 tab 的位置,找到当前应该显示的 tab
for (const tab of sortedTabs) {
// console.log("handlePageTabScroll", scrollTopValue, tab, tab.distance - switchOffset);
if (scrollTopValue >= tab.distance - switchOffset) {
if (currentPageTab.value !== tab.value) {
currentPageTab.value = tab.value;
}
return;
}
}
// 如果都没有匹配上,显示第一个 tab
if (sortedTabs.length > 0) {
const firstTab = sortedTabs[sortedTabs.length - 1];
if (currentPageTab.value !== firstTab.value) {
currentPageTab.value = firstTab.value;
}
}
}, debounceDelay);
/**
*
*/
function handlePageScroll(scrollTop: number): void {
currentScrollTop.value = scrollTop;
// 处理 Tab 滚动
handlePageTabScroll(scrollTop);
}
function setBaseOffsetTop(offsetTop: number): void {
baseOffsetTop = offsetTop;
updatePageTabAnchorDistance();
}
return {
scrollTop,
currentScrollTop,
currentPageTab,
pageTabAnchorSelectorMap,
pageTabAnchorOffsetMap,
pageTabAnchorDistanceMap,
isPageTabScrolling,
handlePageScroll,
updatePageTabAnchorDistance,
scrollToPageTab,
tabChange,
setBaseOffsetTop,
handlePageTabScroll,
};
}

View File

@ -0,0 +1,73 @@
import { onReady } from "@dcloudio/uni-app";
import qs from "qs";
/**
*
* NOTE titlequery应该与withShareTicketmenus分开配置
*/
export interface ShareOptions {
title: string;
path?: string;
query: Record<string, string>;
imageUrl?: string;
withShareTicket?: boolean;
menus?: UniApp.ShowShareMenuOptionsMenu[];
}
function getDefaultShareOptions(): ShareOptions {
return {
title: "",
query: {},
withShareTicket: true,
menus: ["shareAppMessage", "shareTimeline"],
};
}
function getOptions(
options?: ShareOptions | (() => ShareOptions),
): ShareOptions {
const oOptions = typeof options === "function" ? options() : options;
const shareOptions = Object.assign(getDefaultShareOptions(), oOptions);
return shareOptions;
}
export function useShare(options?: ShareOptions | (() => ShareOptions)) {
const pages = getCurrentPages();
const currentPage = pages.at(-1) as Page.PageInstance;
const currentRoute = currentPage.route;
const shareOptions = getOptions(options);
const withShareTicket = shareOptions.withShareTicket;
const menus = shareOptions.menus;
onReady(() => {
const launchOptions = uni.getLaunchOptionsSync();
if ([1154].includes(launchOptions.scene)) {
return;
}
uni.showShareMenu({
withShareTicket,
menus,
});
});
function getShareOptions() {
const shareOptions = getOptions(options);
const query = qs.stringify(shareOptions.query);
const path = shareOptions.path || `/${currentRoute}`;
return {
title: shareOptions.title,
imageUrl: shareOptions.imageUrl,
path: `${path}?${query}`,
query,
};
}
return {
getShareOptions,
};
}

View File

@ -0,0 +1,45 @@
/*
* @file /src/vue3/hooks/top-sticky.ts
* @description
* @author tsl (randy1924@163.com)
* @date 2026-03-20 17:51:50
* @lastModified 2026-03-26 10:24:21
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { ref } from "vue";
export interface UseTopStickyOptions {
context?: never;
offsetTop?: number;
targetSelector: string;
onToTopChange?: (toTop: boolean) => void;
}
export function useTopSticky(options: UseTopStickyOptions) {
const context = options.context;
const offsetTop = options.offsetTop ?? 0;
const targetSelector = options.targetSelector;
const onToTopChange = options.onToTopChange;
// 是否达到了顶部
const isToTop = ref(false);
const systemInfo = uni.getSystemInfoSync();
const intersectionObserver = uni.createIntersectionObserver(context);
// 观察目标元素
intersectionObserver
.relativeToViewport({
top: 0,
bottom: -systemInfo.windowHeight + offsetTop,
})
.observe(targetSelector, (res) => {
console.log("intersectionObserver", res);
// 当目标与视口顶部距离小于0px时isToTop 设为 true
isToTop.value = res.intersectionRatio > 0;
onToTopChange?.(isToTop.value);
});
return { isToTop };
}

View File

@ -0,0 +1,10 @@
/*
* @file: /src/vue3/index.ts
* @description:
* @author: tsl (randy1924@163.com)
* @date: 2026-03-20 09:09:08
* @lastModified: 2026-03-20 09:45:42
* @lastModifiedBy: tsl (randy1924@163.com)
*/
export * from "./hooks";

View File

@ -7,7 +7,6 @@
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"outDir": "./dist"
}
}
}

File diff suppressed because it is too large Load Diff

45
packages/uni-app/types/nfc/android.d.ts vendored Normal file
View File

@ -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[];
}

6
packages/uni-app/types/nfc/index.d.ts vendored Normal file
View File

@ -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;

69
packages/uni-app/types/nfc/weixin.d.ts vendored Normal file
View File

@ -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>;
}

View File

@ -1,4 +1,5 @@
import * as QQMapWX from "@jonny1994/qqmap-wx-jssdk";
export * from "@jonny1994/qqmap-wx-jssdk";
/**
*

View File

@ -1,10 +1,10 @@
/// <reference types="@dcloudio/types" />
declare namespace UniNamespace {
type LocaldataItem = {
type LocaldataItem<T = string> = {
text: string;
value: string;
children?: LocaldataItem[];
value: T;
children?: LocaldataItem<T>[];
};
type LocaldataNodeclickValue = {
text: string;
@ -15,7 +15,7 @@ declare namespace UniNamespace {
type Event<T> = { detail: T };
type InputEvent = Event<{ value: string }>;
type PickerEvent = Event<{ value: string }>;
type PickerViewEvent = Event<{ value: unknown[] }>;
type PickerViewEvent<T> = Event<{ value: T[] }>;
type ScrollEvent = Event<{ scrollTop: number }>;
type SwiperEvent = Event<{ current: number }>;
type UniDataPickerEvent = Event<{ value: LocaldataChangeValue[] }>;
@ -111,7 +111,7 @@ interface PlusIo {
resolveLocalFileSystemURL(
url?: string,
succesCB?: (result: PlusIoFileEntry) => void,
errorCB?: (result: any) => void,
errorCB?: (result: PlusIoDirectoryEntry) => void,
): void;
}

View File

@ -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",
}),
],
});

View File

@ -0,0 +1,13 @@
# @r-utils/uview-plus
## 2.0.0
### Major Changes
- 添加工具
## 1.3.0
### Minor Changes
- 添加工具

View File

@ -0,0 +1,86 @@
# @r-utils/uview-plus
在项目已使用 [uview-plus](https://uview-plus.jiangruyi.com/) 的前提下,提供适用于 uview-plus 组件的组合式 API Hooks。
## 适用范围
- 适用于已接入 `uview-plus` 的项目。
- 典型场景是 uni-app + Vue3 + uview-plus。
## 不适用范围
- 未使用 `uview-plus` 的项目不建议使用。
- 不适用于 Vue2 项目。
## 安装
```bash
pnpm add @r-utils/uview-plus
```
## 导入方式
### 推荐:根入口导入
大多数场景推荐从根入口导入,路径简单,使用心智负担更低。
```ts
import {
usePickerSingle,
usePicker,
useDateTimePicker,
useCalendar,
} from "@r-utils/uview-plus";
```
### 兼容:子路径导入
如果你希望模块边界更清晰,也可以使用子路径导入。两种方式都支持。
```ts
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";
```
## 导出模块
| 子路径 | 说明 |
| --- | --- |
| `@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 |
## 使用示例
### 单列选择器
```ts
import { usePickerSingle } from "@r-utils/uview-plus";
const picker = usePickerSingle({
list: [
{ text: "启用", value: 1 },
{ text: "禁用", value: 0 },
],
});
picker.showPicker();
```
### 子路径导入
```ts
import { usePicker } from "@r-utils/uview-plus/picker";
const picker = usePicker({
columns: [[{ text: "浙江", value: "zhejiang" }]],
});
```
## 注意事项
- 推荐优先使用根入口导入;如果需要更精确的模块边界,也可以使用子路径导入。
- 本包依赖 Vue3 和 uview-plus请确保业务项目已安装并正确配置。

View File

@ -0,0 +1,99 @@
{
"name": "@r-utils/uview-plus",
"version": "2.0.0",
"private": false,
"description": "uview-plus 组合式 API Hooks",
"type": "module",
"main": "dist/index.cjs",
"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",
"uview-plus",
"uni-app"
],
"publishConfig": {
"registry": "http://npm.nps.yunvip123.cn"
},
"author": {
"name": "CodiceFabbrica",
"email": "randy1924@163.com",
"url": "https://gitee.com/codice_fabbrica"
},
"homepage": "https://gitee.com/codice_fabbrica/r-util-js",
"repository": {
"url": "https://gitee.com/codice_fabbrica/r-util-js.git",
"type": "git"
},
"bugs": {
"url": "https://gitee.com/codice_fabbrica/r-util-js/issues"
},
"engines": {
"node": ">=18.12.0",
"pnpm": ">=10.0.0"
},
"license": "ISC",
"packageManager": "pnpm@10.32.1",
"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",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "vitest run"
},
"peerDependencies": {
"uview-plus": ">=3.0.0",
"vue": "^3.3.0"
},
"peerDependenciesMeta": {
"uview-plus": {
"optional": true
}
},
"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:",
"vitest": "catalog:",
"vue": "^3.5.13"
}
}

View File

@ -0,0 +1,67 @@
import { toRef, computed } from "vue";
import type { Ref } from "vue";
interface UseCalendarOptions {
value?: string | string[] | Ref<string | string[]>;
show?: boolean | Ref<boolean>;
mode?: "single" | "multiple";
placeholder?: string;
separator?: string;
}
/**
* 使 uview-plus calendar
* uview-plus版本~3.4.51
* @param options
* @returns
*/
export function useCalendar(options: UseCalendarOptions) {
const value = toRef(options.value ?? null);
const show = toRef(options.show ?? false);
const mode = toRef(options.mode ?? "single");
const placeholder = toRef(options.placeholder ?? "请选择");
const separator = toRef(options.separator ?? ",");
const text = computed(() => {
if (mode.value === "multiple") {
return (
(value.value as string[])?.join(separator.value) ?? placeholder.value
);
} else {
return value.value ?? placeholder.value;
}
});
function showCalendar() {
show.value = true;
}
function hideCalendar() {
show.value = false;
}
function handleConfirm(e: UViewPlus.CalendarConfirmEvent) {
console.log("handleConfirm:", e);
if (mode.value === "multiple") {
value.value = e;
} else {
value.value = e[0];
}
hideCalendar();
}
function handleClose() {
hideCalendar();
}
return {
value,
show,
text,
showCalendar,
hideCalendar,
handleConfirm,
handleClose,
};
}

View File

@ -0,0 +1,51 @@
import { toRef, computed } from "vue";
import type { Ref } from "vue";
interface UseDateTimePickerOptions {
value?: string | Ref<string>;
show?: boolean | Ref<boolean>;
placeholder?: string;
}
/**
* 使 uview-plus datetime-picker
* uview-plus版本~3.4.51
* @param options
* @returns
*/
export function useDateTimePicker(options: UseDateTimePickerOptions) {
const value = toRef(options.value ?? null);
const show = toRef(options.show ?? false);
const placeholder = toRef(options.placeholder ?? "请选择");
const text = computed(() => {
return value.value ?? placeholder.value;
});
function showPicker() {
show.value = true;
}
function hidePicker() {
show.value = false;
}
function handleConfirm(e: UViewPlus.PickerConfirmEvent) {
console.log("handleConfirm:", e);
hidePicker();
}
function handleCancel() {
hidePicker();
}
return {
value,
show,
text,
showPicker,
hidePicker,
handleConfirm,
handleCancel,
};
}

View File

@ -0,0 +1,4 @@
export * from "./picker-single";
export * from "./picker";
export * from "./datetime-picker";
export * from "./calendar";

View File

@ -0,0 +1,139 @@
import { cloneDeep } from "lodash-es";
import { toRef, computed, watch, nextTick, ref } from "vue";
import type { Ref } from "vue";
interface UsePickerSingleOptions {
value?: unknown | Ref<unknown>;
show?: boolean | Ref<boolean>;
indexes?: Array<number | null> | Ref<Array<number | null>>;
list?: UViewPlus.PickerColumns[0] | Ref<UViewPlus.PickerColumns[0]>;
textName?: string;
valueName?: string;
placeholder?: string;
}
/**
* picker
* @param options
* @returns
*/
export function usePickerSingle(options: UsePickerSingleOptions) {
const value = toRef(options.value ?? null);
const show = toRef(options.show ?? false);
const indexes = toRef(options.indexes ?? [null]);
const list = toRef(options.list ?? []);
const textName = toRef(options.textName ?? "text");
const valueName = toRef(options.valueName ?? "value");
const placeholder = toRef(options.placeholder ?? "请选择");
const defaultIndex = ref(cloneDeep(indexes.value));
const modelValue = computed({
get() {
return [value.value];
},
set(v) {
value.value = v[0];
},
});
// 将单列数据转换为 columns 格式以兼容 picker 组件
const columns = computed(() => [list.value]);
// 获取当前选中项的文本
const text = computed(() => {
const index = indexes.value[0];
if (index == null) return placeholder.value;
const item = list.value[index];
return item?.[textName.value] ?? placeholder.value;
});
// 监听 indexes 变化,更新 value
watch(
[indexes, list],
([newIndexes, newList]) => {
console.log("indexes changed:", newIndexes, newList);
const index = newIndexes[0];
if (index == null) return;
const item = newList[index];
const newValue = item?.[valueName.value];
if (newValue !== value.value) {
value.value = newValue;
}
},
{ immediate: true, deep: true },
);
// 监听 value 变化,更新 indexes
watch(
[value, list],
([newValue, newList]) => {
console.log("value changed:", newValue, newList);
const newIndex = newList.findIndex(
(item) => item[valueName.value] === newValue,
);
const finalIndex = newIndex >= 0 ? newIndex : null;
if (finalIndex !== indexes.value[0]) {
indexes.value = [finalIndex];
}
},
{ immediate: true },
);
function handleConfirm(e: UViewPlus.PickerConfirmEvent) {
console.log("handleConfirm:", e);
const indexs = e.indexs;
// 确认后必须选择值如果未选择则设置成第1个
if (indexs[0] == null) {
indexs[0] = 0;
}
indexes.value = indexs;
hidePicker();
}
function handleCancel() {
hidePicker();
}
function showPicker() {
const oIndexes = cloneDeep(indexes.value);
defaultIndex.value = [0];
show.value = true;
nextTick(() => {
// TODO 刷新默认选项,待优化
defaultIndex.value = oIndexes;
});
}
function hidePicker() {
show.value = false;
}
function valueToText(v: unknown) {
const currentItem = list.value.find((item) => item[valueName.value] === v);
return currentItem?.[textName.value] ?? placeholder.value;
}
return {
text,
value,
modelValue,
indexes,
defaultIndex,
columns,
textName,
valueName,
show,
handleConfirm,
valueToText,
showPicker,
hidePicker,
handleCancel,
};
}

View File

@ -0,0 +1,123 @@
import { isEqual } from "lodash-es";
import { toRef, computed, watch } from "vue";
import type { Ref } from "vue";
interface UsePickerOptions {
/** 选中的值 */
value?: unknown[] | Ref<unknown[]>;
show?: boolean | Ref<boolean>;
/** 选中的值 */
indexes?: Array<number | null> | Ref<Array<number | null>>;
columns?: UViewPlus.PickerColumns | Ref<UViewPlus.PickerColumns>;
textName?: string;
valueName?: string;
placeholder?: string;
separator?: string;
}
/**
* picker
* @param options
* @returns
*/
export function usePicker(options: UsePickerOptions) {
const value = toRef(options.value ?? []);
const show = toRef(options.show ?? false);
const indexes = toRef(options.indexes ?? []);
const columns = toRef(options.columns ?? [[]]);
const textName = toRef(options.textName ?? "text");
const valueName = toRef(options.valueName ?? "value");
const placeholder = toRef(options.placeholder ?? "请选择");
const separator = toRef(options.separator ?? " ");
const text = computed(() => {
const textArray = indexes.value.map((i, index) => {
if (i == null) return null;
const item = columns.value[index][i];
return item?.[textName.value] ?? null;
});
if (textArray.some((v) => v == null)) {
return placeholder.value;
}
return textArray.join(separator.value);
});
// 监听 indexes 变化,更新 value
watch(
[indexes, columns],
([newIndexes, newColumns]) => {
console.log("indexes changed:", newIndexes, newColumns);
if (!newIndexes.length) return;
const newValue = newIndexes.map((index, columnIndex) => {
if (index == null) return null;
const item = newColumns[columnIndex]?.[index];
return item?.[valueName.value] ?? null;
});
if (!isEqual(newValue, value.value)) {
value.value = newValue;
}
},
{ immediate: true, deep: true },
);
// 监听 value 变化,更新 indexes
watch(
[value, columns],
([newValue, newColumns]) => {
console.log("value changed:", newValue, newColumns);
const newIndexes = newColumns.map((column, columnIndex) => {
const index = column.findIndex(
(item) => item[valueName.value] === newValue?.[columnIndex],
);
return index >= 0 ? index : null;
});
if (!isEqual(newIndexes, indexes.value)) {
indexes.value = newIndexes;
}
},
{ immediate: true, deep: true },
);
function handleConfirm(e: UViewPlus.PickerConfirmEvent) {
console.log("handleConfirm:", e);
indexes.value = e.indexs;
}
function columnValueToText(v: unknown, colIndex = 0) {
const currentItem = columns.value[colIndex].find(
(item) => item[valueName.value] === v,
);
return currentItem?.[textName.value];
}
function valueToText(v: unknown[]) {
return v.map((item, index) => columnValueToText(item, index));
}
function showPicker() {
show.value = true;
}
function hidePicker() {
show.value = false;
}
return {
text,
value,
indexes,
columns,
handleConfirm,
valueToText,
columnValueToText,
textName,
valueName,
show,
showPicker,
hidePicker,
};
}

View File

@ -0,0 +1,98 @@
import { describe, test, expect } from "vitest";
import { effectScope } from "vue";
import { useCalendar } from "../src/calendar";
describe("useCalendar", () => {
test("默认值", () => {
const scope = effectScope();
scope.run(() => {
const { value, show, text } = useCalendar({});
expect(value.value).toBeNull();
expect(show.value).toBe(false);
expect(text.value).toBe("请选择");
});
scope.stop();
});
test("单选模式text 显示 value无值时显示 placeholder", () => {
const scope = effectScope();
scope.run(() => {
const { value, text } = useCalendar({
mode: "single",
placeholder: "请选日期",
});
expect(text.value).toBe("请选日期");
value.value = "2024-01-15";
expect(text.value).toBe("2024-01-15");
});
scope.stop();
});
test("多选模式text 用 separator 拼接", () => {
const scope = effectScope();
scope.run(() => {
const { value, text } = useCalendar({ mode: "multiple", separator: "-" });
expect(text.value).toBe("请选择");
value.value = ["2024-01-15", "2024-01-16"];
expect(text.value).toBe("2024-01-15-2024-01-16");
});
scope.stop();
});
test("showCalendar / hideCalendar", () => {
const scope = effectScope();
scope.run(() => {
const { show, showCalendar, hideCalendar } = useCalendar({});
expect(show.value).toBe(false);
showCalendar();
expect(show.value).toBe(true);
hideCalendar();
expect(show.value).toBe(false);
});
scope.stop();
});
test("handleConfirm 单选模式:设置 value 并关闭", () => {
const scope = effectScope();
scope.run(() => {
const { value, show, handleConfirm } = useCalendar({ mode: "single" });
show.value = true;
handleConfirm(["2024-06-01", "2024-06-02"]);
expect(value.value).toBe("2024-06-01");
expect(show.value).toBe(false);
});
scope.stop();
});
test("handleConfirm 多选模式:设置 value 数组并关闭", () => {
const scope = effectScope();
scope.run(() => {
const { value, show, handleConfirm } = useCalendar({ mode: "multiple" });
show.value = true;
handleConfirm(["2024-06-01", "2024-06-02"]);
expect(value.value).toEqual(["2024-06-01", "2024-06-02"]);
expect(show.value).toBe(false);
});
scope.stop();
});
test("handleClose 关闭弹窗", () => {
const scope = effectScope();
scope.run(() => {
const { show, handleClose } = useCalendar({});
show.value = true;
handleClose();
expect(show.value).toBe(false);
});
scope.stop();
});
test("接受初始 value", () => {
const scope = effectScope();
scope.run(() => {
const { text } = useCalendar({ value: "2024-03-10", mode: "single" });
expect(text.value).toBe("2024-03-10");
});
scope.stop();
});
});

View File

@ -0,0 +1,75 @@
import { describe, test, expect } from "vitest";
import { effectScope } from "vue";
import { useDateTimePicker } from "../src/datetime-picker";
describe("useDateTimePicker", () => {
test("默认值", () => {
const scope = effectScope();
scope.run(() => {
const { value, show, text } = useDateTimePicker({});
expect(value.value).toBeNull();
expect(show.value).toBe(false);
expect(text.value).toBe("请选择");
});
scope.stop();
});
test("自定义 placeholder", () => {
const scope = effectScope();
scope.run(() => {
const { text } = useDateTimePicker({ placeholder: "选择时间" });
expect(text.value).toBe("选择时间");
});
scope.stop();
});
test("有 value 时 text 显示 value", () => {
const scope = effectScope();
scope.run(() => {
const { value, text } = useDateTimePicker({ value: "2024-01-15 10:00" });
expect(text.value).toBe("2024-01-15 10:00");
value.value = "2024-06-01 08:30";
expect(text.value).toBe("2024-06-01 08:30");
});
scope.stop();
});
test("showPicker / hidePicker", () => {
const scope = effectScope();
scope.run(() => {
const { show, showPicker, hidePicker } = useDateTimePicker({});
expect(show.value).toBe(false);
showPicker();
expect(show.value).toBe(true);
hidePicker();
expect(show.value).toBe(false);
});
scope.stop();
});
test("handleConfirm 关闭弹窗(不更新 value", () => {
const scope = effectScope();
scope.run(() => {
const { value, show, handleConfirm } = useDateTimePicker({
value: "2024-01-01",
});
show.value = true;
const event = { indexs: [], value: [], values: [] } as any;
handleConfirm(event);
expect(show.value).toBe(false);
expect(value.value).toBe("2024-01-01");
});
scope.stop();
});
test("handleCancel 关闭弹窗", () => {
const scope = effectScope();
scope.run(() => {
const { show, handleCancel } = useDateTimePicker({});
show.value = true;
handleCancel();
expect(show.value).toBe(false);
});
scope.stop();
});
});

View File

@ -0,0 +1,132 @@
import { describe, test, expect } from "vitest";
import { effectScope, nextTick } from "vue";
import { usePickerSingle } from "../src/picker-single";
const list = [
{ text: "选项A", value: "a" },
{ text: "选项B", value: "b" },
{ text: "选项C", value: "c" },
];
describe("usePickerSingle", () => {
test("默认值", () => {
const scope = effectScope();
scope.run(() => {
const { value, show, text, indexes } = usePickerSingle({});
expect(value.value).toBeNull();
expect(show.value).toBe(false);
expect(text.value).toBe("请选择");
expect(indexes.value).toEqual([null]);
});
scope.stop();
});
test("columns 是 [list]", () => {
const scope = effectScope();
scope.run(() => {
const { columns } = usePickerSingle({ list });
expect(columns.value).toEqual([list]);
});
scope.stop();
});
test("初始 value 匹配 list 时设置正确 indexes 和 text", async () => {
const scope = effectScope();
await scope.run(async () => {
const { text, indexes } = usePickerSingle({ value: "b", list });
await nextTick();
expect(indexes.value).toEqual([1]);
expect(text.value).toBe("选项B");
});
scope.stop();
});
test("初始 indexes 匹配 list 时设置正确 value 和 text", async () => {
const scope = effectScope();
await scope.run(async () => {
const { value, text } = usePickerSingle({ indexes: [2], list });
await nextTick();
expect(value.value).toBe("c");
expect(text.value).toBe("选项C");
});
scope.stop();
});
test("valueToText根据 value 查找 text", () => {
const scope = effectScope();
scope.run(() => {
const { valueToText } = usePickerSingle({ list });
expect(valueToText("a")).toBe("选项A");
expect(valueToText("b")).toBe("选项B");
expect(valueToText("unknown")).toBe("请选择");
});
scope.stop();
});
test("showPicker / hidePicker", () => {
const scope = effectScope();
scope.run(() => {
const { show, showPicker, hidePicker } = usePickerSingle({});
expect(show.value).toBe(false);
showPicker();
expect(show.value).toBe(true);
hidePicker();
expect(show.value).toBe(false);
});
scope.stop();
});
test("handleConfirm更新 indexes 并关闭", async () => {
const scope = effectScope();
await scope.run(async () => {
const { value, show, indexes, handleConfirm } = usePickerSingle({ list });
show.value = true;
handleConfirm({ indexs: [1], value: [list[1]], values: [list] } as any);
expect(indexes.value).toEqual([1]);
expect(show.value).toBe(false);
await nextTick();
expect(value.value).toBe("b");
});
scope.stop();
});
test("handleCancel关闭弹窗", () => {
const scope = effectScope();
scope.run(() => {
const { show, handleCancel } = usePickerSingle({});
show.value = true;
handleCancel();
expect(show.value).toBe(false);
});
scope.stop();
});
test("modelValueget/set 代理 value", async () => {
const scope = effectScope();
await scope.run(async () => {
const { value, modelValue } = usePickerSingle({ list });
expect(modelValue.value).toEqual([null]);
modelValue.value = "c";
expect(value.value).toBe("c");
});
scope.stop();
});
test("自定义 textName 和 valueName", () => {
const customList = [
{ label: "甲", id: 1 },
{ label: "乙", id: 2 },
];
const scope = effectScope();
scope.run(() => {
const { valueToText } = usePickerSingle({
list: customList as any,
textName: "label",
valueName: "id",
});
expect(valueToText(1)).toBe("甲");
expect(valueToText(2)).toBe("乙");
});
scope.stop();
});
});

View File

@ -0,0 +1,142 @@
import { describe, test, expect } from "vitest";
import { effectScope, nextTick } from "vue";
import { usePicker } from "../src/picker";
const columns = [
[
{ text: "省A", value: "pa" },
{ text: "省B", value: "pb" },
],
[
{ text: "市A", value: "ca" },
{ text: "市B", value: "cb" },
],
];
describe("usePicker", () => {
test("默认值", async () => {
const scope = effectScope();
await scope.run(async () => {
const { value, show, text, indexes } = usePicker({});
await nextTick();
expect(value.value).toEqual([null]);
expect(show.value).toBe(false);
expect(text.value).toBe("请选择");
expect(indexes.value).toEqual([null]);
});
scope.stop();
});
test("初始 indexes 同步更新 value", async () => {
const scope = effectScope();
await scope.run(async () => {
const { value } = usePicker({ indexes: [0, 1], columns });
await nextTick();
expect(value.value).toEqual(["pa", "cb"]);
});
scope.stop();
});
test("初始 value 同步更新 indexes", async () => {
const scope = effectScope();
await scope.run(async () => {
const { indexes } = usePicker({ value: ["pb", "ca"], columns });
await nextTick();
expect(indexes.value).toEqual([1, 0]);
});
scope.stop();
});
test("text 由 indexes 和 columns 计算得出", async () => {
const scope = effectScope();
await scope.run(async () => {
const { text } = usePicker({ indexes: [0, 1], columns, separator: "/" });
await nextTick();
expect(text.value).toBe("省A/市B");
});
scope.stop();
});
test("indexes 含 null 时 text 显示 placeholder", async () => {
const scope = effectScope();
await scope.run(async () => {
const { text } = usePicker({
indexes: [null, 1],
columns,
placeholder: "请选择地区",
});
await nextTick();
expect(text.value).toBe("请选择地区");
});
scope.stop();
});
test("handleConfirm更新 indexes", async () => {
const scope = effectScope();
await scope.run(async () => {
const { indexes, value, handleConfirm } = usePicker({ columns });
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"]);
});
scope.stop();
});
test("showPicker / hidePicker", () => {
const scope = effectScope();
scope.run(() => {
const { show, showPicker, hidePicker } = usePicker({});
showPicker();
expect(show.value).toBe(true);
hidePicker();
expect(show.value).toBe(false);
});
scope.stop();
});
test("columnValueToText根据列索引和值查找 text", () => {
const scope = effectScope();
scope.run(() => {
const { columnValueToText } = usePicker({ columns });
expect(columnValueToText("pa", 0)).toBe("省A");
expect(columnValueToText("cb", 1)).toBe("市B");
expect(columnValueToText("xx", 0)).toBeUndefined();
});
scope.stop();
});
test("valueToText返回各列 text 数组", () => {
const scope = effectScope();
scope.run(() => {
const { valueToText } = usePicker({ columns });
expect(valueToText(["pa", "cb"])).toEqual(["省A", "市B"]);
});
scope.stop();
});
test("自定义 textName 和 valueName", async () => {
const customColumns = [
[
{ label: "甲", id: 1 },
{ label: "乙", id: 2 },
],
];
const scope = effectScope();
await scope.run(async () => {
const { text } = usePicker({
columns: customColumns as any,
indexes: [0],
textName: "label",
valueName: "id",
});
await nextTick();
expect(text.value).toBe("甲");
});
scope.stop();
});
});

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts", "src/**/*.js", "types/**/*.d.ts"],
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}

View File

@ -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 {
/**
* 01使getchildren接口时
*/
result: GetCityListSuccessResultResult[];
}

View File

@ -0,0 +1,15 @@
/// <reference types="uview-plus/types" />
declare namespace UViewPlus {
type PickerColumns = never[][];
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[];
}

View File

@ -0,0 +1,48 @@
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));
export default defineConfig({
build: {
lib: {
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"],
output: {
chunkFileNames: "chunks/[name]-[hash].js",
assetFileNames: "assets/[name]-[hash][extname]",
},
},
sourcemap: true,
},
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
plugins: [
dts({
include: ["src", "types"],
outDir: "dist",
}),
],
});

View File

@ -0,0 +1,19 @@
# @r-utils/vue2
## 2.0.1
### Patch Changes
- 修复类型问题
## 2.0.0
### Major Changes
- 添加工具
## 1.4.0
### Minor Changes
- 添加工具

76
packages/vue2/README.md Normal file
View File

@ -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`

View File

@ -1,6 +1,6 @@
{
"name": "@r-utils/vue2",
"version": "1.2.1",
"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,11 +50,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": {
"vue": "^2.7.0"

View File

@ -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;

View File

@ -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: {

View File

@ -6,7 +6,6 @@
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"outDir": "./dist"
}
}
}

View File

@ -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",
}),
],
});

View File

@ -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

92
packages/vue3/README.md Normal file
View File

@ -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`

View File

@ -1,6 +1,6 @@
{
"name": "@r-utils/vue3",
"version": "1.2.1",
"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,11 +59,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"
},
"license": "ISC",
"peerDependencies": {

View File

@ -0,0 +1,120 @@
/*
* @file /src/hooks/list.ts
* @description Hook
* @author tsl (randy1924@163.com)
* @date 2026-03-19 11:11:11
* @lastModified 2026-06-30 10:57:44
* @lastModifiedBy tsl (randy1924@163.com)
*/
import { ref, computed } from "vue";
/**
*
*/
export type LoadStatus = "loadmore" | "loading" | "nomore";
/**
* Hook
* @param fetchFunction
* @param options
* @returns
*/
export function useLoadMore(
fetchFunction: (
pageNum: number,
pageSize: number,
) => Promise<{ total: number; list: unknown[] }>,
options = {
pageSize: 10,
},
) {
// 数据列表 - 确保初始化为空数组而不是undefined
const list = ref<unknown[]>([]);
// 分页和加载状态
const pageNum = ref(1);
const pageSize = ref(options.pageSize);
const status = ref<LoadStatus>("loadmore");
const total = ref(0);
const isLoading = computed(() => status.value === "loading");
const isFinished = computed(() => status.value === "nomore");
const isEmpty = computed(() => total.value === 0);
/**
*
*/
const initList = async () => {
list.value = []; // 重置为空数组
pageNum.value = 1;
status.value = "loadmore";
return loadMore();
};
/**
*
*/
const loadMore = async () => {
// 如果当前不是可加载状态,则直接返回
if (status.value !== "loadmore") {
return;
}
try {
// 先设置状态,再请求数据
status.value = "loading";
// 调用传入的获取数据函数
const response = await fetchFunction(pageNum.value, pageSize.value);
// 确保response.list是数组
const responseList = response.list || [];
// 更新数据确保list.value总是数组
if (pageNum.value === 1) {
list.value = [...responseList];
} else {
// 确保数据正确合并
list.value = [...list.value, ...responseList];
}
total.value = response.total || 0;
pageNum.value++;
// 只有当列表长度确实达到或超过总数或者当前页返回空数据时才设置为nomore
if (
list.value.length >= (response.total || 0) ||
responseList.length === 0
) {
status.value = "nomore";
} else {
status.value = "loadmore";
}
} catch (err) {
console.error("加载数据失败:", err);
// 出错时立即恢复为可加载状态
status.value = "loadmore";
}
};
/**
*
*/
const refresh = () => {
return initList();
};
return {
list,
total,
pageNum,
pageSize,
status,
isLoading,
isFinished,
isEmpty,
loadMore,
initList,
refresh,
};
}

Some files were not shown because too many files have changed in this diff Show More