feat(): 改为vite

This commit is contained in:
CodiceFabbrica 2026-03-16 17:58:57 +08:00
parent cd734d8524
commit 1ccbd4e872
74 changed files with 46574 additions and 2145 deletions

View File

@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Bash(pnpm --filter @r-utils/vue3 add vue)",
"Bash(grep -A 5 '\"\"dependencies\"\"' /d/Projects/r-util-js/packages/*/package.json)",
"Bash(grep -r \"console.log\" /d/Projects/r-util-js/packages/*/webpack.config.js)",
"Bash(grep -c \"__dirname\" /d/Projects/r-util-js/packages/*/webpack.config.js)",
"Bash(grep -r \"private.*true\" /d/Projects/r-util-js/packages/*/package.json)",
"Bash(grep \"name\" /d/Projects/r-util-js/packages/*/package.json)",
"Bash(git -C /d/Projects/r-util-js status --short)",
"Bash(pnpm install)",
"Bash(pnpm build)",
"Bash(pnpm test)",
"Bash(printf 'npm run lint-staged\\\\nnpm test\\\\n')",
"Bash(printf 'npx commitlint --edit \"\"$1\"\"\\\\n')"
]
}
}

View File

@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit "$1"

View File

@ -1,5 +1,2 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint-staged
npm test

View File

@ -1,13 +1,6 @@
# https://prettier.io/docs/en/ignore.html
node_modules/
.vscode/
.idea/
.husky/
Dockerfile
.hbuilderx/
.vite/
/dist/
/public/*
!/public/index.html

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
}

99
CLAUDE.md Normal file
View File

@ -0,0 +1,99 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个前端工具库 monorepo使用 **pnpm workspace** 管理,包含以下子包:
| 包名 | 路径 | 描述 |
|------|------|------|
| `@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
# 构建单个包(在包目录内)
cd packages/common && pnpm build
```
> `@r-utils/common` 同时有 `webpack.config.js``rollup.config.js`,当前 `build` 脚本使用 webpackrollup 配置备用。
### 测试
```bash
# 在根目录运行所有测试
pnpm test
# 运行单个测试文件
npx jest packages/common/test/permission.test.ts
```
测试文件位于各包的 `test/` 目录下,使用 Jest + Babel。
### 代码检查与格式化
```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 和 jest 测试commit-msg 钩子会用 commitlint 校验提交信息格式。
## 架构说明
### Monorepo 结构
- 根 `package.json` 定义 devDependencies构建工具、ESLint、Jest 等),各子包在自身 `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,36 +0,0 @@
# r-util-js
#### Description
js工具
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

172
README.md
View File

@ -1,37 +1,159 @@
# r-util-js
# r-utils
#### 介绍
js工具
前端工具库 monorepo使用 pnpm workspace 管理Vite 构建。
#### 软件架构
软件架构说明
## 包列表
| 包名 | 路径 | 描述 |
| ---- | ---- | ---- |
| `@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/vue2` | `packages/vue2` | Vue2 工具visibility 插件) |
#### 安装教程
## 环境要求
1. xxxx
2. xxxx
3. xxxx
- Node.js >= 18.12.0
- pnpm >= 8.15.6
#### 使用说明
## 安装依赖
1. xxxx
2. xxxx
3. xxxx
```bash
pnpm install
```
#### 参与贡献
## 构建
### 构建所有包
```bash
pnpm build
```
### 构建单个包
```bash
cd packages/common
pnpm build
```
每个包使用 **Vite lib 模式**构建,输出到 `dist/` 目录:
```text
dist/
├── index.mjs # ESM 格式
├── index.cjs # CJS 格式
├── index.d.ts # TypeScript 类型声明
├── index.mjs.map # Source Map
└── index.cjs.map
```
### 监听模式
```bash
cd packages/common
pnpm watch
```
## 使用方式
### ESM 引入(推荐)
支持 tree-shaking打包工具会自动按需引入用到的部分。
```ts
// 完整引入
import { Permission, Countdown, KnockTest } from '@r-utils/common';
// 按需引入bundler 自动 tree-shaking只打包 Permission
import { Permission } from '@r-utils/common';
// Vue3 工具
import { mergeClass, dispatch } from '@r-utils/vue3';
// uni-app 工具
import { Request } from '@r-utils/uni-app';
// Vue2 插件
import { VisibilityPlugin } from '@r-utils/vue2';
Vue.use(VisibilityPlugin);
```
### CJS 引入
```js
const { Permission } = require('@r-utils/common');
```
### package.json exports
每个包均配置了标准的 `exports` 字段,打包工具会自动选择正确的格式:
```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
```
## 开发
### 测试
```bash
# 运行所有测试
pnpm test
# 运行单个测试文件
npx jest packages/common/test/permission.test.ts
```
### 代码检查与格式化
```bash
pnpm lint
pnpm format
```
### Git 提交
```bash
# 使用 commitizen遵循 conventional commits
pnpm commit
```
提交时 husky 会自动运行 lint-staged 和 jest 测试。
## 项目结构
```text
r-util-js/
├── packages/
│ ├── common/ # @r-utils/common
│ │ ├── src/
│ │ │ ├── permission/ # Permission 权限校验
│ │ │ ├── timer/ # Countdown、TimeoutTimer
│ │ │ ├── printer/ # ESC/TSC 打印指令
│ │ │ ├── knock-test/ # KnockTest 敲击触发
│ │ │ └── time/ # 时间工具
│ │ ├── vite.config.ts
│ │ └── tsconfig.json
│ ├── vue3/ # @r-utils/vue3
│ ├── uni-app/ # @r-utils/uni-app
│ └── vue2/ # @r-utils/vue2
├── eslint.config.js # ESLint v9 flat config
├── prettier.config.js
└── pnpm-workspace.yaml
```
## 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
2. 新建 `feat/xxx` 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

65
eslint.config.js Normal file
View File

@ -0,0 +1,65 @@
import js from "@eslint/js";
import ts from "typescript-eslint";
import prettier from "eslint-config-prettier";
import globals from "globals";
export default [
// 基础配置
{
files: ["**/*.{js,mjs,cjs,ts,vue}"],
ignores: [
"**/node_modules/*",
"**/dist/*",
"build/",
"public/",
"docs/",
"bin/",
"pnpm-lock.yaml",
".vscode/",
".idea/",
".husky/",
"Dockerfile",
".hbuilderx/",
".vite/",
"uni_modules/",
"libs/",
"static/",
"patches/",
"wxcomponents/",
"lib/",
],
languageOptions: {
globals: {
...globals.node,
...globals.browser,
},
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
ecmaFeatures: {
impliedStrict: true,
},
},
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
},
// 应用推荐规则
js.configs.recommended,
...ts.configs.recommended,
prettier,
// 测试文件覆盖配置
{
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
languageOptions: {
globals: {
...globals.mocha,
...globals.jest,
},
},
},
];

10730
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,79 @@
{
"name": "r-util-js",
"name": "@r-utils/root",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"description": "前端工具库",
"keywords": [
"vue",
"js",
"utils",
"tools"
],
"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"
},
"license": "ISC",
"engines": {
"node": ">=18.12.0"
},
"packageManager": "pnpm@8.15.6",
"type": "module",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "webpack",
"watch": "webpack watch --mode development",
"start": "webpack --mode development && node ./dist/index.js",
"build": "pnpm -r run build",
"prepare": "husky install",
"lint": "eslint --ext .js,ts --fix src",
"format": "prettier --write src",
"release": "standard-version",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21",
"webpack": "^5.80.0",
"webpack-cli": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@babel/preset-env": "^7.12.0",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/config-conventional": "^17.6.1",
"@commitlint/cz-commitlint": "^17.5.0",
"@commitlint/config-conventional": "^19.8.1",
"@commitlint/cz-commitlint": "^19.8.1",
"@eslint/js": "^9.26.0",
"@jest/globals": "^29.5.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"babel-jest": "^29.5.0",
"commitizen": "^4.3.0",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.3",
"eslint-define-config": "^1.18.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"inquirer": "^8.0.0",
"eslint-plugin-prettier": "^5.4.0",
"globals": "^16.1.0",
"husky": "^9.1.7",
"inquirer": "^12.6.0",
"jest": "^29.5.0",
"lint-staged": "^13.2.1",
"prettier": "^2.8.7",
"lint-staged": "^15.5.2",
"prettier": "^3.5.3",
"standard-version": "^9.5.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4"
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0",
"vite": "^6.0.0",
"vite-plugin-dts": "^4.0.0"
},
"overrides": {
"@types/lodash": "4.17.16",
"dayjs": "1.11.13",
"lodash": "4.17.21",
"lodash-es": "4.17.21"
}
}

View File

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

1372
packages/common/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "@r-utils/common",
"version": "1.0.0",
"private": true,
"description": "js通用工具库",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./*": "./*"
},
"keywords": [
"utils",
"common"
],
"scripts": {
"build": "vite build",
"watch": "vite build --watch",
"lint": "eslint --ext .js,ts --fix src",
"format": "prettier --write src",
"release": "standard-version",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "jest"
},
"dependencies": {
"dayjs": "^1.11.13",
"lodash-es": "^4.17.21",
"text-encoding": "^0.7.0"
},
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/lodash-es": "^4.17.12"
}
}

View File

@ -0,0 +1,5 @@
export * from "./knock-test";
export * from "./permission";
export * from "./printer";
export * from "./time";
export * from "./timer";

View File

@ -0,0 +1,138 @@
import { debounce } from "lodash-es";
export class Operation {
duration = 1000;
delay = 1000;
times = 1;
}
export class Config {
maxWaitTime = 5000;
operations: Operation[] = [];
}
export class KnockTest {
config: Config;
index = 0;
times = 0;
/**
* "idle"times从0开始计数duration期
* duration期间"knocking"times达到Operation的指定次数后进入delay期
* delay期间"wait"
*/
status: "idle" | "knocking" | "wait" = "idle";
durationTimerId: number | null = null;
delayTimerId: number | null = null;
callbackList: (() => void)[] = [];
maxWaitFun: () => void;
constructor(config: Config) {
const c = new Config();
this.config = Object.assign(c, config);
this.maxWaitFun = debounce(() => {
console.log("maxWaitFun");
this.reset();
}, this.config.maxWaitTime);
}
addCallback(cb: () => void) {
this.callbackList.push(cb);
}
knock() {
console.log("knock");
this.maxWaitFun();
this.times++;
switch (this.status) {
case "idle":
this.handleIdle();
break;
case "knocking":
this.handleKnocking();
break;
case "wait":
this.handleWait();
break;
default:
break;
}
}
private reset() {
this.status = "idle";
this.times = 0;
this.index = 0;
if (this.durationTimerId != null) {
clearTimeout(this.durationTimerId);
this.durationTimerId = null;
}
if (this.delayTimerId != null) {
clearTimeout(this.delayTimerId);
}
}
private handleIdle() {
if (this.config.operations.length === 0) {
throw new Error("至少添加一个操作项");
}
const operation = this.config.operations[this.index];
this.status = "knocking";
this.durationTimerId = window.setTimeout(() => {
if (operation.times !== this.times) {
this.reset();
}
}, operation.duration);
this.checkKnock();
}
private handleKnocking() {
this.checkKnock();
}
private checkKnock() {
const operation = this.config.operations[this.index];
if (operation.times === this.times) {
if (this.durationTimerId != null) {
clearTimeout(this.durationTimerId);
this.durationTimerId = null;
}
this.status = "wait";
this.times = 0;
this.index++;
this.delayTimerId = window.setTimeout(() => {
this.status = "idle";
this.times = 0;
if (this.delayTimerId != null) {
clearTimeout(this.delayTimerId);
this.delayTimerId = null;
}
}, operation.delay);
if (this.index > this.config.operations.length - 1) {
console.log("成功");
this.reset();
this.callbackList.forEach((cb) => cb());
}
}
}
private handleWait() {
if (this.delayTimerId != null) {
clearTimeout(this.delayTimerId);
this.delayTimerId = null;
}
this.reset();
}
}

View File

@ -0,0 +1 @@
export * from "./KnockTest";

View File

@ -13,7 +13,7 @@ export class Permission {
}
isValid(str: string) {
const p = new Array(this.level).fill("\\w+?").join(":");
const p = new Array(this.level).fill("\\w+?").join(this.separator);
// ^\w+?:\w+?:\w+?$
const r = new RegExp(`^${p}$`);
return r.test(str);

View File

@ -0,0 +1,49 @@
import { JpPrinter } from "./esc.js";
export class PrintUtil extends JpPrinter {
/**
*
* @param title
*/
addTitle(title: string) {
return this.storeLayout()
.setAlign("m")
.setLineSpacing(7.5 * 8 * 2)
.setFontSize(0x10 + 0x1)
.addText(title)
.addLF()
.restoreLayout();
}
/**
*
* @param text
* @param align
* @returns
*/
addTextLine(text: string, align = "l") {
return this.storeLayout()
.setAlign(align)
.setFontSize(0x00 + 0x00)
.addText(text)
.addLF()
.restoreLayout();
}
/**
*
* @param label
* @param value
* @returns
*/
addItem(label: string, value: string) {
return this.storeLayout()
.setAlign("l")
.setFontSize(0x00 + 0x00)
.addText(label)
.addText("")
.addText(value)
.addLF()
.restoreLayout();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
export * from "./esc";
export * from "./esc-plus";
export * from "./tsc-plus";
export * from "./tsc";

View File

@ -0,0 +1,307 @@
import { TSC } from "./tsc.js";
const fontInfoList = [
{
name: "1",
width: 8,
height: 12,
},
{
name: "2",
width: 12,
height: 20,
},
{
name: "3",
width: 16,
height: 24,
},
{
name: "4",
width: 24,
height: 32,
},
{
name: "5",
width: 32,
height: 48,
},
{
name: "6",
width: 14,
height: 19,
},
{
name: "7",
width: 21,
height: 27,
},
{
name: "8",
width: 14,
height: 25,
},
{
name: "9",
width: 9,
height: 17,
},
{
name: "10",
width: 12,
height: 24,
},
{
name: "TSS16.BF2",
width: 16,
height: 16,
},
{
name: "TSS20.BF2",
width: 20,
height: 20,
},
{
name: "TST24.BF2",
width: 24,
height: 24,
},
{
name: "TSS24.BF2",
width: 24,
height: 24,
},
{
name: "K",
width: 24,
height: 24,
},
{
name: "TSS32.BF2",
width: 32,
height: 32,
},
];
export class TSCPlus extends TSC {
static MILLIMETERS_PER_INCH = 25.4;
constructor(data = []) {
super(data);
this.font = "TSS24.BF2";
this.lineHeight = "1.2";
this.dpi = 200;
this.nextX = 0;
this.nextY = 0;
this.widthDot = this.mmToDot(40);
this.heightDot = this.mmToDot(30);
this.paddingTopDot = 0;
this.paddingLeftDot = 0;
this.paddingRightDot = 0;
this.paddingBottomDot = 0;
this.setSizeDot(this.widthDot, this.heightDot);
}
/**
* 设置内边距
* @param {number} paddingTopDot
* @param {number} paddingRightDot
* @param {number} paddingBottomDot
* @param {number} paddingLeftDot
*/
setPadding(paddingTopDot, paddingRightDot, paddingBottomDot, paddingLeftDot) {
this.paddingTopDot = paddingTopDot;
this.paddingRightDot = paddingRightDot;
this.paddingBottomDot = paddingBottomDot;
this.paddingLeftDot = paddingLeftDot;
this.nextX = this.paddingLeftDot;
this.nextY = this.paddingTopDot;
}
mmToDot(mm) {
mm = Number(mm);
return Math.round(this.dpi / TSCPlus.MILLIMETERS_PER_INCH) * mm;
}
setDpi(dpi) {
this.dpi = dpi;
}
setSize(w, h) {
if (/^\d+(\.\d+)? mm$/.test(w)) {
this.widthDot = this.mmToDot(parseInt(w));
this.heightDot = this.mmToDot(parseInt(h));
} else {
this.widthDot = parseInt(w) * this.dpi;
this.heightDot = parseInt(h) * this.dpi;
}
return super.setSize(w, h);
}
setSizeMM(w, h) {
this.widthDot = w * 8;
this.heightDot = h * 8;
return super.setSizeMM(w, h);
}
setSizeDot(w, h) {
return this.setSizeMM(w / 8, h / 8);
}
/**
* 设置字体
* @param {string} font
*/
setFont(font) {
this.font = String(font);
}
/**
* 设置行高
* @param {string|number} lineHeight
*/
setLineHeight(lineHeight) {
this.lineHeight = String(lineHeight);
}
static getLetterWidthDot(font) {
const fontInfo = fontInfoList.find((v) => v.name === font);
return fontInfo.width;
}
static getFontLineHeightDot(font, lineHeight) {
const fontInfo = fontInfoList.find((v) => v.name === font);
if (/^\d+(\.\d+)?dot$/.test(lineHeight)) {
const [, lineHeightDot] = /^(\d+(\.\d+))?dot$/.exec(lineHeight);
return lineHeightDot;
} else if (/^\d+(\.\d+)?$/.test(lineHeight)) {
const [, lineHeightNum] = /^(\d+(\.\d+))?$/.exec(lineHeight);
const lineHeightDot = fontInfo.height * lineHeightNum;
return lineHeightDot;
} else {
throw new Error("无效行高:" + lineHeight);
}
}
/**
* 获取文本宽度点数
* @param {string} text
* @returns {number}
*/
static getTextDots(text, letterDots = 12) {
text = String(text);
let ch = 0;
text.split("").forEach((c) => {
// 是否是汉字,汉字两倍宽
const isChinese = /[^\x00-\xff]/.test(c);
if (isChinese) {
ch += 2;
} else {
ch++;
}
});
return ch * letterDots;
}
/**
* 根据最大点数获取文本
* @param {string} text
* @param {number} maxDots
* @return {string}
*/
static getMaxText(text = "", maxDots = 0) {
let subStr = "";
for (let i = 0; i < text.length; i++) {
subStr = text.substring(0, text.length - i);
if (TSCPlus.getTextDots(subStr) <= maxDots) {
break;
}
}
return subStr;
}
/**
* 分割字符串
* "123456789" => ["1234", "5678", "9"]
* @param {string} str
* @param {number} maxDots
* @return {string[]}
*/
static splitString(str, maxDots) {
let itemDots = TSCPlus.getTextDots(str);
const arr = [];
while (itemDots > 0) {
const maxStr = TSCPlus.getMaxText(str, maxDots);
arr.push(maxStr);
str = str.slice(maxStr.length);
itemDots = TSCPlus.getTextDots(str);
}
if (str.length > 0) {
arr.push(str);
}
return arr;
}
addTextLn(text, options = {}) {
text = String(text);
const font = String(options.font ?? this.font);
let x = parseInt(options.x ?? this.nextX);
this.nextY = parseInt(options.y ?? this.nextY);
const rotation = parseInt(options.rotation ?? 0);
const sx = parseInt(options.sx ?? 1);
const sy = parseInt(options.sy ?? 1);
const align = options.align ?? "left";
const contentWidth =
this.widthDot - this.paddingLeftDot - this.paddingRightDot;
const textList = TSCPlus.splitString(text, contentWidth);
textList.forEach((t) => {
const textDots = parseInt(TSCPlus.getTextDots(t) * sx);
console.log(align, textDots, contentWidth);
if (textDots < contentWidth) {
if (align == "center") {
x += parseInt((contentWidth - textDots) / 2);
} else if (align == "right") {
x += parseInt(contentWidth - textDots);
}
}
console.log("x", x);
this.setText(x, this.nextY, font, rotation, sx, sy, t);
const lineHeightDot =
TSCPlus.getFontLineHeightDot(font, this.lineHeight) * sy;
this.nextY = parseInt(this.nextY + lineHeightDot);
console.log("this.nextY", this.nextY);
});
return this;
}
addAddBarCode(content, options = {}) {
const x = parseInt(options.x ?? this.nextX);
this.nextY = parseInt(options.y ?? this.nextY);
const height = parseInt(options.height ?? 80);
const rotation = parseInt(options.rotation ?? 0);
this.setBarCode(
x,
this.nextY,
options.codeType,
height,
options.readable,
rotation,
options.narrow,
options.wide,
content,
);
this.nextY += height + 24;
return this;
}
}

View File

@ -0,0 +1,616 @@
import * as encode from "text-encoding";
export class TSC {
static LINE_BREAK = "\r\n";
constructor(data = []) {
// 打印数据
this.data = data;
}
addData(...data) {
this.data.push(...data);
}
addDataArray(dataArray) {
this.data.push(...dataArray);
}
addCode(code) {
const textEncoder = new encode.TextEncoder("gb18030", {
NONSTANDARD_allowLegacyEncoding: true,
});
const data = textEncoder.encode(code);
this.data.push(...data);
return this;
}
getData = function () {
return this.data;
};
// 系统设定指令====================
/**
* 该指令用于设定卷标纸的宽度和长度
* @param {number|string} w 标签宽度 单位英寸inch
* @param {number|string} h 标签高度 单位英寸inch
* @returns {TSC}
*/
setSize(w, h) {
const code = `SIZE ${w},${h}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于设定卷标纸的宽度和长度
* @param {number|string} w 标签宽度 单位毫米mm
* @param {number|string} h 标签高度 单位毫米mm
* @returns {TSC}
*/
setSizeMM(w, h) {
return this.setSize(`${w} mm`, `${h} mm`);
}
/**
* 该指令用于定义两张卷标纸间的垂直间距距离
* @param {number|string} m 两标签纸中间的垂直距离 单位英寸inch
* @param {number|string} n 垂直间距偏移 单位英寸inch
* @returns {TSC}
*/
setGap(m = 0, n = 0) {
const code = `GAP ${m},${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于定义两张卷标纸间的垂直间距距离
* @param {number|string} m 两标签纸中间的垂直距离 单位毫米mm
* @param {number|string} n 垂直间距偏移 单位毫米mm
* @returns {TSC}
*/
setGapMM(m = 0, n = 0) {
const code = `GAP ${m} mm,${n} mm${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于设定黑标高度及定义标签印完后标签额外送出的长度
* @param {number|string} m 黑标高度 单位英寸inch
* @param {number|string} n 额外送出纸张长度 单位英寸inch
* @returns {TSC}
*/
setBLine(m = 0, n = 0) {
const code = `BLINE ${m},${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于控制在剥离模式时peel-off mode每张卷标停止的位置
* 在打印下一张时打印机会将原先多推出或少推出的部分以回拉方式补偿回来
* 该指令仅适用于剥离模式
* @param {number|string} m 纸张停止的距离 单位英寸inch
* @returns {TSC}
*/
setOffset(offset = 0) {
const code = `OFFSET ${offset}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于控制打印速度
* @param {number|string} speed 打印速度
* @returns {TSC}
*/
setSpeed(speed = 1.5) {
const code = `SPEED ${speed}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于控制打印时的浓度
* @param {number|string} density 打印浓度
* @returns {TSC}
*/
setDensity(density = 10) {
const code = `DENSITY ${density}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于定义打印时出纸和打印字体的方向
* @param {number|string} direction
* @returns {TSC}
*/
setDirection(direction = 0) {
const code = `DIRECTION ${direction}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于定义卷标的参考坐标原点坐标原点位置和打印方向有关
* @param {number|string} x 水平方向的坐标位置,单位dot
* @param {number|string} y 垂直方向的坐标位置,单位dot
* @returns {TSC}
*/
setReference(x = 0, y = 0) {
const code = `REFERENCE ${x},${y}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令表示标签打印偏移量多少设置
* @param {number|string} n 打印偏移量
* @returns {TSC}
*/
setShift(n = 0) {
const code = `SHIFT ${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于选择对应的国际字符集
* 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 = 0) {
const code = `COUNTRY ${country}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于选择对应的国际代码页
* 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 = 0) {
const code = `CODEPAGE ${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于清除图像缓冲区image buffer)的数据
* 此项指令必须置于 SIZE 指令之后
* @param {number|string} n 代码页
* @returns {TSC}
*/
setCls() {
const code = `CLS${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于将标签纸向前推送指定的长度
* 打印机分辨率200 DPI:1 mm = 8 dots
* 打印机分辨率300 DPI:1 mm = 12 dots
* @param {number|string} n 1n9999单位dot
* @returns {TSC}
*/
setFeed(n) {
const code = `FEED ${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于将标签纸向前推送指定的长度
* 打印机分辨率200 DPI:1 mm = 8 dots
* 打印机分辨率300 DPI:1 mm = 12 dots
* @param {number|string} n 1n9999单位dot
* @returns {TSC}
*/
setBackFeed(n) {
const code = `BACKFEED ${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于将标签纸向前推送指定的长度
* 打印机分辨率200 DPI:1 mm = 8 dots
* 打印机分辨率300 DPI:1 mm = 12 dots
* @param {number|string} n 1n9999单位dot
* @returns {TSC}
*/
setBackUp(n) {
const code = `BACKUP ${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于控制打印机进一张标签纸
* @returns {TSC}
*/
setFromFeed() {
const code = `FORMFEED${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 在使用含有间隙或黑标的标签纸时
* 若不能确定第一张标签纸是否在正确打印位置时
* 此指令可将标签纸向前推送至下一张标签纸的起点开始打印
* 标签尺寸和间隙需要在本条指令前设置
* 使用该指令时纸张高度大于或等于30 mm
* @returns {TSC}
*/
setHome() {
const code = `HOME${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于打印出存储于影像缓冲区内的数据
* @param {number|string} m 指定打印的份数set
* @param {number|string} n 每张标签需重复打印的张数
* @returns {TSC}
*/
setPrint(m = 1, n = 1) {
const code = `PRINT ${m},${n}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于控制蜂鸣器的频率可设定10阶的声音
* 频率可设定10阶的声音每阶声音的长短由第二个参数控制
* @param {number|string} m 指定打印的份数set
* @param {number|string} n 每张标签需重复打印的张数
* @returns {TSC}
*/
setSound(level = 1, interval = 300) {
const code = `SOUND ${level},${interval}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于设定打印机进纸时若经过所设定的长度仍无法侦测到垂直间距
* 则打印机在连续纸模式工作
* @param {number|string} limit 英制系统inch
* @returns {TSC}
*/
setLimitFeed(limit = 1) {
const code = `LIMITFEED ${limit}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 不经自测动作直接打印自检页信息
* @returns {TSC}
*/
setSelfTest() {
const code = `SELFTEST${TSC.LINE_BREAK}`;
return this.addCode(code);
}
// 卷标内容设计指令==============================
/**
* 该指令用于在标签上画线
* @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, y, width, height) {
const code = `BAR ${x},${y},${width},${height}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于在标签上画线
* @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,
y,
codeType = "128",
height = 80,
readable = 1,
rotation = 0,
narrow = 2,
wide = 2,
content,
) {
x = parseInt(x);
y = parseInt(y);
height = parseInt(height);
readable = parseInt(readable);
rotation = parseInt(rotation);
narrow = parseInt(narrow);
wide = parseInt(wide);
content = String(content);
const code = `BARCODE ${x},${y},"${codeType}",${height},${readable},${rotation},${narrow},${wide},"${content}"${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于在卷标上绘制矩形方框
* @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, y1, x2, y2, thickness) {
const code = `BOX ${x1},${y1},${x2},${y2},${thickness}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于在卷标上绘制位图非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, y, width, height, mode, bitmapData) {
const w = width;
const h = height;
const bitw = parseInt((w + 7) / 8) * 8;
// var bitw = (parseInt(w) % 8) == 0 ? (parseInt(w) / 8) :( parseInt(w) / 8+1);
const pitch = parseInt(bitw / 8);
const bits = new Uint8Array(h * pitch);
const code = `BITMAP "${x},${y},${bitw},${pitch},${h},${mode},`;
this.addCode(code);
// for (var i=0; i<bits.length; i++) {
// bits[i] = 0;
// }
for (y = 0; y < h; y++) {
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;
}
}
}
for (var i = 0; i < bits.length; i++) {
this.addData(~bits[i] & 0xff);
}
return this;
}
/**
* 该指令用于清除影像缓冲区部分区域的数据
* @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, y, width, height) {
const code = `ERASE ${x},${y},${width},${height}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 将指定的区域反相打印
* @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, y, width, height) {
const code = `REVERSE ${x},${y},${width},${height}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用于打印字符串
* @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, y, font, rotation, sx, sy, content) {
x = parseInt(x);
y = parseInt(y);
rotation = parseInt(rotation);
sx = parseInt(sx);
sy = parseInt(sy);
content = String(content);
const code = `TEXT ${x},${y},"${font}",${rotation},${sx},${sy},"${content}"${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用来打印二维码
* @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, y, level, width, mode, rotation, content) {
const code = `QRCODE ${x},${y},${level},${width},${mode},${rotation},"${content}"${TSC.LINE_BREAK}`;
return this.addCode(code);
}
// 询问打印机状态指令=========================
// 打印机外围功能设定指令=========================
/**
* 该指令用来起动Key1 的预设功能
* 预设为进纸功能
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setKey1(k) {
const code = `SET KYE1 ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用来起动Key2 的预设功能
* 预设为暂停功能
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setKey2(k) {
const code = `SET KYE2 ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 该指令用来启动/关闭剥离模式默认值为关闭
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setPeel(k) {
const code = `SET PEEL ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此命令是用来启用/禁用撕纸位置走到撕纸处此设置关掉电源后将保存在打印机内
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setTear(k) {
const code = `SET TEAR ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此命令是用来启用/禁用撕纸位置走到撕纸处此设置关掉电源后将保存在打印机内
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setStripper(k) {
const code = `SET STRIPPER ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此设置用于启用/禁用打印头合盖传感器如果禁用合盖传感器打印机头被打开时将不会传回错误信息
* 此设置将保存在打印机内存
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setHead(k) {
const code = `SET HEAD ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此设置用于启用/禁用打印头合盖传感器如果禁用合盖传感器打印机头被打开时将不会传回错误信息
* 此设置将保存在打印机内存
* @param {"ON"|"OFF"|"AUTO"|string|number} k 开启按键/关闭按键
* @returns {TSC}
*/
setHead2(k) {
const code = `SET PRINTKEY ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此命令将禁用/启用标签机在无纸或开盖错误发生后
* 上纸或合盖后重新打印一次标签内容
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setReprint(k) {
const code = `SET REPRINT ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 设定开启/关闭碳带感应器即切换热转式/热感印式打印通常打印机于开启电源时
* 碳带感应器即会自动检测打印机是否已装上碳带并藉此决定使用热感式或热转式打印
* 此项设定并不会存于打印机中此方法仅适用于热转式机器
* @param {"ON"|"OFF"} k 开启按键/关闭按键
* @returns {TSC}
*/
setRibbon(k) {
const code = `SET RIBBON ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此命令用于设置切刀状态关闭打印机电源后该设置将会被存储在打印机内存中
* @param {"OFF"|"BATCH"|string|number} k 开启按键/关闭按键
* @returns {TSC}
*/
setCutter(k) {
const code = `SET CUTTER ${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
/**
* 此指令用于设置打印机自动返回状态
* @param {"ON"|"OFF"|"BATCH"} k 开启按键/关闭按键
* @param {string} content
* @returns {TSC}
*/
setResponse(k, content) {
const code = `SET RESPONSE "${content}",${k}${TSC.LINE_BREAK}`;
return this.addCode(code);
}
}

View File

@ -0,0 +1,16 @@
/**
*
* @param timeout ms
*/
export function wait(timeout: number): Promise<void> {
return new Promise((resolve, reject) => {
try {
const id = setTimeout(() => {
resolve();
clearTimeout(id);
}, timeout);
} catch (error) {
reject(error);
}
});
}

View File

@ -0,0 +1,86 @@
import dayjs from "dayjs";
import { Duration } from "dayjs/plugin/duration";
type TimerCallback = (time: string) => void;
export default class Countdown {
id = -1;
duration: Duration;
format = "HH:mm:ss";
/**
*
*/
stepEventListenerList = <TimerCallback[]>[];
/**
*
*/
countdownEventListenerList = <TimerCallback[]>[];
initialTime = 0;
constructor(time: number) {
this.initialTime = time;
this.duration = dayjs.duration(time);
}
/**
*
* @param cb
* @returns
*/
addStepEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
return new TypeError("回调函数类型错误: cb: " + cb);
}
this.stepEventListenerList.push(cb);
}
/**
*
* @param cb
* @returns
*/
addCountdownEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
return new TypeError("回调函数类型错误: cb: " + cb);
}
this.countdownEventListenerList.push(cb);
}
/**
*
*/
start() {
this.id = window.setInterval(() => {
this.duration = this.duration.subtract(1000);
const str = this.duration.format(this.format);
this.stepEventListenerList.forEach((cb) => cb(str));
if (this.duration.asMilliseconds() <= 0) {
this.stop();
}
}, 1000);
}
/**
*
*/
stop() {
console.log("stop", this.id);
const str = this.duration.format(this.format);
clearInterval(this.id);
this.id = -1;
this.countdownEventListenerList.forEach((cb) => cb(str));
}
/**
*
*/
restart() {
clearInterval(this.id);
this.id = -1;
this.duration = dayjs.duration(this.initialTime);
this.start();
}
}

View File

@ -0,0 +1,101 @@
type TimerCallback = (time: number) => void;
export default class TimeoutTimer {
startTime = 0;
endTime = 0;
interval = 0;
time = 0;
id = -1;
stepEventListenerList = <TimerCallback[]>[];
countdownEventListenerList = <TimerCallback[]>[];
/**
*
* @param startTime
* @param endTime
* @param interval (ms)
*/
constructor(startTime = 5, endTime = 0, interval = 1000) {
this.startTime = startTime;
this.endTime = endTime;
this.interval = interval;
}
/**
*
* @param cb
* @returns
*/
addStepEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
return new TypeError("回调函数类型错误: cb: " + cb);
}
this.stepEventListenerList.push(cb);
}
/**
*
* @param cb
* @returns
*/
addCountdownEventListener(cb: TimerCallback) {
if (!(cb instanceof Function)) {
return new TypeError("回调函数类型错误: cb: " + cb);
}
this.countdownEventListenerList.push(cb);
}
/**
*
* @param startTime
* @param endTime
* @param interval
* @param stepEventListener
* @param countdownEventListener
* @returns
*/
static start(
startTime: number,
endTime: number = 0,
interval = 1000,
stepEventListener?: TimerCallback,
countdownEventListener?: TimerCallback,
) {
const timer = new TimeoutTimer(startTime, endTime, interval);
if (stepEventListener != null) {
timer.addStepEventListener(stepEventListener);
}
if (countdownEventListener != null) {
timer.addCountdownEventListener(countdownEventListener);
}
return timer;
}
/**
*
*/
start() {
this.time = this.startTime;
this.id = window.setInterval(() => {
this.time -= this.interval;
console.log(this.time);
this.stepEventListenerList.forEach((cb) => cb(this.time));
if (this.time <= this.endTime && this.id !== -1) {
this.stop();
}
}, this.interval);
}
/**
*
*/
stop() {
console.log("stop", this.id);
clearInterval(this.id);
this.id = -1;
this.time = this.endTime;
this.countdownEventListenerList.forEach((cb) => cb(this.time));
}
}

View File

@ -0,0 +1,2 @@
export { default as Countdown } from "./Countdown";
export { default as TimeoutTimer } from "./TimeoutTimer";

View File

@ -1,5 +1,5 @@
import { test, expect } from "@jest/globals";
import { Permission } from "../src/utils/permission";
import { Permission } from "../src/permission";
test("测试 Permission#isValid()", () => {
const permission = new Permission([]);

View File

@ -0,0 +1,23 @@
{
"exclude": ["node_modules", "dist"],
"include": ["**/*.ts"],
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["dom", "esnext"],
// "types": ["@dcloudio/types"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"allowJs": true,
"outDir": "./types"
}
}

View File

@ -0,0 +1,31 @@
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: resolve(__dirname, 'src/index.ts'),
formats: ['es', 'cjs'],
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
external: ['dayjs', 'lodash-es', 'text-encoding', 'tslib'],
},
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
plugins: [
dts({
include: ['src'],
outDir: 'dist',
}),
],
});

10515
packages/uni-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "@r-utils/uni-app",
"version": "1.0.0",
"private": true,
"description": "uni-app工具库",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./*": "./*"
},
"keywords": [
"uni-app"
],
"scripts": {
"build": "vite build",
"watch": "vite build --watch",
"lint": "eslint --ext .js,ts --fix src",
"release": "standard-version",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "jest"
},
"dependencies": {
"@r-utils/common": "workspace:^",
"lodash": "^4.17.21"
},
"devDependencies": {
"@dcloudio/types": "^3.0.7",
"vue": "^3.3.0"
}
}

View File

@ -0,0 +1,461 @@
import { toPromise, showToast } from "@/uni-helper";
const systemInfo = uni.getSystemInfoSync();
const isAndroidApp =
systemInfo.uniPlatform === "app" && systemInfo.osName === "android";
// #ifdef APP-PLUS
const BluetoothAdapter = plus.android.importClass(
"android.bluetooth.BluetoothAdapter",
) as Android.BluetoothAdapter;
const UUID = plus.android.importClass("java.util.UUID") as Android.UUID;
const Context = plus.android.importClass("android.content.Context");
const Intent = plus.android.importClass("android.content.Intent");
const IntentFilter = plus.android.importClass("android.content.IntentFilter");
const InputStream = plus.android.importClass("java.io.InputStream");
const OutputStream = plus.android.importClass("java.io.OutputStream");
const Activity = plus.android.importClass("android.app.Activity");
const BluetoothSocket = plus.android.importClass(
"android.bluetooth.BluetoothSocket",
);
const BluetoothDevice = plus.android.importClass(
"android.bluetooth.BluetoothDevice",
) as Android.BluetoothDevice;
const invoke = plus.android.invoke;
// #endif
export interface Device extends UniNamespace.BluetoothDeviceInfo {
// 通知服务ID
notifyServiceId?: string;
// 通知特征值ID
notifyCharacterId?: string;
// 写入服务ID
writeServiceId?: string;
// 写入特征值ID
writeCharacterId?: string;
// 读取服务ID
readServiceId?: string;
// 读取特征值ID
readCharacterId?: string;
}
export class BluetoothError extends Error {
constructor(massage?: string) {
super(massage);
this.name = "BluetoothError";
}
}
export class BluetoothUtils {
static async safeInit(cb: () => Promise<unknown>) {
try {
console.log("uni.openBluetoothAdapter");
await uni.openBluetoothAdapter();
return await cb();
} catch (error) {
if (error instanceof BluetoothError) {
showToast(error.message, "error");
} else {
showToast("初始化蓝牙失败", "error");
}
throw error;
} finally {
console.log("uni.closeBluetoothAdapter");
await uni.closeBluetoothAdapter();
}
}
/**
*
* @param cb
* @param time
* @returns
*/
static async getDevices(cb: (d: Device[]) => void, time: number) {
return BluetoothUtils.safeInit(() => BluetoothUtils._getDevices(cb, time));
}
private static async _getDevices(cb: (d: Device[]) => void, time = 3000) {
console.log("BluetoothUtils._getDevices()");
const foundDevices = <Device[]>[];
let btFindReceiver = null;
let activity = null;
let btAdapter: Android.BluetoothAdapter = <Android.BluetoothAdapter>{};
if (isAndroidApp) {
btAdapter = BluetoothAdapter.getDefaultAdapter();
}
try {
if (isAndroidApp) {
activity = plus.android.runtimeMainActivity() as Android.Activity;
console.log({ activity, btAdapter });
if (btAdapter.isDiscovering()) {
btAdapter.cancelDiscovery();
}
btFindReceiver = plus.android.implements(
"io.dcloud.android.content.BroadcastReceiver",
{
onReceive: function (
context: Android.Context,
intent: Android.Intent,
) {
// plus.android.importClass(context);
// plus.android.importClass(intent);
const action = intent.getAction();
console.log("onReceive");
if (BluetoothDevice.ACTION_FOUND === action) {
// 找到设备
const device =
intent.getParcelableExtra<Android.BluetoothDevice>(
BluetoothDevice.EXTRA_DEVICE,
);
const newDevice = {
name: device.getName(),
deviceId: device.getAddress(),
RSSI: -1,
advertisData: [],
advertisServiceUUIDs: [],
localName: "",
serviceData: [],
};
const repetition = foundDevices.some(
(v) => v.deviceId === newDevice.deviceId,
);
console.log({ newDevice });
console.log({ repetition });
if (
!repetition &&
newDevice.name &&
newDevice.name !== "未知设备"
) {
cb([newDevice]);
foundDevices.push(newDevice);
}
}
// if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
// // 搜索完成
// }
},
},
);
const filter = plus.android.newObject(
"android.content.IntentFilter",
) as Android.IntentFilter;
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
activity.registerReceiver(btFindReceiver, filter);
btAdapter.startDiscovery();
} else {
const bluetoothAdapterState = await uni.getBluetoothAdapterState();
console.log(bluetoothAdapterState);
if (!bluetoothAdapterState.available) {
throw new BluetoothError("蓝牙适配器不可用");
}
if (bluetoothAdapterState.discovering) {
const res = await uni.stopBluetoothDevicesDiscovery();
console.log(res);
}
const sbddRes = await uni.startBluetoothDevicesDiscovery();
console.log("startBluetoothDevicesDiscovery", sbddRes);
// 蓝牙设备监听 plus.bluetooth.onBluetoothDeviceFound
uni.onBluetoothDeviceFound((result) => {
// console.log("uni.onBluetoothDeviceFound", result);
const newDevices = result.devices.filter((v) => {
// console.log(foundDevices, v);
return (
v.name &&
v.name !== "未知设备" &&
!foundDevices.some((v2) => v2.deviceId === v.deviceId)
);
});
if (newDevices.length > 0) {
cb(newDevices);
foundDevices.push(...newDevices);
}
});
}
return await new Promise((resolve) => {
setTimeout(async () => {
if (isAndroidApp) {
btAdapter.cancelDiscovery();
}
console.log({ foundDevices });
resolve(foundDevices);
}, time);
});
} catch (error) {
console.error(error);
if (error instanceof BluetoothError) {
showToast(error.message, "error");
} else {
showToast("获取蓝牙设备失败", "error");
}
return Promise.reject(error);
} finally {
if (isAndroidApp) {
if (activity) {
if (btFindReceiver != null) {
activity.unregisterReceiver(btFindReceiver);
}
activity = null;
}
} else {
await uni.stopBluetoothDevicesDiscovery();
}
}
}
/**
*
* @param device
* @returns
*/
static async bindDevice(device: Device, cb: () => Promise<unknown>) {
return BluetoothUtils.safeInit(() =>
BluetoothUtils._bindDevice(device, cb),
);
}
private static async _bindDevice(device: Device, cb: () => Promise<unknown>) {
console.log("_bindDevice", device);
try {
let res = null;
if (isAndroidApp) {
res = await cb();
} else {
// 失败重连5次
for (var i = 1; i <= 5; i++) {
try {
await uni.createBLEConnection({
deviceId: device.deviceId,
});
break;
} catch (error) {
console.log(`蓝牙失败重连${i}`);
if (i == 5) {
throw error;
}
}
}
res = await new Promise((resolve) => {
setTimeout(async () => {
const services = await BluetoothUtils._getServices(device);
console.log("services", services);
await BluetoothUtils._setCharacteristics(device, services);
resolve(await cb());
// await cb()
}, 1000);
});
}
return res;
} catch (error) {
showToast("绑定蓝牙设备失败", "error");
throw error;
} finally {
if (!isAndroidApp) {
await uni.closeBLEConnection({
deviceId: device.deviceId,
});
}
}
}
/**
*
* @param device
* @returns
*/
private static async _getServices(device: Device) {
console.log("获取蓝牙设备所有服务");
// const res = await new Promise<UniNamespace.GetBLEDeviceServicesSuccess>(
// (resolve, reject) => {
// logger.info("uni.getBLEDeviceServices", device.deviceId);
// uni.getBLEDeviceServices({
// deviceId: device.deviceId,
// success: (res) => {
// console.log(res);
// logger.info("res: ", res);
// resolve(res);
// },
// fail: (err) => {
// console.error(err);
// logger.info("err: ", err);
// reject(err);
// },
// });
// }
// );
const res = await uni.getBLEDeviceServices({
deviceId: device.deviceId,
});
console.log(res);
return res.services;
}
/**
*
*/
private static async _setCharacteristics(
device: Device,
services: UniNamespace.GetBLEDeviceServicesSuccessData[],
) {
console.log("_setCharacteristics", services);
// 获取蓝牙设备某个服务中所有特征值
// plus.bluetooth.getBLEDeviceCharacteristics
let notifyServiceId = "";
let writeServiceId = "";
let readServiceId = "";
let notifyCharacterId = "";
let writeCharacterId = "";
let readCharacterId = "";
for (const service of services) {
// const { notifyServiceId, writeServiceId, readServiceId } = device;
const done = [notifyServiceId, writeServiceId, readServiceId].every(
(v) => v !== "",
);
if (done) {
break;
}
console.log(
"获取蓝牙设备某个服务中所有特征值/uni.getBLEDeviceCharacteristics",
);
const res = await uni.getBLEDeviceCharacteristics({
deviceId: device.deviceId,
serviceId: service.uuid,
});
console.log(res);
for (const characteristic of res.characteristics) {
console.log(characteristic);
// const { notifyCharacterId, writeCharacterId, readCharacterId } = device;
const done = [
notifyCharacterId,
writeCharacterId,
readCharacterId,
].every((v) => v !== "");
if (done) {
break;
}
const { uuid, properties } = characteristic;
if (!notifyCharacterId) {
if (properties.notify) {
notifyCharacterId = uuid;
notifyServiceId = service.uuid;
}
}
if (!writeCharacterId) {
if (properties.write) {
writeCharacterId = uuid;
writeServiceId = service.uuid;
}
}
if (!readCharacterId) {
if (properties.read) {
readCharacterId = uuid;
readServiceId = service.uuid;
}
}
}
}
device.notifyServiceId = notifyServiceId;
device.notifyCharacterId = notifyCharacterId;
device.writeServiceId = writeServiceId;
device.writeCharacterId = writeCharacterId;
device.readServiceId = readServiceId;
device.readCharacterId = readCharacterId;
console.log(device);
}
static async getBLEMTU(device: Device) {
return BluetoothUtils.bindDevice(device, () =>
BluetoothUtils._getBLEMTU(device),
);
}
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);
// },
});
// });
}
static async sendData(device: Device, data: number[]) {
if (isAndroidApp) {
BluetoothUtils.sendDataAndroid(device, data);
} else {
const buf = new ArrayBuffer(data.length);
const dataView = new DataView(buf);
data.forEach((d, i) => {
dataView.setUint8(i, d);
});
return await new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: device.deviceId ?? "",
serviceId: device.writeServiceId ?? "",
characteristicId: device.writeCharacterId ?? "",
value: buf,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
},
});
});
}
}
static async sendDataAndroid(device: Device, data: number[]) {
const btAdapter = BluetoothAdapter.getDefaultAdapter();
const deviceObj = invoke(btAdapter, "getRemoteDevice", device.deviceId);
const PRINTER_UUID = UUID.fromString(
"00001101-0000-1000-8000-00805F9B34FB",
);
const btSocket = invoke(
deviceObj,
"createRfcommSocketToServiceRecord",
PRINTER_UUID,
);
if (!invoke(btSocket, "isConnected")) {
console.log("检测到设备未连接,尝试连接....");
invoke(btSocket, "connect");
}
console.log("设备已连接");
const outputStream = invoke(btSocket, "getOutputStream");
invoke(outputStream, "write", data);
invoke(outputStream, "flush");
setTimeout(() => {
invoke(outputStream, "close");
invoke(btSocket, "close");
}, 10 * 1000);
}
}

View File

@ -0,0 +1,6 @@
export * from "./bluetooth-utils";
export * from "./nfc";
export * from "./printer";
export * from "./request";
export * from "./uni-helper";
export * from "./upload";

View File

@ -0,0 +1,206 @@
const systemInfo = uni.getSystemInfoSync();
const isAndroid =
systemInfo.uniPlatform === "app" && systemInfo.osName === "android";
let runtimeMainActivity = null;
let newObject = null;
let getAttribute = null;
let setAttribute = null;
let importClass = null;
let invoke = null;
let Intent = null;
let Activity = null;
let PendingIntent = null;
let IntentFilter = null;
let NfcAdapter = null;
let NdefRecord = null;
let NdefMessage = null;
let MifareClassic = null;
let Ndef = null;
let Tag = null;
let Parcelable = null;
let NfcV = null;
if (isAndroid) {
runtimeMainActivity = plus.android.runtimeMainActivity;
newObject = plus.android.newObject;
getAttribute = plus.android.getAttribute;
setAttribute = plus.android.setAttribute;
importClass = plus.android.importClass;
invoke = plus.android.invoke;
Intent = importClass("android.content.Intent");
Activity = importClass("android.app.Activity");
PendingIntent = importClass("android.app.PendingIntent");
IntentFilter = importClass("android.content.IntentFilter");
NfcAdapter = importClass("android.nfc.NfcAdapter");
NdefRecord = importClass("android.nfc.NdefRecord");
NdefMessage = importClass("android.nfc.NdefMessage");
MifareClassic = importClass("android.nfc.tech.MifareClassic");
Ndef = importClass("android.nfc.tech.Ndef");
Tag = importClass("android.nfc.Tag");
Parcelable = importClass("android.os.Parcelable");
NfcV = importClass("android.nfc.tech.NfcV");
}
export class AndroidNfcUtil {
main = null;
nfcAdapter = null;
pendingIntent = null;
intentFiltersArray = null;
techListsArray = null;
discoveredListenerList = [];
/**
* @type {Function}
*/
_discoveredHandler = null;
/**
* @type {Function}
*/
_resumeHandler = null;
constructor() {
this.main = runtimeMainActivity();
this.nfcAdapter = NfcAdapter.getDefaultAdapter(this.main);
this._discoveredHandler = this.discoveredHandler.bind(this);
this._resumeHandler = this.resumeHandler.bind(this);
}
/**
*
* @param {Function} cb
*/
addDiscoveredListener(cb) {
this.discoveredListenerList.push(cb);
}
/**
* 是否支持NFC
* @returns
*/
static isNfcSupported() {
const main = runtimeMainActivity();
const nfcAdapter = NfcAdapter.getDefaultAdapter(main);
return nfcAdapter != null;
}
/**
* 是否开启NFC
* @returns {boolean} true 已开启
*/
static async isNfcEnabled() {
const main = runtimeMainActivity();
const nfcAdapter = NfcAdapter.getDefaultAdapter(main);
return nfcAdapter && nfcAdapter.isEnabled();
}
/**
*
* @returns {Promise<number[]>}
*/
async startNfcScan() {
console.log("startNfcScan", this.nfcAdapter);
if (this.nfcAdapter == null) {
throw new Error("该设备不支持NFC");
} else if (!this.nfcAdapter.isEnabled()) {
throw new Error("未开启NFC");
} else {
return await this.init();
}
}
async stopNfcScan() {
console.log("stopNfcScan");
plus.globalEvent.removeEventListener("newintent", this._discoveredHandler);
plus.globalEvent.removeEventListener("resume", this._discoveredHandler);
this.nfcAdapter.disableForegroundDispatch(this.main);
}
/**
* 初始化ncf 并开启监听
*/
async init() {
console.log("init");
const ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
const tag = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
const tech = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
this.intentFiltersArray = [ndef, tag, tech];
this.techListsArray = [
["android.nfc.tech.Ndef"],
["android.nfc.tech.IsoDep"],
["android.nfc.tech.NfcA"],
["android.nfc.tech.NfcB"],
["android.nfc.tech.NfcF"],
["android.nfc.tech.Nfcf"],
["android.nfc.tech.Nfef"],
["android.nfc.tech.Ndef"],
["android.nfc.tech.NfcV"],
["android.nfc.tech.NdefFormatable"],
["android.nfc.tech.MifareClassi"],
["android.nfc.tech.MifareUltralight"],
];
const intent = new Intent(this.main, this.main.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
this.pendingIntent = PendingIntent.getActivity(this.main, 0, intent, 0);
this.nfcAdapter.enableForegroundDispatch(
this.main,
this.pendingIntent,
this.intentFiltersArray,
this.techListsArray,
);
plus.globalEvent.addEventListener("newintent", this._discoveredHandler);
plus.globalEvent.addEventListener("resume", this._resumeHandler);
}
resumeHandler() {
console.log("resumeHandler");
this.nfcAdapter.enableForegroundDispatch(
this.main,
this.pendingIntent,
this.intentFiltersArray,
this.techListsArray,
);
}
discoveredHandler() {
console.log("discoveredHandler");
this.discoveredListenerList.forEach((cb) => {
cb(this.readId());
});
}
readId() {
console.log("readId");
const intent = this.main.getIntent();
const action = intent.getAction();
console.log("action: " + action);
// if (
// NfcAdapter.ACTION_NDEF_DISCOVERED == action ||
// NfcAdapter.ACTION_TAG_DISCOVERED == action ||
// NfcAdapter.ACTION_TECH_DISCOVERED == action
// ) {
console.log("intent.getAction()", intent.getAction());
const tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
console.log("tagFromIntent.getId", tagFromIntent.getId());
return tagFromIntent.getId();
// }
}
static async scan() {
const androidNfcUtil = new AndroidNfcUtil();
return await new Promise(async (resolve, reject) => {
await androidNfcUtil.startNfcScan();
androidNfcUtil.addDiscoveredListener((res) => {
androidNfcUtil.stopNfcScan();
resolve(res);
});
});
}
}

View File

@ -0,0 +1,38 @@
import { AndroidNfcUtil } from "./android";
import { WeixinNfcUtil } from "./weixin";
const systemInfo = uni.getSystemInfoSync();
const isAndroidApp =
systemInfo.uniPlatform === "app" && systemInfo.osName === "android";
const isAndroidWeixin =
systemInfo.uniPlatform === "mp-weixin" && systemInfo.osName === "android";
export async function nfcScan() {
if (isAndroidApp) {
return await AndroidNfcUtil.scan();
} else if (isAndroidWeixin) {
return await WeixinNfcUtil.scan();
} else {
console.warn("不支持当前平台");
}
}
export async function isNfcEnabled() {
if (isAndroidApp) {
return await AndroidNfcUtil.isNfcEnabled();
} else if (isAndroidWeixin) {
return await WeixinNfcUtil.isNfcEnabled();
} else {
console.warn("不支持当前平台");
}
}
export function getNFCUtil(){
if (isAndroidApp) {
return new AndroidNfcUtil();
} else if (isAndroidWeixin) {
return new WeixinNfcUtil();
} else {
console.warn("不支持当前平台");
}
}

View File

@ -0,0 +1,78 @@
export async function startNfcScan() {
const nfcAdapter = wx.getNFCAdapter();
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);
});
});
}
export class WeixinNfcUtil {
nfcAdapter = null;
discoveredListenerList = [];
/**
* @type {Function}
*/
_discoveredHandler = null;
constructor() {
this.nfcAdapter = wx.getNFCAdapter();
this._discoveredHandler = this.discoveredHandler.bind(this);
this.nfcAdapter.onDiscovered(this._discoveredHandler);
}
/**
* 是否开启NFC
* NOTE 微信无法判断是否开启
* @returns {boolean} true 已开启
*/
static async isNfcEnabled() {
try {
wx.getNFCAdapter();
return true;
} catch (e) {
console.error(e);
return false;
}
}
discoveredHandler(res) {
console.log("onDiscovered", res);
const arr = Array.from(new Int8Array(res.id));
this.discoveredListenerList.forEach((cb) => {
cb(arr);
});
}
/**
*
* @param {Function} cb
*/
addDiscoveredListener(cb) {
this.discoveredListenerList.push(cb);
}
async startNfcScan() {
return await this.nfcAdapter.startDiscovery();
}
async stopNfcScan() {
this.nfcAdapter.offDiscovered(this._discoveredHandler);
return await this.nfcAdapter.stopDiscovery();
}
static async scan() {
const weixinNfcUtil = new WeixinNfcUtil();
return await new Promise(async (resolve, reject) => {
await weixinNfcUtil.startNfcScan();
weixinNfcUtil.addDiscoveredListener((res) => {
weixinNfcUtil.stopNfcScan();
resolve(res);
});
});
}
}

View File

@ -0,0 +1,65 @@
import { BluetoothUtils, Device } from "@/bluetooth-utils";
import { wait } from "@r-utils/common";
type DeviceData = Required<Device>;
export class Printer {
device: DeviceData;
size = 0;
constructor(device: DeviceData, { size = 80 } = {}) {
this.device = device;
this.size = size;
}
async print(data: number[]) {
console.log(
data.length,
data.slice(0, 100),
data.slice(data.length - 100, data.length)
);
let res = null;
const systemInfo = uni.getSystemInfoSync();
if (systemInfo.uniPlatform === "app" && systemInfo.osName === "android") {
res = await BluetoothUtils.sendDataAndroid(this.device, data);
} else {
res = await BluetoothUtils.bindDevice(this.device, async () => {
await wait(100);
return await this._print(data);
});
}
return res;
}
async _print(data: number[]):Promise<any> {
const size = Math.min(data.length, this.size);
if (size === 0) {
return;
}
const buf = new ArrayBuffer(size);
const dataView = new DataView(buf);
for (let i = 0; i < size; i++) {
dataView.setUint8(i, data[i]);
}
await new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: this.device.deviceId,
serviceId: this.device.writeServiceId,
characteristicId: this.device.writeCharacterId,
value: buf,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
},
});
});
await wait(30);
return await this._print(data.slice(size));
}
}

View File

@ -0,0 +1,183 @@
export type DataType = string | AnyObject | ArrayBuffer;
/** 请求配置 */
export interface Config<T extends DataType>
extends Partial<UniApp.RequestOptions> {
baseURL?: string;
data?: T;
[key: string]: any;
}
/** 响应 */
export interface Response<
T extends DataType,
D extends DataType,
C extends Config<D> = Config<D>
> extends UniApp.RequestSuccessCallbackResult {
data: T;
errMsg?: string;
config: C;
}
/** 成功拦截器 */
type FulfilledInterceptor<R, T> = (res: R) => T | Promise<T>;
/** 失败拦截器 */
type RejectedInterceptor = (error: any) => any;
/** 拦截器管理 */
class InterceptorManager<R> {
/** 拦截器列表 */
handlers: [FulfilledInterceptor<R, any>?, RejectedInterceptor?][] = [];
/** 添加拦截器 */
add<T = R>(
onFulfilled?: FulfilledInterceptor<R, T>,
onRejected?: RejectedInterceptor
): number {
this.handlers.push([onFulfilled, onRejected]);
return this.handlers.length - 1;
}
/** 移除拦截器 */
remove(id: number) {
return this.handlers.splice(id, 1);
}
forEach(
fn: (
onFulfilled?: FulfilledInterceptor<R, any>,
onRejected?: RejectedInterceptor
) => void
) {
this.handlers.forEach(([onFulfilled, onRejected]) => {
fn(onFulfilled, onRejected);
});
}
}
/**
*
*/
export class Request {
/** 请求配置 */
config: Config<any>;
/** 拦截器 */
interceptors: {
/** 请求拦截器 */
request: InterceptorManager<Config<any>>;
/** 响应拦截器 */
response: InterceptorManager<any>;
};
constructor(config: Config<any> = { url: "" }) {
this.config = config;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
}
/**
*
*/
async request<
RESPD extends DataType,
REQD extends DataType,
R = Response<RESPD, REQD>
>(config: Config<REQD>): Promise<R> {
// 合并方法配置 与 实例配置
let newConfig = Object.assign({}, this.config, config);
// 赋值默认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;
}
}
});
// 发送请求
// 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({
...newConfig,
url: newConfig.url,
}));
// 替换新请求的配置
responsePromise = {
...responsePromise,
config: newConfig,
};
// 执行响应拦截器
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;
}
}
});
return responsePromise;
}
/** GET 请求 */
get<RESPD extends DataType, REQD extends DataType, R = Response<RESPD, REQD>>(
url: string,
config?: Config<REQD>
): Promise<R> {
return this.request({ ...config, url, method: "GET" });
}
/** POST 请求 */
post<
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: "POST", data });
}
/** PUT 请求 */
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 });
}
}
export default Request;

View File

@ -0,0 +1 @@
export * from "./Request";

View File

@ -0,0 +1,245 @@
import { ComponentInternalInstance } from "vue";
/** 扫码 */
export async function scanCode() {
const res = await new Promise<UniApp.ScanCodeSuccessRes>(
(resolve, reject) => {
uni.scanCode({
success(res) {
resolve(res);
},
fail(res) {
reject(res);
},
});
},
);
console.log({ res });
return res.result;
}
/** 获取当前页面 */
export function getCurrentPage() {
const pages = getCurrentPages();
return pages[pages.length - 1];
}
/**
* 使 TypeScript toPromise
*
*
* 1. 使 Parameters<T>[0]
* 2. success
* 3. 使 infer
* 4. ReturnTypeuni API返回void
*/
// 方法1直接使用 Parameters<T> 工具类型
// type ExtractFirstParameter<T> = Parameters<T>[0]; // 这样也可以但需要T是函数类型
// 方法2使用条件类型实现更灵活等价于 Parameters<T>[0]
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;
// 提取函数参数类型并排除回调函数用于args参数
type ExtractOptions<T> = T extends (options: infer P) => unknown
? Omit<P, "success" | "fail" | "complete">
: never;
// 类型测试示例(编译时验证)
// type TestDownloadOptions = ExtractOptions<typeof uni.downloadFile>; // UniNamespace.DownloadFileOption 去除回调
// type TestDownloadResult = ExtractSuccessResult<Parameters<typeof uni.downloadFile>[0]>; // UniNamespace.DownloadSuccessData
/**
* uni-app Promise使
*
*
* -
* - TypeScript
* - uni-app APIAPI单独写重载
* -
*
* @example
* // 自动推断为 Promise<UniNamespace.DownloadSuccessData>
* const downloadRes = await toPromise(uni.downloadFile, { url: 'https://example.com/file.jpg' });
* console.log(downloadRes.tempFilePath); // ✅ 类型安全,有智能提示
*
* // 自动推断为 Promise<UniNamespace.ScanCodeSuccessRes>
* const scanRes = await toPromise(uni.scanCode, {});
* console.log(scanRes.result); // ✅ 类型安全,有智能提示
*
* // 自动推断为 Promise<UniNamespace.UploadFileSuccessCallbackResult>
* const uploadRes = await toPromise(uni.uploadFile, {
* url: '/upload',
* filePath: 'temp://file.jpg',
* name: 'file'
* });
* console.log(uploadRes.statusCode); // ✅ 类型安全,有智能提示
*
* // 自动推断为 Promise<UniNamespace.RequestSuccessCallbackResult>
* const requestRes = await toPromise(uni.request, { url: '/api/data' });
* console.log(requestRes.data); // ✅ 类型安全,有智能提示
*/
export function toPromise<F extends (options: Record<string, unknown>) => void>(
uniFun: F,
args?: ExtractOptions<F>,
): Promise<ExtractSuccessResult<ExtractFirstParameter<F>>> {
type OptionsType = ExtractFirstParameter<F>;
type ResultType = ExtractSuccessResult<OptionsType>;
return new Promise<ResultType>((resolve, reject) => {
uniFun({
...args,
success(res: ResultType) {
resolve(res);
},
fail(err: unknown) {
reject(err);
},
} as OptionsType);
});
}
export function showToast(
title: string,
icon: "success" | "loading" | "error" | "none" = "none",
options?: UniNamespace.ShowToastOptions,
) {
uni.showToast({
...options,
title,
icon,
duration: options?.duration ?? 1000,
});
}
/**
* URL生成canvas二维码
* @param canvasId
* @param url URL
* @param x canvas左边宽度px
* @param y canvas顶部高度px
* @param width px
* @param height px
* @param thisArg
* @returns
*/
export async function getCanvasImageData(
canvasId: string,
url: string,
x = 0,
y = 0,
width = 200,
height = 200,
thisArg: ComponentInternalInstance,
) {
const context = uni.createCanvasContext(canvasId, thisArg);
const imgRes = await toPromise(
uni.downloadFile,
{
url,
},
);
console.log(imgRes);
context.drawImage(imgRes.tempFilePath, x, y, width, height);
await new Promise((resolve) => {
context.draw(false, (res) => {
console.log(res);
resolve(res);
});
});
const imageData: UniNamespace.CanvasGetImageDataRes =
await uni.canvasGetImageData({
canvasId: canvasId,
x,
y,
width: width,
height: height,
});
return imageData.data;
}
export async function getCanvasImageDataSimplify(
canvasId: string,
url: string,
width = 200,
height = 200,
thisArg: ComponentInternalInstance,
) {
return await getCanvasImageData(canvasId, url, 0, 0, width, height, thisArg);
}
export async function getCanvasImageDataTransObj({
canvasId,
url,
x = 0,
y = 0,
width = 200,
height = 200,
thisArg,
}: {
canvasId: string;
url: string;
x: number;
y: number;
width: number;
height: number;
thisArg: ComponentInternalInstance;
}) {
return await getCanvasImageData(canvasId, url, x, y, width, height, thisArg);
}
export function rpxToPx(rpx: number | string) {
if (typeof rpx === "string") {
rpx = Number.parseInt(rpx);
}
const screenWidth = uni.getSystemInfoSync().screenWidth;
return (screenWidth * rpx) / 750;
}
export function pxToRpx(px: number | string) {
if (typeof px === "string") {
px = Number.parseInt(px);
}
const screenWidth = uni.getSystemInfoSync().screenWidth;
return (750 * px) / screenWidth;
}
/**
* 退
*/
export function exitApp() {
// #ifdef APP-PLUS
if (plus.os.name?.toLowerCase() === "android") {
plus.runtime.quit();
} else {
const threadClass = plus.ios.importClass("NSThread");
const mainThread = plus.ios.invoke(threadClass, "mainThread");
plus.ios.invoke(mainThread, "exit");
}
// #endif
}
/**
*
*/
export function checkAndroidPermission(permissionList: string[]) {
// #ifdef APP-PLUS
const ActivityCompat = plus.android.importClass(
"androidx.core.app.ActivityCompat",
) as Android.ActivityCompat;
const activity = plus.android.runtimeMainActivity();
return permissionList.every((v) => {
const p = ActivityCompat.checkSelfPermission(activity, v);
return p === 0;
});
// #endif
}

View File

@ -0,0 +1,78 @@
type UploadResponseData = {
originalFileName: string;
url: string;
};
type UploadResult = {
code: number;
msg: string;
data: UploadResponseData;
};
type UploadResponse = {
errno: number;
errmsg: string;
originalFileName: string;
url: string;
};
/**
*
* @param options
* @returns
*/
export async function upload(
options: UniApp.UploadFileOption,
): Promise<UploadResult> {
console.log(options);
return new Promise<UniApp.UploadFileSuccessCallbackResult>(
(resolve, reject) => {
uni.uploadFile({
url: options.url,
filePath: options.filePath,
name: options.name ?? "ImageFile",
// header: {
// Authorization: `Bearer ${token}`,
// },
// formData: {
// shopId: userStore.shop?.shopId,
// },
success(res) {
console.log(res);
resolve(res);
},
fail(err) {
reject(err);
},
});
},
)
.then((res) => {
if (res.statusCode === 200) {
const data: UploadResponse = JSON.parse(res.data);
return {
code: data.errno,
msg:
data.errmsg != null && data.errmsg != "" ? data.errmsg : "上传成功",
data: {
originalFileName: data.originalFileName,
url: data.url,
},
} as UploadResult;
} else {
throw new Error(res.errMsg);
}
})
.catch((err) => {
console.error(err);
uni.showToast({
icon: "none",
title: err ?? "网络错误",
});
throw err;
});
}

View File

@ -0,0 +1,22 @@
{
"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts", "src/**/*.js"],
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["dom", "esnext"],
"types": ["@dcloudio/types"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"allowJs": true
}
}

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

@ -0,0 +1,69 @@
/// <reference types="@dcloudio/types" />
declare namespace Android {
class ActivityCompat implements PlusAndroidInstanceObject {
checkSelfPermission(context: Context, permission: string): number;
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
class Build implements PlusAndroidClassObject {
readonly VERSION: {
SDK_INT: number;
ECLAIR_0_1: number;
};
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
class BluetoothAdapter implements PlusAndroidInstanceObject {
readonly ACTION_DISCOVERY_FINISHED =
"android.bluetooth.device.extra.DEVICE";
startDiscovery(): boolean;
cancelDiscovery(): boolean;
isDiscovering(): boolean;
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
getDefaultAdapter(): BluetoothAdapter;
getBondedDevices(): Set<BluetoothDevice>;
}
class BluetoothDevice implements PlusAndroidInstanceObject {
readonly ACTION_FOUND = "android.bluetooth.device.action.FOUND";
readonly EXTRA_DEVICE = "android.bluetooth.device.extra.DEVICE";
getName(): string;
getAddress(): string;
createRfcommSocketToServiceRecord(uuid: UUID): BluetoothSocket;
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
interface Set<E> extends PlusAndroidClassObject {
iterator(): Iterator<E>;
}
interface BluetoothSocket extends PlusAndroidClassObject {
isConnected(): boolean;
}
interface UUID extends PlusAndroidClassObject {
fromString(name: string): UUID;
}
class Context implements PlusAndroidInstanceObject {
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
class Intent implements PlusAndroidInstanceObject {
getParcelableExtra<T>(name: string): T;
getAction(): string;
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
class BroadcastReceiver {}
class IntentFilter implements PlusAndroidInstanceObject {
addAction(action: string): void;
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
class Activity implements PlusAndroidInstanceObject {
registerReceiver(receiver: BroadcastReceiver, filter: IntentFilter): Intent;
unregisterReceiver(receiver: BroadcastReceiver): void;
plusGetAttribute(name?: string | undefined): unknown;
plusSetAttribute(name?: string | undefined, value?: unknown): void;
}
}

1074
packages/uni-app/types/api.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

19
packages/uni-app/types/config.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
interface Config {
/** 环境 */
ENV?: string;
/** api 基础地址 */
API_BASE_URL?: string;
/** appId */
APP_ID?: string;
/** static 目录基础地址 */
STATIC_BASE_URL?: string;
/** 上传文件地址 */
UPLOAD_URL?: string;
/** 文件地址 */
FILE_BASE_URL?: string;
}

4
packages/uni-app/types/env.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
/// <reference types="vite/client" />
declare module "uview-ui";
declare module "uview-plus";

5
packages/uni-app/types/globals.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare global {
function setInterval(handler: TimerHandler, timeout?: number): number;
}
export {};

View File

@ -0,0 +1,49 @@
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[];
}

124
packages/uni-app/types/uniapp.d.ts vendored Normal file
View File

@ -0,0 +1,124 @@
/// <reference types="@dcloudio/types" />
declare namespace UniNamespace {
type LocaldataItem = {
text: string;
value: string;
children?: LocaldataItem[];
};
type LocaldataNodeclickValue = {
text: string;
value: string;
parent_value?: string;
};
type LocaldataChangeValue = { text: string; value: string };
type Event<T> = { detail: T };
type InputEvent = Event<{ value: string }>;
type PickerEvent = Event<{ value: string }>;
type PickerViewEvent = Event<{ value: unknown[] }>;
type ScrollEvent = Event<{ scrollTop: number }>;
type SwiperEvent = Event<{ current: number }>;
type UniDataPickerEvent = Event<{ value: LocaldataChangeValue[] }>;
interface WriteBLECharacteristicValueOptions2 {
/**
* id device
*/
deviceId: string;
/**
* uuid
*/
serviceId: string;
/**
* uuid
*/
characteristicId: string;
/**
*
*/
value: ArrayBuffer;
/**
*
*/
success?: (result: StopBluetoothDevicesDiscoverySuccess) => void;
/**
*
*/
fail?: (result: unknown) => void;
/**
*
*/
complete?: (result: unknown) => void;
}
interface BluetoothError {
code: number;
}
interface StopBluetoothDevicesDiscoveryOptions2 {
/**
*
*/
success?: (result: StopBluetoothDevicesDiscoverySuccess) => void;
/**
*
*/
fail?: (result: BluetoothError) => void;
/**
*
*/
complete?: () => void;
}
}
// type DataType = string | AnyObject | ArrayBuffer;
interface Uni {
// request<T extends DataType, D extends DataType>(
// options: RequestOptionsGeneric<D>
// ): Promise<RequestSuccessCallbackResultGeneric<T>>;
request(
options: UniApp.RequestOptions,
): Promise<UniApp.RequestSuccessCallbackResult>;
writeBLECharacteristicValue(
options: UniNamespace.WriteBLECharacteristicValueOptions2,
): void;
stopBluetoothDevicesDiscovery(
options: UniNamespace.StopBluetoothDevicesDiscoveryOptions,
): void;
}
// interface RequestOptionsGeneric<T extends DataType>
// extends UniApp.RequestOptions {
// data?: T;
// }
// interface RequestSuccessCallbackResultGeneric<T extends DataType>
// extends UniApp.RequestSuccessCallbackResult {
// data: T;
// errMsg?: string;
// }
// interface PlusIoDirectoryEntry extends PlusIoFileEntry {
// file(
// succesCB?: (result: PlusIoFile) => void,
// errorCB?: (result: any) => void
// ): void;
// }
interface PlusIo {
resolveLocalFileSystemURL(
url?: string,
succesCB?: (result: PlusIoFileEntry) => void,
errorCB?: (result: any) => void,
): void;
}
// interface PlusIoFileEvent {
// target?: PlusIoFileReader;
// }
interface PlusIoFileReader {
onload?: (result: PlusIoFileEvent) => void;
}

View File

@ -0,0 +1,32 @@
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: resolve(__dirname, 'src/index.ts'),
formats: ['es', 'cjs'],
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
// uni / plus / wx 是 uni-app 运行时注入的全局变量,无需 external
external: ['vue', 'lodash', /^@r-utils\/.*/],
},
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
plugins: [
dts({
include: ['src'],
outDir: 'dist',
}),
],
});

1332
packages/vue2/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
{
"name": "@r-utils/vue2",
"version": "1.0.0",
"private": true,
"description": "Vue2工具库",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./*": "./*"
},
"keywords": [
"vue2"
],
"scripts": {
"build": "vite build",
"watch": "vite build --watch",
"lint": "eslint --ext .js,ts --fix src",
"release": "standard-version",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "jest"
},
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"vue": "^2.7.0"
}
}

View File

@ -0,0 +1,2 @@
export { default } from "./plugins/visibility";
export { default as VisibilityPlugin } from "./plugins/visibility";

View File

@ -0,0 +1,46 @@
function install(Vue) {
Vue.mixin({
created() {
const onShowHook = this.$options.onShow;
const onHideHook = this.$options.onHide;
if (onShowHook != null || onHideHook != null) {
this.visibilitychangeCallback();
document.addEventListener(
"visibilitychange",
this.visibilitychangeCallback
);
}
},
destroyed() {
document.removeEventListener(
"visibilitychange",
this.visibilitychangeCallback
);
},
methods: {
visibilitychangeCallback() {
const onShowHook = this.$options.onShow?.bind(this);
const onHideHook = this.$options.onHide?.bind(this);
if (document.visibilityState == "visible") {
console.log("visible");
if (typeof onShowHook === "function") {
onShowHook();
}
}
if (document.visibilityState == "hidden") {
console.log("hidden");
if (typeof onHideHook === "function") {
onHideHook();
}
}
},
},
});
}
export default {
install,
};

View File

@ -0,0 +1,21 @@
{
"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts", "src/**/*.js"],
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["dom", "esnext"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"allowJs": true
}
}

View File

@ -0,0 +1,31 @@
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: resolve(__dirname, 'src/index.ts'),
formats: ['es', 'cjs'],
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
external: ['vue', 'lodash'],
},
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
plugins: [
dts({
include: ['src'],
outDir: 'dist',
}),
],
});

10566
packages/vue3/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "@r-utils/vue3",
"version": "1.0.0",
"description": "Vue3 工具",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./*": "./*"
},
"keywords": [
"vue3"
],
"scripts": {
"build": "vite build",
"watch": "vite build --watch",
"lint": "eslint --ext .js,ts --fix src",
"release": "standard-version",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "jest"
},
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21",
"vue": "^3.3.0"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@babel/preset-env": "^7.12.0",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/config-conventional": "^17.6.1",
"@commitlint/cz-commitlint": "^17.5.0",
"@jest/globals": "^29.7.0",
"babel-jest": "^29.7.0",
"commitizen": "^4.3.0",
"eslint-define-config": "^1.18.0",
"husky": "^8.0.3",
"inquirer": "^8.0.0",
"jest": "^29.7.0",
"lint-staged": "^15.4.0",
"prettier": "^3.4.2",
"standard-version": "^9.5.0"
},
"packageManager": "pnpm@8.15.6"
}

View File

@ -0,0 +1 @@
export * from "./vue-helper"

View File

@ -0,0 +1,110 @@
import { ComponentInternalInstance, ComponentPublicInstance } from "vue";
type CustomClassObj = Record<string, boolean>;
type CustomClass = string | Array<string> | CustomClassObj;
type DistCustomClass = Record<string, true>;
export function createCustomClassObj(customClass: string): DistCustomClass;
export function createCustomClassObj(
customClass: Array<string>
): DistCustomClass;
export function createCustomClassObj(
customClass: string | Array<string>
): DistCustomClass {
let customClassObj = <DistCustomClass>{};
if (typeof customClass === "string") {
const customClassStr = customClass.trim().replace(" +", " ");
if (customClassStr.length > 0) {
const customClassEntries = customClassStr
.split(" ")
.map((name) => [name, true]);
customClassObj = Object.fromEntries(customClassEntries);
}
} else if (Array.isArray(customClass)) {
if (customClass.length > 0) {
const customClassEntries = customClass.map((name) => [name, true]);
customClassObj = Object.fromEntries(customClassEntries);
}
} else {
throw new TypeError(
`customClass只能是字符串或数组类型customClass: ${customClass}`
);
}
return customClassObj;
}
/**
* vue class vue class
* @param sourceClass class
* @returns
*/
export function convertCustomClass(sourceClass: CustomClass): CustomClassObj {
let customClassObj = <CustomClassObj>{};
if (typeof sourceClass === "string") {
const customClassStr = sourceClass.trim().replace(" +", " ");
if (customClassStr.length > 0) {
const customClassEntries = customClassStr
.split(" ")
.map((name) => [name, true]);
customClassObj = Object.fromEntries(customClassEntries);
}
} else if (Array.isArray(sourceClass)) {
if (sourceClass.length > 0) {
const customClassEntries = sourceClass.map((name) => [name, true]);
customClassObj = Object.fromEntries(customClassEntries);
}
} else if (sourceClass instanceof Object) {
customClassObj = sourceClass;
} else {
throw new TypeError(
`sourceClass不是有效的vue classsourceClass: ${sourceClass}`
);
}
return customClassObj;
}
/**
* class
* @param customClass vue class
* @returns
*/
export function mergeClass(...customClass: CustomClass[]) {
return customClass
.map((cc) => convertCustomClass(cc))
.reduce((a, b) => Object.assign(a, b));
}
/**
*
* @param thisArg
* @param componentName
* @param eventName
* @param params
* @returns
*/
export function dispatch(
thisArg: ComponentPublicInstance,
componentName: ComponentPublicInstance,
eventName: string,
params: unknown
): void {
let parent = thisArg.$parent || thisArg.$root;
if (parent == null) {
return;
}
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.call(parent, eventName, params);
}
}

View File

@ -0,0 +1,21 @@
{
"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts", "src/**/*.js"],
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["dom", "esnext"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"allowJs": true
}
}

View File

@ -0,0 +1,31 @@
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: resolve(__dirname, 'src/index.ts'),
formats: ['es', 'cjs'],
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
external: ['vue', 'lodash'],
},
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
plugins: [
dts({
include: ['src'],
outDir: 'dist',
}),
],
});

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,5 @@
packages:
- packages/**
catalog:
"webpack": ^5.80.0

View File

@ -1,15 +1,25 @@
// https://prettier.io/docs/en/configuration.html
module.exports = {
export default {
tabWidth: 4,
overrides: [
{
files: ["*.js", "*.ts", "*.vue", "*.json"],
files: [
"*.js",
"*.mjs",
"*.jsx",
"*.ts",
"*.tsx",
"*.vue",
"*.json",
"*.yml",
"*.yaml",
],
options: {
tabWidth: 2,
},
},
{
files: ["*.html"],
files: ["*.html", "*.vue", "*.json"],
options: {
printWidth: 120,
},

View File

@ -1,3 +0,0 @@
export * from "./utils/permission";
console.log("index");

View File

@ -1,6 +1,6 @@
{
"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"include": ["**/*.ts"],
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
@ -11,7 +11,7 @@
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"lib": ["esnext"],
// "types": ["@dcloudio/types"],
"baseUrl": "./",
"paths": {

View File

@ -1,18 +0,0 @@
const path = require('path');
module.exports = {
entry: './src/index.ts',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
};