9 Commits

Author SHA1 Message Date
TianSL 5dbedcaf0f chore(release): 1.3.0 2026-04-20 17:55:33 +08:00
TianSL b10d613987 feat(common): 添加工具 2026-04-20 17:54:26 +08:00
TianSL 7ff223fa32 chore(release): 1.2.1 2026-03-26 08:56:15 +08:00
TianSL 154c4c1a51 ci: 🎡 配置.ps1文件的编码及换行符 2026-03-18 16:41:54 +08:00
TianSL da7d53e791 ci: 🎡 publish分离版本号更新 2026-03-18 16:32:27 +08:00
TianSL a8af544e79 chore(release): 1.2.0 2026-03-18 16:31:01 +08:00
TianSL dc114a4835 chore(release): publish v1.0.3 2026-03-18 15:53:43 +08:00
TianSL 5851d40d07 feat(common): 添加input-rules 2026-03-18 15:52:07 +08:00
TianSL 0cecc4df11 ci: 🎡 配置子项目npm仓库地址 2026-03-17 17:45:52 +08:00
76 changed files with 8977 additions and 16475 deletions
+26 -19
View File
@@ -1,27 +1,34 @@
{ {
"permissions": { "permissions": {
"defaultMode": "acceptEdits",
"allow": [ "allow": [
"Read(./)",
"Edit(./)",
"Write(./)",
"Bash(npm run *)",
"Bash(npm test *)",
"Bash(npm run build *)",
"Bash(npm view *)",
"Bash(npm list *)",
"Bash(pnpm install *)",
"Bash(pnpm add *)",
"Bash(pnpm build *)",
"Bash(pnpm run *)",
"Bash(pnpm test *)",
"Bash(pnpm run build *)",
"Bash(pnpm view *)",
"Bash(pnpm list *)",
"Bash(git diff *)",
"Bash(git status *)",
"Bash(git log *)",
"Bash(grep *)",
"Bash(pnpm --filter @r-utils/vue3 add vue)", "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 'npm run lint-staged\\\\nnpm test\\\\n')",
"Bash(printf 'npx commitlint --edit \"\"$1\"\"\\\\n')", "Bash(printf 'npx commitlint --edit \"\"$1\"\"\\\\n')",
"Bash(npm view typescript version)", "Bash(pnpm update typescript)"
"Bash(pnpm update typescript)", ],
"Bash(npm view @typescript-eslint/parser versions --json)", "ask": ["Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)", "Bash(git push *)", "Bash(rm -rf *)"]
"Bash(npm view @typescript-eslint/parser@latest peerDependencies)",
"Bash(npm view @typescript-eslint/parser@latest version)",
"Bash(pnpm list typescript)",
"Bash(pnpm list @typescript-eslint/parser @typescript-eslint/eslint-plugin typescript-eslint)",
"Bash(npm view @commitlint/cz-commitlint@latest peerDependencies)",
"Bash(npm view inquirer@9 version)"
]
} }
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"path": "node_modules/cz-git" "path": "cz-git"
} }
+6 -1
View File
@@ -3,7 +3,12 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
# indent_size = 2 indent_size = 2
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.ps1]
charset = utf-8-bom
end_of_line = crlf
indent_size = 4
-26
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/
-40
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,
},
},
],
});
+5
View File
@@ -0,0 +1,5 @@
# Default: auto-detect
* text=auto eol=lf
# PowerShell scripts must use CRLF
*.ps1 text eol=crlf
+1
View File
@@ -1,3 +1,4 @@
node_modules node_modules
dist dist
coverage coverage
*.bak
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"src/*.{js,jsx,ts,tsx,vue}": "npm run lint" "packages/*/src/**/*.{js,jsx,ts,tsx,vue}": "eslint --fix"
} }
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+2
View File
@@ -0,0 +1,2 @@
/cache
/project.local.yml
+152
View File
@@ -0,0 +1,152 @@
# 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: []
+9
View File
@@ -0,0 +1,9 @@
{
"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" }
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"recommendations": [
"psioniq.psi-header",
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig"
]
}
+36 -1
View File
@@ -1,3 +1,38 @@
{ {
"psi-header.config": {
"forceToTop": true,
"blankLinesAfter": 1,
"overridePathSeparator": "/"
},
"psi-header.changes-tracking": {
"isActive": true,
"autoHeader": "autoSave",
"modDate": "@lastModified",
"modDateFormat": "YYYY-MM-DD HH:mm:ss",
"modAuthor": "@lastModifiedBy",
"exclude": ["markdown", "json"],
"excludeGlob": ["**/settings.json"]
},
"psi-header.templates": [
{
"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>>)"
],
"changeLogNaturalOrder": true
}
],
"workbench.editor.limit.value": 3,
"cSpell.words": [
"Codice",
"commitlint",
"dcloudio",
"Fabbrica"
],
"js/ts.tsdk.path": "node_modules\\typescript\\lib"
} }
+16
View File
@@ -2,6 +2,22 @@
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. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [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)
## [1.2.0](https://gitee.com/codice_fabbrica/r-util-js/compare/v1.1.0...v1.2.0) (2026-03-18)
### Features
* **common:** :sparkles: 添加input-rules ([5851d40](https://gitee.com/codice_fabbrica/r-util-js/commit/5851d40d079da9818499121b5f576a761f448773))
## 1.1.0 (2026-03-17) ## 1.1.0 (2026-03-17)
+16
View File
@@ -60,6 +60,22 @@ pnpm commit
**注意**:提交时 husky pre-commit 钩子会自动运行 lint-staged 和 jest 测试,commit-msg 钩子会用 commitlint 校验提交信息格式。 **注意**:提交时 husky pre-commit 钩子会自动运行 lint-staged 和 jest 测试,commit-msg 钩子会用 commitlint 校验提交信息格式。
### 发布
发布分两步:先用 `standard-version` 管理版本,再用脚本构建和发布。
```bash
# 1. 版本管理(更新版本号 + 生成 CHANGELOG + git commit/tag
pnpm release # 默认 patch
npx standard-version --release-as minor # 指定版本类型
# 2. 构建并发布到 npm
bash scripts/publish.sh # Linux/macOS
powershell scripts/publish.ps1 # Windows
```
版本配置见 `.versionrc.json`,会同步更新根包和所有子包的版本号。
## 架构说明 ## 架构说明
### Monorepo 结构 ### Monorepo 结构
+23 -19
View File
@@ -9,6 +9,7 @@
| `@r-utils/common` | `packages/common` | 通用 JS/TS 工具(权限、倒计时、打印、时间等) | | `@r-utils/common` | `packages/common` | 通用 JS/TS 工具(权限、倒计时、打印、时间等) |
| `@r-utils/vue3` | `packages/vue3` | Vue3 工具(class 处理、dispatch 等) | | `@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、打印等) |
| `@r-utils/uview-plus` | `packages/uview-plus` | uview-plus 组合式 API HooksPicker、Calendar 等) |
| `@r-utils/vue2` | `packages/vue2` | Vue2 工具(visibility 插件) | | `@r-utils/vue2` | `packages/vue2` | Vue2 工具(visibility 插件) |
## 环境要求 ## 环境要求
@@ -74,6 +75,9 @@ import { mergeClass, dispatch } from '@r-utils/vue3';
// uni-app 工具 // uni-app 工具
import { Request } from '@r-utils/uni-app'; import { Request } from '@r-utils/uni-app';
// uview-plus Hooks
import { usePickerSingle, usePicker, useCalendar } from '@r-utils/uview-plus';
// Vue2 插件 // Vue2 插件
import { VisibilityPlugin } from '@r-utils/vue2'; import { VisibilityPlugin } from '@r-utils/vue2';
Vue.use(VisibilityPlugin); Vue.use(VisibilityPlugin);
@@ -139,44 +143,42 @@ pnpm commit
## 发布 ## 发布
### 发布到私有 npm 仓库 ### 1. 版本管理(standard-version
使用 `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
```
### 2. 构建并发布到私有 npm 仓库
项目已配置发布到私有 npm 仓库 `http://npm.nps.yunvip123.cn` 项目已配置发布到私有 npm 仓库 `http://npm.nps.yunvip123.cn`
#### Windows (PowerShell) #### Windows (PowerShell)
```powershell ```powershell
# 发布所有子包(默认 patch 版本)
powershell scripts/publish.ps1 powershell scripts/publish.ps1
# 指定版本类型
powershell scripts/publish.ps1 patch # 1.0.0 -> 1.0.1
powershell scripts/publish.ps1 minor # 1.0.0 -> 1.1.0
powershell scripts/publish.ps1 major # 1.0.0 -> 2.0.0
``` ```
#### Linux/macOS (Bash) #### Linux/macOS (Bash)
```bash ```bash
# 发布所有子包(默认 patch 版本)
bash scripts/publish.sh bash scripts/publish.sh
# 指定版本类型
bash scripts/publish.sh patch # 1.0.0 -> 1.0.1
bash scripts/publish.sh minor # 1.0.0 -> 1.1.0
bash scripts/publish.sh major # 1.0.0 -> 2.0.0
``` ```
发布脚本会自动执行以下步骤: 发布脚本会自动执行以下步骤:
1. 检查 Git 工作区状态 1. 检查 Git 工作区状态
2. 运行测试(确保代码质量) 2. 运行测试(确保代码质量)
3. 更新所有子包的版本号 3. 构建所有子包
4. 构建所有子包 4. 依次发布 `@r-utils/common``@r-utils/vue3``@r-utils/uni-app``@r-utils/vue2`
5. 提交版本更新到 Git 5. 询问是否推送到远程仓库
6. 依次发布 `@r-utils/common``@r-utils/vue3``@r-utils/uni-app``@r-utils/vue2`
7. 创建 Git 标签(如 `v1.0.1`
8. 询问是否推送到远程仓库
### 安装已发布的包 ### 安装已发布的包
@@ -191,6 +193,7 @@ echo "registry=http://npm.nps.yunvip123.cn" > .npmrc
pnpm add @r-utils/common pnpm add @r-utils/common
pnpm add @r-utils/vue3 pnpm add @r-utils/vue3
pnpm add @r-utils/uni-app pnpm add @r-utils/uni-app
pnpm add @r-utils/uview-plus
pnpm add @r-utils/vue2 pnpm add @r-utils/vue2
``` ```
@@ -210,6 +213,7 @@ r-util-js/
│ │ └── tsconfig.json │ │ └── tsconfig.json
│ ├── vue3/ # @r-utils/vue3 │ ├── vue3/ # @r-utils/vue3
│ ├── uni-app/ # @r-utils/uni-app │ ├── uni-app/ # @r-utils/uni-app
│ ├── uview-plus/ # @r-utils/uview-plus
│ └── vue2/ # @r-utils/vue2 │ └── vue2/ # @r-utils/vue2
├── eslint.config.js # ESLint v9 flat config ├── eslint.config.js # ESLint v9 flat config
├── prettier.config.js ├── prettier.config.js
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
};
+38 -106
View File
@@ -33,12 +33,8 @@ module.exports = {
"revert", "revert",
], ],
], ],
// scope 可选,但如果提供必须是以下之一 // scope 可自定义(保留 prompt 的推荐列表,不做白名单限制)
"scope-enum": [ "scope-enum": [0],
2,
"always",
["common", "vue3", "vue2", "uni-app", "deps", "release"],
],
// subject 不能为空 // subject 不能为空
"subject-empty": [2, "never"], "subject-empty": [2, "never"],
// subject 不能以句号结尾 // subject 不能以句号结尾
@@ -74,108 +70,44 @@ module.exports = {
{ value: "deps", name: "deps: 依赖更新" }, { value: "deps", name: "deps: 依赖更新" },
{ value: "release", name: "release: 版本发布" }, { value: "release", name: "release: 版本发布" },
], ],
allowCustomScopes: false, allowCustomScopes: true,
allowEmptyScopes: true, allowEmptyScopes: true,
// 破坏性变更默认跳过;如需声明可手动在提交信息中添加 BREAKING CHANGE
allowBreakingChanges: [],
messages: { messages: {
skip: "跳过", type: '选择你要提交的类型:',
max: "最多 %d 个字符", scope: '选择一个提交范围(可选):',
min: "最少 %d 个字符", customScope: '请输入自定义范围:',
emptyWarning: "不能为空", subject: '请简短描述这次改动:\n',
upperLimitWarning: "超出字数限制", body: '请详细描述这次改动(可选,用 "|" 换行):\n',
lowerLimitWarning: "低于字数限制", breaking: '是否有破坏性变更(可选,用 "|" 换行):\n',
}, footerPrefixesSelect: '选择关联的 ISSUE 类型(可选):',
questions: { customFooterPrefix: '输入 ISSUE 前缀:',
type: { footer: '列出关联的 ISSUE,如: #31, #34:\n',
description: "选择提交类型", generatingByAI: '正在通过 AI 生成提交信息...',
enum: { generatedSelectByAI: '选择一个 AI 生成的 subject:',
feat: { confirmCommit: '是否确认提交以上信息?',
description: "一个新特性",
title: "新特性",
emoji: "✨",
},
fix: {
description: "一个漏洞修复",
title: "漏洞修复",
emoji: "🐛",
},
docs: {
description: "仅更改文档",
title: "文档",
emoji: "📚",
},
style: {
description: "不影响代码含义的更改(空白符、格式化、缺少分号等)",
title: "样式",
emoji: "💎",
},
refactor: {
description: "既不修复错误也不添加功能的代码更改",
title: "重构",
emoji: "📦",
},
perf: {
description: "提高性能的代码更改",
title: "性能提升",
emoji: "🚀",
},
test: {
description: "添加缺失的测试或纠正现有的测试",
title: "测试",
emoji: "🚨",
},
build: {
description:
"影响构建系统或外部依赖项的更改(例如:gulp、broccoli、npm",
title: "构建",
emoji: "🛠",
},
ci: {
description:
"更改我们的集成配置文件和脚本(例如:Travis、Circle、BrowserStack、SauceLabs",
title: "集成",
emoji: "⚙️",
},
chore: {
description: "不修改源码或测试文件的其他更改",
title: "杂项",
emoji: "♻️",
},
revert: {
description: "恢复之前的提交",
title: "还原",
emoji: "🗑",
},
},
},
scope: {
description: "更改的范围(scope)(例:common、vue3、uni-app",
},
subject: {
description: "主题(subject",
},
body: {
description: "主体(body",
},
isBreaking: {
description: "是否有破坏性更改(BREAKING CHANGE)?",
},
breakingBody: {
description:
"破坏性更改(BREAKING CHANGE)需要一个主体(body),请输入详细描述",
},
breaking: {
description: "描述破坏性更改(BREAKING CHANGE",
},
isIssueAffected: {
description: "此改变是否影响打开的问题(open ssues",
},
issuesBody: {
description: "如果问题关闭,则需要提交一个主体(body),请输入详细描述",
},
issues: {
description:
'添加问题参考(issue references (例: "fix #123"、"re #123"',
},
}, },
// subject 最小长度提示(中文化 "[x more chars needed]"
minSubjectLength: 4,
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:' },
],
// issue 前缀也可以用中文说明
issuePrefixs: [
{ value: 'link', name: 'link: 关联 ISSUE' },
{ value: 'close', name: 'close: 关闭 ISSUE' },
],
}, },
}; };
+54 -6
View File
@@ -1,9 +1,16 @@
import js from "@eslint/js"; import js from "@eslint/js";
import ts from "typescript-eslint"; import ts from "typescript-eslint";
import prettier from "eslint-config-prettier"; import prettier from "eslint-config-prettier";
import prettierPlugin from "eslint-plugin-prettier";
import globals from "globals"; import globals from "globals";
import importPlugin from "eslint-plugin-import-x";
export default [ export default [
// 应用推荐规则
js.configs.recommended,
...ts.configs.recommended,
prettier,
// 基础配置 // 基础配置
{ {
files: ["**/*.{js,mjs,cjs,ts,vue}"], files: ["**/*.{js,mjs,cjs,ts,vue}"],
@@ -27,7 +34,17 @@ export default [
"patches/", "patches/",
"wxcomponents/", "wxcomponents/",
"lib/", "lib/",
"scripts/",
], ],
plugins: { prettier: prettierPlugin, "import-x": importPlugin },
settings: {
"import-x/resolver": {
typescript: {
project: "tsconfig.eslint.json",
},
node: true,
},
},
languageOptions: { languageOptions: {
globals: { globals: {
...globals.node, ...globals.node,
@@ -42,15 +59,46 @@ export default [
}, },
}, },
rules: { rules: {
...importPlugin.flatConfigs.recommended.rules,
// Prettier 规则
"prettier/prettier": "warn",
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
},
// 应用推荐规则 // Import 规则
js.configs.recommended, "import-x/no-self-import": "error",
...ts.configs.recommended, "import-x/no-cycle": "error",
prettier, "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",
},
},
],
},
},
// 测试文件覆盖配置 // 测试文件覆盖配置
{ {
-198
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
View File
File diff suppressed because it is too large Load Diff
+14 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "@r-utils/root", "name": "@r-utils/root",
"version": "1.1.0", "version": "1.3.0",
"private": true, "private": true,
"description": "前端工具库", "description": "前端工具库",
"keywords": [ "keywords": [
@@ -27,44 +27,46 @@
}, },
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=18.12.0" "node": ">=18.12.0",
"pnpm": ">=10.0.0"
}, },
"packageManager": "pnpm@8.15.6", "packageManager": "pnpm@10.32.1",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"build": "pnpm -r run build", "build": "pnpm -r run build",
"prepare": "husky install", "prepare": "husky install",
"lint": "eslint --ext .js,ts --fix src", "lint": "eslint --fix packages/*/src",
"format": "prettier --write src", "format": "prettier --write packages/*/src",
"release": "standard-version", "release": "standard-version",
"commit": "cz", "commit": "cz",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"test": "jest" "test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-typescript": "^7.28.5",
"@commitlint/config-conventional": "^20.5.0", "@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@jest/globals": "^30.3.0",
"@typescript-eslint/eslint-plugin": "^8.57.1", "@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1", "@typescript-eslint/parser": "^8.57.1",
"babel-jest": "^30.3.0", "@vitest/coverage-v8": "catalog:",
"commitizen": "^4.3.1", "commitizen": "^4.3.1",
"cz-git": "^1.12.0", "cz-git": "^1.12.0",
"eslint": "^10.0.3", "eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-define-config": "^1.18.0", "eslint-define-config": "^1.18.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.5",
"globals": "^17.4.0", "globals": "^17.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"inquirer": "^9.3.8", "inquirer": "^9.3.8",
"jest": "^30.3.0",
"lint-staged": "^16.4.0", "lint-staged": "^16.4.0",
"vitest": "catalog:",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"tslib": "^2.8.1", "tslib": "^2.8.1",
-3
View File
@@ -1,3 +0,0 @@
import rootConfig from "../../eslint.config.js";
export default [...rootConfig];
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@r-utils/common", "name": "@r-utils/common",
"version": "1.0.2", "version": "1.0.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@r-utils/common", "name": "@r-utils/common",
"version": "1.0.2", "version": "1.0.3",
"dependencies": { "dependencies": {
"@r-utils/common": "file:", "@r-utils/common": "file:",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
+29 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@r-utils/common", "name": "@r-utils/common",
"version": "1.0.2", "version": "1.3.0",
"private": false, "private": false,
"description": "js通用工具库", "description": "js通用工具库",
"type": "module", "type": "module",
@@ -20,6 +20,27 @@
"utils", "utils",
"common" "common"
], ],
"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"
},
"packageManager": "pnpm@10.32.1",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"watch": "vite build --watch", "watch": "vite build --watch",
@@ -28,15 +49,19 @@
"release": "standard-version", "release": "standard-version",
"commit": "cz", "commit": "cz",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"test": "jest" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"lodash-es": "^4.17.21", "js-easing-functions": "^1.0.3",
"lodash-es": "catalog:",
"text-encoding": "^0.7.0" "text-encoding": "^0.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/lodash-es": "^4.17.12" "@types/lodash-es": "catalog:",
"vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vitest": "catalog:"
} }
} }
+2
View File
@@ -3,3 +3,5 @@ export * from "./permission";
export * from "./printer"; export * from "./printer";
export * from "./time"; export * from "./time";
export * from "./timer"; export * from "./timer";
export * from "./input-rule";
export * from "./ui";
+163
View File
@@ -0,0 +1,163 @@
/*
* @file: /src/input-rule.ts
* @description: 限制输入框输入的规则
* @author: tsl (randy1924@163.com)
* @date: 2026-03-18 13:37:11
* @lastModified: 2026-03-18 15:04:44
* @lastModifiedBy: tsl (randy1924@163.com)
*/
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; // 不允许输入正负号
}
/**
* 根据规则获取目标值
* @example
* 限制输入的最小值:0,最大值:99999.99,必填,小数位数:2
* const value = getValueOfRule(e.detail.value, {
* min: 0,
* max: 99999.99,
* required: true,
* digits: 2,
* })
*/
export function getValueOfRule(
value: InputValue,
options: InputOptions,
callback?: (value: InputValue) => void,
): InputValue {
console.log("getValueOfRule", value, options);
const { text } = options;
let targetValue: InputValue;
if (text) {
targetValue = getTextTargetValue(value, options);
} else {
targetValue = getNumberTargetValue(value, options);
}
console.log("targetValue", targetValue);
callback?.(targetValue);
return targetValue;
}
/**
* 根据输入的获取处理后值-文本类型
*/
function getTextTargetValue(innerValue: InputValue, options: InputOptions): InputValue {
const { required, init, pattern, number } = options;
innerValue ??= "";
innerValue = String(innerValue);
let targetValue: string | number = innerValue;
// 如果提供了pattern参数,检查输入是否匹配该模式
if (pattern && !new RegExp(pattern).test(innerValue)) {
return "";
}
if (!/^[+-]?\d+(\.\d+)?$/.test(innerValue)) {
if (required) {
if (init !== undefined) {
targetValue = String(init);
}
}
}
if (number) {
targetValue = Number(targetValue);
}
return targetValue;
}
/**
* 根据 floor/ceil 选项选择取整方式
*/
function getRoundFn(floor?: boolean, ceil?: boolean): (n: number) => number {
if (floor) return Math.floor;
if (ceil) return Math.ceil;
return Math.round;
}
/**
* 判断字符串是否为合法数字输入
*/
function isValidNumberInput(value: string, noSign?: boolean): boolean {
if (!value) return false;
if (!/^[+-]?\d+(\.\d+)?$/.test(value)) return false;
if (noSign && /[+-]/.test(value)) return false;
return true;
}
/**
* 获取非法输入时的回退值
*/
function getFallbackValue(options: InputOptions): string {
const { required, init, min } = options;
if (!required) return "";
if (init != null) return String(init);
return min == null ? "0" : String(min);
}
/**
* 对数字应用精度和取整处理
*/
function applyPrecision(num: number, options: InputOptions): number {
const { digits, integer, floor, ceil, min, max } = options;
let result = num;
if (min != null) result = Math.max(result, min);
if (max != null) result = Math.min(result, max);
const roundFn = getRoundFn(floor, ceil);
if (digits != null) {
const precision = Math.pow(10, digits);
result = roundFn(result * precision) / precision;
}
if (integer) {
result = roundFn(result);
}
return result;
}
/**
* 根据输入的获取处理后值-数字类型
*/
function getNumberTargetValue(innerValue: InputValue, options: InputOptions): InputValue {
const { number, keepDecimal, noSign, digits } = options;
const strValue = String(innerValue ?? "");
let targetValue: string;
if (isValidNumberInput(strValue, noSign)) {
targetValue = String(applyPrecision(Number(strValue), options));
} else {
targetValue = getFallbackValue(options);
}
if (digits != null && keepDecimal) {
targetValue = Number(targetValue).toFixed(digits);
}
return number ? Number(targetValue) : targetValue;
}
+47
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
});
}
+10
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";
+166
View File
@@ -0,0 +1,166 @@
import { test, expect, describe } from "vitest";
import { getValueOfRule, InputOptions } from "../src/input-rule";
describe("getValueOfRule - 文本模式 (text: true)", () => {
const textOpts: InputOptions = { text: true };
test("普通文本直接返回", () => {
expect(getValueOfRule("hello", { ...textOpts })).toBe("hello");
});
test("null 值返回空字符串", () => {
expect(getValueOfRule(null, { ...textOpts })).toBe("");
});
test("required + init:非数字文本回退到 init", () => {
expect(getValueOfRule("abc", { ...textOpts, required: true, init: "默认" })).toBe("默认");
});
test("required 无 init:非数字文本保持原值", () => {
expect(getValueOfRule("abc", { ...textOpts, required: true })).toBe("abc");
});
test("number 选项将结果转为数字", () => {
expect(getValueOfRule("123", { ...textOpts, number: true })).toBe(123);
});
test("pattern 匹配失败返回空字符串", () => {
expect(getValueOfRule("abc", { ...textOpts, pattern: "^\\d+$" })).toBe("");
});
test("pattern 匹配成功返回原值", () => {
expect(getValueOfRule("123", { ...textOpts, pattern: "^\\d+$" })).toBe("123");
});
});
describe("getValueOfRule - 数字模式", () => {
test("正常数字字符串返回字符串", () => {
expect(getValueOfRule("42", {})).toBe("42");
});
test("number 选项返回数字类型", () => {
expect(getValueOfRule("42", { number: true })).toBe(42);
});
test("非法输入 + required 回退到 init", () => {
expect(getValueOfRule("abc", { required: true, init: 10 })).toBe("10");
});
test("非法输入 + required 无 init 回退到 min", () => {
expect(getValueOfRule("abc", { required: true, min: 5 })).toBe("5");
});
test("非法输入 + required 无 init 无 min 回退到 0", () => {
expect(getValueOfRule("abc", { required: true })).toBe("0");
});
test("非法输入 + 非 required 返回空字符串", () => {
expect(getValueOfRule("abc", {})).toBe("");
});
test("null 值 + required 回退到 init", () => {
expect(getValueOfRule(null, { required: true, init: 0 })).toBe("0");
});
test("null 值 + 非 required 返回空字符串", () => {
expect(getValueOfRule(null, {})).toBe("");
});
});
describe("getValueOfRule - min/max 限制", () => {
test("值小于 min 时限制为 min", () => {
expect(getValueOfRule("3", { min: 5 })).toBe("5");
});
test("值大于 max 时限制为 max", () => {
expect(getValueOfRule("100", { max: 50 })).toBe("50");
});
test("值在 min/max 范围内保持不变", () => {
expect(getValueOfRule("25", { min: 0, max: 50 })).toBe("25");
});
test("负数受 min 限制", () => {
expect(getValueOfRule("-10", { min: 0 })).toBe("0");
});
});
describe("getValueOfRule - digits 小数位数", () => {
test("保留 2 位小数(四舍五入)", () => {
expect(getValueOfRule("3.456", { digits: 2 })).toBe("3.46");
});
test("保留 0 位小数", () => {
expect(getValueOfRule("3.6", { digits: 0 })).toBe("4");
});
test("floor 向下取整小数", () => {
expect(getValueOfRule("3.456", { digits: 2, floor: true })).toBe("3.45");
});
test("ceil 向上取整小数", () => {
expect(getValueOfRule("3.451", { digits: 2, ceil: true })).toBe("3.46");
});
test("keepDecimal 保留尾部零", () => {
expect(getValueOfRule("3", { digits: 2, keepDecimal: true })).toBe("3.00");
});
test("keepDecimal + number 返回数字类型", () => {
expect(getValueOfRule("3.10", { digits: 2, keepDecimal: true, number: true })).toBe(3.1);
});
});
describe("getValueOfRule - integer 整数模式", () => {
test("integer 四舍五入取整", () => {
expect(getValueOfRule("3.6", { integer: true })).toBe("4");
});
test("integer + floor 向下取整", () => {
expect(getValueOfRule("3.9", { integer: true, floor: true })).toBe("3");
});
test("integer + ceil 向上取整", () => {
expect(getValueOfRule("3.1", { integer: true, ceil: true })).toBe("4");
});
});
describe("getValueOfRule - noSign 不允许正负号", () => {
test("带正号的输入被视为非法", () => {
expect(getValueOfRule("+5", { noSign: true, required: true, init: 0 })).toBe("0");
});
test("带负号的输入被视为非法", () => {
expect(getValueOfRule("-5", { noSign: true, required: true, init: 0 })).toBe("0");
});
test("无符号数字正常通过", () => {
expect(getValueOfRule("5", { noSign: true })).toBe("5");
});
});
describe("getValueOfRule - callback", () => {
test("callback 接收最终处理后的值", () => {
let received: unknown;
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");
});
test("超出 max 时截断到 max", () => {
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");
});
test("负数输入限制到 min:0", () => {
expect(getValueOfRule("-5", { min: 0, max: 100, required: true })).toBe("0");
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect } from "@jest/globals"; import { test, expect } from "vitest";
import { Permission } from "../src/permission"; import { Permission } from "../src/permission";
test("测试 Permission#isValid()", () => { test("测试 Permission#isValid()", () => {
-1
View File
@@ -7,6 +7,5 @@
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"outDir": "./dist"
} }
} }
+12 -12
View File
@@ -1,31 +1,31 @@
import { defineConfig } from 'vite'; import { resolve } from "node:path";
import dts from 'vite-plugin-dts'; import { fileURLToPath } from "node:url";
import { resolve } from 'node:path'; import { defineConfig } from "vite";
import { fileURLToPath } from 'node:url'; import dts from "vite-plugin-dts";
const __dirname = fileURLToPath(new URL('.', import.meta.url)); const __dirname = fileURLToPath(new URL(".", import.meta.url));
export default defineConfig({ export default defineConfig({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/index.ts'), entry: resolve(__dirname, "src/index.ts"),
formats: ['es', 'cjs'], formats: ["es", "cjs"],
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`, fileName: (format) => `index.${format === "es" ? "mjs" : "cjs"}`,
}, },
rollupOptions: { rollupOptions: {
external: ['dayjs', 'lodash-es', 'text-encoding', 'tslib'], external: ["dayjs", "lodash-es", "text-encoding", "tslib"],
}, },
sourcemap: true, sourcemap: true,
}, },
resolve: { resolve: {
alias: { alias: {
'@': resolve(__dirname, 'src'), "@": resolve(__dirname, "src"),
}, },
}, },
plugins: [ plugins: [
dts({ dts({
include: ['src'], include: ["src"],
outDir: 'dist', outDir: "dist",
}), }),
], ],
}); });
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@r-utils/uni-app", "name": "@r-utils/uni-app",
"version": "1.0.1", "version": "1.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@r-utils/uni-app", "name": "@r-utils/uni-app",
"version": "1.0.1", "version": "1.0.2",
"dependencies": { "dependencies": {
"@r-utils/root": "file:", "@r-utils/root": "file:",
"@r-utils/uni-app": "file:", "@r-utils/uni-app": "file:",
+39 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@r-utils/uni-app", "name": "@r-utils/uni-app",
"version": "1.0.1", "version": "1.3.0",
"private": false, "private": false,
"description": "uni-app工具库", "description": "uni-app工具库",
"type": "module", "type": "module",
@@ -19,6 +19,27 @@
"keywords": [ "keywords": [
"uni-app" "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"
},
"packageManager": "pnpm@10.32.1",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"watch": "vite build --watch", "watch": "vite build --watch",
@@ -26,16 +47,31 @@
"release": "standard-version", "release": "standard-version",
"commit": "cz", "commit": "cz",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"test": "jest" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@r-utils/common": "workspace:^" "@r-utils/common": "workspace:^",
"chroma-js": "catalog:",
"lodash-es": "catalog:",
"qs": "catalog:"
}, },
"peerDependencies": { "peerDependencies": {
"@dcloudio/uni-app": ">=3.0.0",
"vue": "^3.3.0" "vue": "^3.3.0"
}, },
"peerDependenciesMeta": {
"@dcloudio/uni-app": {
"optional": true
}
},
"devDependencies": { "devDependencies": {
"@dcloudio/types": "^3.0.7", "@dcloudio/types": "^3.0.7",
"@dcloudio/uni-app": "3.0.0-4070520250711001",
"@types/chroma-js": "catalog:",
"@types/lodash-es": "catalog:",
"@types/qs": "catalog:",
"vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vue": "^3.3.0" "vue": "^3.3.0"
} }
} }
+53 -9
View File
@@ -24,7 +24,6 @@ export function getCurrentPage() {
return pages[pages.length - 1]; return pages[pages.length - 1];
} }
/** /**
* 使用 TypeScript 工具类型实现自动类型推断的 toPromise * 使用 TypeScript 工具类型实现自动类型推断的 toPromise
* *
@@ -39,10 +38,17 @@ export function getCurrentPage() {
// type ExtractFirstParameter<T> = Parameters<T>[0]; // 这样也可以,但需要T是函数类型 // type ExtractFirstParameter<T> = Parameters<T>[0]; // 这样也可以,但需要T是函数类型
// 方法2:使用条件类型实现(更灵活,等价于 Parameters<T>[0] // 方法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 的思路,但提取回调参数) // 提取 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参数) // 提取函数参数类型并排除回调函数(用于args参数)
type ExtractOptions<T> = T extends (options: infer P) => unknown 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( export function showToast(
title: string, title: string,
icon: "success" | "loading" | "error" | "none" = "none", icon: "success" | "loading" | "error" | "none" = "none",
@@ -139,12 +144,9 @@ export async function getCanvasImageData(
) { ) {
const context = uni.createCanvasContext(canvasId, thisArg); const context = uni.createCanvasContext(canvasId, thisArg);
const imgRes = await toPromise( const imgRes = await toPromise(uni.downloadFile, {
uni.downloadFile,
{
url, url,
}, });
);
console.log(imgRes); console.log(imgRes);
context.drawImage(imgRes.tempFilePath, x, y, width, height); context.drawImage(imgRes.tempFilePath, x, y, width, height);
@@ -243,3 +245,45 @@ export function checkAndroidPermission(permissionList: string[]) {
}); });
// #endif // #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));
}
@@ -0,0 +1,450 @@
/*
* @file /src/vue3/hooks/components.ts
* @description
* @author tsl (randy1924@163.com)
* @date 2026-03-19 11:11:11
* @lastModified 2026-03-26 09:16:44
* @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,
};
}
+13
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";
+280
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,
};
}
+73
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,
};
}
@@ -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 };
}
+10
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";
-1
View File
@@ -8,6 +8,5 @@
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"outDir": "./dist"
} }
} }
+4 -4
View File
@@ -1,10 +1,10 @@
/// <reference types="@dcloudio/types" /> /// <reference types="@dcloudio/types" />
declare namespace UniNamespace { declare namespace UniNamespace {
type LocaldataItem = { type LocaldataItem<T=string> = {
text: string; text: string;
value: string; value: T;
children?: LocaldataItem[]; children?: LocaldataItem<T>[];
}; };
type LocaldataNodeclickValue = { type LocaldataNodeclickValue = {
text: string; text: string;
@@ -15,7 +15,7 @@ declare namespace UniNamespace {
type Event<T> = { detail: T }; type Event<T> = { detail: T };
type InputEvent = Event<{ value: string }>; type InputEvent = Event<{ value: string }>;
type PickerEvent = 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 ScrollEvent = Event<{ scrollTop: number }>;
type SwiperEvent = Event<{ current: number }>; type SwiperEvent = Event<{ current: number }>;
type UniDataPickerEvent = Event<{ value: LocaldataChangeValue[] }>; type UniDataPickerEvent = Event<{ value: LocaldataChangeValue[] }>;
+109
View File
@@ -0,0 +1,109 @@
# @r-utils/uview-plus
基于 [uview-plus](https://uview-plus.jiangruyi.com/) 的 Vue3 组合式 API 工具 Hooks,适用于 uni-app 项目。
## 安装
```bash
pnpm add @r-utils/uview-plus
```
## 使用
```ts
import { usePickerSingle, usePicker, useCalendar } from '@r-utils/uview-plus'
```
## API
### `usePickerSingle(options)`
单列 Picker 封装。
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `value` | `unknown \| Ref<unknown>` | `null` | 选中的值 |
| `show` | `boolean \| Ref<boolean>` | `false` | 是否显示 |
| `indexes` | `Array<number \| null> \| Ref<...>` | `[null]` | 选中的索引 |
| `list` | `PickerColumns[0] \| Ref<...>` | `[]` | 列数据 |
| `textName` | `string` | `'text'` | 显示字段名 |
| `valueName` | `string` | `'value'` | 值字段名 |
| `placeholder` | `string` | `'请选择'` | 占位文本 |
**返回值:**
```ts
{
value, show, indexes, columns, text, defaultIndex,
showPicker, hidePicker, handleConfirm, handleClose
}
```
---
### `usePicker(options)`
多列 Picker 封装。
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `value` | `unknown[] \| Ref<unknown[]>` | `[]` | 选中的值数组 |
| `show` | `boolean \| Ref<boolean>` | `false` | 是否显示 |
| `indexes` | `Array<number \| null> \| Ref<...>` | `[]` | 选中的索引数组 |
| `columns` | `PickerColumns \| Ref<PickerColumns>` | `[]` | 列数据 |
| `textName` | `string` | `'text'` | 显示字段名 |
| `valueName` | `string` | `'value'` | 值字段名 |
| `placeholder` | `string` | `'请选择'` | 占位文本 |
| `separator` | `string` | `' '` | 多列值拼接分隔符 |
**返回值:**
```ts
{
value, show, indexes, columns, text, defaultIndex,
showPicker, hidePicker, handleConfirm, handleClose
}
```
---
### `useCalendar(options)`
日历选择封装。
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `value` | `string \| string[] \| Ref<...>` | `null` | 选中的日期 |
| `show` | `boolean \| Ref<boolean>` | `false` | 是否显示 |
| `mode` | `'single' \| 'multiple' \| 'range' \| Ref<...>` | `'single'` | 日历模式 |
| `placeholder` | `string` | `'请选择'` | 占位文本 |
**返回值:**
```ts
{
value, show, text,
showCalendar, hideCalendar, handleConfirm, handleClose
}
```
## 类型声明
包内置了 `UViewPlus` namespace 类型声明,无需额外引入。
```ts
declare namespace UViewPlus {
type PickerColumns = any[][];
type PickerValue<T extends PickerColumns = PickerColumns> = T[number][number][];
type PickerConfirmEvent<T extends PickerColumns = PickerColumns> = {
indexs: number[];
value: PickerValue<T>;
values: T;
};
type CalendarConfirmEvent = string[];
}
```
## 许可证
ISC
+74
View File
@@ -0,0 +1,74 @@
{
"name": "@r-utils/uview-plus",
"version": "1.2.1",
"private": false,
"description": "uview-plus 组合式 API Hooks",
"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",
"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",
"lint": "eslint --ext .js,ts --fix src",
"release": "standard-version",
"commit": "cz",
"lint-staged": "lint-staged",
"test": "jest"
},
"peerDependencies": {
"uview-plus": ">=3.0.0",
"vue": "^3.3.0"
},
"peerDependenciesMeta": {
"uview-plus": {
"optional": true
}
},
"dependencies": {
"lodash-es": "catalog:"
},
"devDependencies": {
"@types/lodash-es": "catalog:",
"vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vitest": "catalog:",
"vue": "^3.5.13"
}
}
+67
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,
};
}
@@ -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,
};
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./picker-single";
export * from "./picker";
export * from "./datetime-picker";
export * from "./calendar";
+139
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,
};
}
+123
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,
};
}
+95
View File
@@ -0,0 +1,95 @@
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();
});
});
@@ -0,0 +1,73 @@
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();
});
});
@@ -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();
});
});
+134
View File
@@ -0,0 +1,134 @@
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();
});
});
+11
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/*"]
},
}
}
+16
View File
@@ -0,0 +1,16 @@
/// <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[];
}
+31
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-es'],
},
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
plugins: [
dts({
include: ['src', 'types'],
outDir: 'dist',
}),
],
});
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@r-utils/vue2", "name": "@r-utils/vue2",
"version": "1.0.1", "version": "1.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@r-utils/vue2", "name": "@r-utils/vue2",
"version": "1.0.1", "version": "1.0.2",
"dependencies": { "dependencies": {
"@r-utils/vue2": "file:", "@r-utils/vue2": "file:",
"lodash": "^4.17.21" "lodash": "^4.17.21"
+25 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@r-utils/vue2", "name": "@r-utils/vue2",
"version": "1.0.1", "version": "1.3.0",
"private": false, "private": false,
"description": "Vue2工具库", "description": "Vue2工具库",
"type": "module", "type": "module",
@@ -19,6 +19,27 @@
"keywords": [ "keywords": [
"vue2" "vue2"
], ],
"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"
},
"packageManager": "pnpm@10.32.1",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"watch": "vite build --watch", "watch": "vite build --watch",
@@ -26,12 +47,14 @@
"release": "standard-version", "release": "standard-version",
"commit": "cz", "commit": "cz",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"test": "jest" "test": "vitest run"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^2.7.0" "vue": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {
"vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vue": "^2.7.0" "vue": "^2.7.0"
} }
} }
-1
View File
@@ -7,6 +7,5 @@
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"outDir": "./dist"
} }
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@r-utils/vue3", "name": "@r-utils/vue3",
"version": "1.0.2", "version": "1.0.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@r-utils/vue3", "name": "@r-utils/vue3",
"version": "1.0.2", "version": "1.0.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@r-utils/vue3": "file:", "@r-utils/vue3": "file:",
+29 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@r-utils/vue3", "name": "@r-utils/vue3",
"version": "1.0.2", "version": "1.3.0",
"private": false, "private": false,
"description": "Vue3 工具", "description": "Vue3 工具",
"type": "module", "type": "module",
@@ -19,6 +19,26 @@
"keywords": [ "keywords": [
"vue3" "vue3"
], ],
"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"
},
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"watch": "vite build --watch", "watch": "vite build --watch",
@@ -26,14 +46,19 @@
"release": "standard-version", "release": "standard-version",
"commit": "cz", "commit": "cz",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"test": "jest" "test": "vitest run"
}, },
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"vue": "^3.3.0" "vue": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"vue": "^3.3.0" "vite": "catalog:",
"vite-plugin-dts": "catalog:",
"vue": "^3.5.13"
}, },
"packageManager": "pnpm@8.15.6" "packageManager": "pnpm@10.32.1",
"dependencies": {
"@r-utils/common": "workspace:^"
}
} }
+120
View File
@@ -0,0 +1,120 @@
/*
* @file /src/hooks/list.ts
* @description
* @author tsl (randy1924@163.com)
* @date 2026-03-19 11:11:11
* @lastModified 2026-03-20 10:43:38
* @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,
};
}
+115
View File
@@ -0,0 +1,115 @@
import { getValueOfRule } from "@r-utils/common";
import { ref, computed, toRef } from "vue";
import type { InputOptions, InputValue } from "@r-utils/common";
import type { Ref } from "vue";
/**
* 倒计时工具
* @param initTime 初始时间,秒
*/
export function useTimer(initTime = 0) {
const time = ref(initTime);
const timeInterval = ref();
const start = () => {
timeInterval.value = setInterval(() => {
if (time.value <= 0) {
clearInterval(timeInterval.value);
return;
}
time.value--;
}, 1000);
};
const restart = () => {
time.value = initTime;
start();
};
const stop = () => {
time.value = 0;
clearInterval(timeInterval.value);
};
return { time, start, restart, timeInterval, stop };
}
type UseNumberSetFn = (v: string | number) => string | number;
const defaultUseNumberSetFn: UseNumberSetFn = (v: string | number) => String(v);
/**
* 把响应式变量转为数字输出
* @param value 要监听的值
* @param setFn 在监听值改变时,处理函数
*/
export function useNumber(
value: Ref<string | number>,
setFn: UseNumberSetFn = defaultUseNumberSetFn,
) {
const int = computed<number>({
// 总是返回整数
get: () => Number.parseInt(String(value.value)),
set: (v) => {
value.value = setFn(v);
},
});
const float = computed<number>({
// 总是返回 float
get: () => Number.parseFloat(String(value.value)),
set: (v) => {
value.value = setFn(v);
},
});
const number = computed<number>({
// 总是返回 Number
get: () => Number(value.value),
set: (v) => {
value.value = setFn(v);
},
});
return { number, int, float };
}
/**
* 监听一个响应式变量,转为字符串输出
* @param value 要监听的值
*/
export function useString(value: Ref<string | number>) {
const string = computed<string>({
get: () => String(value.value),
set: (v) => {
value.value = v;
},
});
return { string };
}
interface UseValueOfRuleOptions {
value: InputValue | Ref<InputValue>;
rule: InputOptions;
}
/**
* 按自定义规则转换数字或文本
* @param options
*/
export function useValueOfRule(options: UseValueOfRuleOptions) {
const value = toRef(options.value);
const rule = toRef(options.rule);
function handleChange() {
const newValue = getValueOfRule(value.value, rule.value);
value.value = newValue;
}
return {
value,
rule,
handleChange,
};
}
-1
View File
@@ -7,6 +7,5 @@
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"outDir": "./dist"
} }
} }
+59
View File
@@ -0,0 +1,59 @@
/*
* @file: /types/qqmap-wx-jssdk.d.ts
* @description:
* @author: tsl (randy1924@163.com)
* @date: 2026-03-19 11:26:24
* @lastModified: 2026-03-19 11:26:24
* @lastModifiedBy: tsl (randy1924@163.com)
*/
import * as QQMapWX from "@jonny1994/qqmap-wx-jssdk";
export * from "@jonny1994/qqmap-wx-jssdk";
/**
* 行政区划列表
* @example
* cidx: [103, 118]
* fullname: "张家口市"
* id: "130700"
* location: {lat: 40.82444, lng: 114.88755}
* name: "张家口"
* pinyin: ["zhang", "jia", "kou"]
*/
export interface GetCityListSuccessResultResult {
/**
* 行政区划唯一标识
* @example "110000"
*/
id: number;
/**
* 简称,如“内蒙古”
*/
name?: string;
/**
* 全称,如“内蒙古自治区”
* @example "北京市"
*/
fullname: string;
/**
* 中心点坐标
* @example {lat: 39.90469, lng: 116.40717}
*/
location: QQMapWX.ResultLocation;
/**
* 行政区划拼音,每一下标为一个字的全拼,如:[“nei”,“meng”,“gu”]
*/
pinyin: string[];
/**
* 子级行政区划在下级数组中的下标位置
* @example [0, 15]
*/
cidx?: number[];
}
export interface GetCityListSuccessResult extends QQMapWX.CommonResult {
/**
* 结果数组,第0项,代表一级行政区划,第1项代表二级行政区划,以此类推;使用getchildren接口时,仅为指定父级行政区划的子级
*/
result: GetCityListSuccessResultResult[];
}
+5674 -5146
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -3,3 +3,13 @@ packages:
catalog: catalog:
"webpack": ^5.80.0 "webpack": ^5.80.0
"vite": ^8.0.0
"vite-plugin-dts": ^4.5.4
"vitest": ^3.2.0
"@vitest/coverage-v8": ^3.2.0
"lodash-es": ^4.17.21
"@types/lodash-es": ^4.17.12
"chroma-js": ^3.2.0
"qs": ^6.15.0
"@types/qs": ^6.15.0
"@types/chroma-js": ^3.1.2
+8
View File
@@ -5,6 +5,7 @@ export default {
{ {
files: [ files: [
"*.js", "*.js",
"*.cjs",
"*.mjs", "*.mjs",
"*.jsx", "*.jsx",
"*.ts", "*.ts",
@@ -24,5 +25,12 @@ export default {
printWidth: 120, printWidth: 120,
}, },
}, },
{
files: ["*.ps1"],
options: {
endOfLine: "crlf",
tabWidth: 4,
},
},
], ],
}; };
+10 -58
View File
@@ -1,21 +1,11 @@
# 发布所有子包到私有 npm 仓库 # 发布所有子包到私有 npm 仓库
# 使用方法: powershell scripts/publish.ps1 [版本类型] # 使用方法: powershell scripts/publish.ps1
# 版本类型: major | minor | patch (默认 patch)
# 注意: 发布仓库地址使用各子包 package.json 中的 publishConfig.registry 配置 # 注意: 发布仓库地址使用各子包 package.json 中的 publishConfig.registry 配置
# 版本管理请先通过 pnpm release (standard-version) 完成,本脚本仅负责构建和发布
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# 获取版本类型参数,默认为 patch Write-Host "🚀 开始发布流程" -ForegroundColor Green
$VERSION_TYPE = if ($args.Count -gt 0) { $args[0] } else { "patch" }
# 验证版本类型
if ($VERSION_TYPE -notin @("major", "minor", "patch")) {
Write-Host "❌ 无效的版本类型: $VERSION_TYPE" -ForegroundColor Red
Write-Host "使用方法: powershell scripts/publish.ps1 [major|minor|patch]" -ForegroundColor Yellow
exit 1
}
Write-Host "🚀 开始发布流程 (版本类型: $VERSION_TYPE)" -ForegroundColor Green
Write-Host "" Write-Host ""
# 子包列表 # 子包列表
@@ -23,6 +13,7 @@ $PACKAGES = @(
"packages/common", "packages/common",
"packages/vue3", "packages/vue3",
"packages/uni-app", "packages/uni-app",
"packages/uview-plus",
"packages/vue2" "packages/vue2"
) )
@@ -48,19 +39,7 @@ if ($LASTEXITCODE -ne 0) {
} }
Write-Host "" Write-Host ""
# 3. 更新版本号 # 3. 构建所有包
Write-Host "📝 更新版本号 ($VERSION_TYPE)..." -ForegroundColor Cyan
foreach ($package in $PACKAGES) {
if (Test-Path $package) {
Write-Host " 更新 $package 版本号..." -ForegroundColor Gray
Push-Location $package
npm version $VERSION_TYPE --no-git-tag-version
Pop-Location
}
}
Write-Host ""
# 4. 构建所有包
Write-Host "📦 构建所有包..." -ForegroundColor Cyan Write-Host "📦 构建所有包..." -ForegroundColor Cyan
pnpm build pnpm build
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
@@ -69,22 +48,12 @@ if ($LASTEXITCODE -ne 0) {
} }
Write-Host "" Write-Host ""
# 5. 更新 README 中的版本信息 # 获取当前版本号
Write-Host "📄 更新 README..." -ForegroundColor Cyan $CURRENT_VERSION = node -p "require('./package.json').version"
$COMMON_VERSION = node -p "require('./packages/common/package.json').version" Write-Host " 当前版本: v$CURRENT_VERSION" -ForegroundColor Gray
Write-Host " 当前版本: $COMMON_VERSION" -ForegroundColor Gray
Write-Host "" Write-Host ""
# 6. 提交版本更新 # 4. 发布每个包
Write-Host "💾 提交版本更新..." -ForegroundColor Cyan
git add packages/*/package.json
git commit -m "chore(release): publish v$COMMON_VERSION"
if ($LASTEXITCODE -ne 0) {
Write-Host " 没有需要提交的更改" -ForegroundColor Gray
}
Write-Host ""
# 7. 发布每个包
Write-Host "📤 开始发布包到 npm..." -ForegroundColor Cyan Write-Host "📤 开始发布包到 npm..." -ForegroundColor Cyan
foreach ($package in $PACKAGES) { foreach ($package in $PACKAGES) {
if (Test-Path $package) { if (Test-Path $package) {
@@ -106,23 +75,6 @@ foreach ($package in $PACKAGES) {
} }
} }
# 8. 创建 Git 标签
Write-Host "🏷️ 创建 Git 标签..." -ForegroundColor Cyan
git tag "v$COMMON_VERSION"
Write-Host ""
# 9. 推送到远程仓库
Write-Host "🔼 推送到远程仓库..." -ForegroundColor Cyan
$push = Read-Host "是否推送到远程仓库? (y/N)"
if ($push -eq "y" -or $push -eq "Y") {
git push origin master
git push origin "v$COMMON_VERSION"
Write-Host "✅ 已推送到远程仓库" -ForegroundColor Green
} else {
Write-Host "⚠️ 跳过推送,请手动执行:" -ForegroundColor Yellow
Write-Host " git push origin master" -ForegroundColor Gray
Write-Host " git push origin v$COMMON_VERSION" -ForegroundColor Gray
}
Write-Host ""
Write-Host "🎉 所有包发布完成!版本: v$COMMON_VERSION" -ForegroundColor Green Write-Host "🎉 所有包发布完成!版本: v$CURRENT_VERSION" -ForegroundColor Green
+13 -47
View File
@@ -1,23 +1,13 @@
#!/bin/bash #!/bin/bash
# 发布所有子包到私有 npm 仓库 # 发布所有子包到私有 npm 仓库
# 使用方法: bash scripts/publish.sh [版本类型] # 使用方法: bash scripts/publish.sh
# 版本类型: major | minor | patch (默认 patch)
# 注意: 发布仓库地址使用各子包 package.json 中的 publishConfig.registry 配置 # 注意: 发布仓库地址使用各子包 package.json 中的 publishConfig.registry 配置
# 版本管理请先通过 pnpm release (standard-version) 完成,本脚本仅负责构建和发布
set -e set -e
# 获取版本类型参数,默认为 patch echo "🚀 开始发布流程"
VERSION_TYPE=${1:-patch}
# 验证版本类型
if [[ ! "$VERSION_TYPE" =~ ^(major|minor|patch)$ ]]; then
echo "❌ 无效的版本类型: $VERSION_TYPE"
echo "使用方法: bash scripts/publish.sh [major|minor|patch]"
exit 1
fi
echo "🚀 开始发布流程 (版本类型: $VERSION_TYPE)"
echo "" echo ""
# 子包列表 # 子包列表
@@ -49,39 +39,20 @@ if ! pnpm test; then
fi fi
echo "" echo ""
# 3. 更新版本号 # 3. 构建所有包
echo "📝 更新版本号 ($VERSION_TYPE)..."
for package in "${PACKAGES[@]}"; do
if [ -d "$package" ]; then
echo " 更新 $package 版本号..."
cd "$package"
npm version $VERSION_TYPE --no-git-tag-version
cd - > /dev/null
fi
done
echo ""
# 4. 构建所有包
echo "📦 构建所有包..." echo "📦 构建所有包..."
if ! pnpm build; then if ! pnpm build; then
echo "❌构建失败,停止发布" echo "❌ 构建失败,停止发布"
exit 1 exit 1
fi fi
echo "" echo ""
# 5. 更新 README 中的版本信息 # 获取当前版本号
echo "📄 更新 README..." CURRENT_VERSION=$(node -p "require('./package.json').version")
COMMON_VERSION=$(node -p "require('./packages/common/package.json').version") echo " 当前版本: v$CURRENT_VERSION"
echo " 当前版本: $COMMON_VERSION"
echo "" echo ""
# 6. 提交版本更新 # 4. 发布每个包
echo "💾 提交版本更新..."
git add packages/*/package.json
git commit -m "chore(release): publish v$COMMON_VERSION" || echo " 没有需要提交的更改"
echo ""
# 7. 发布每个包
echo "📤 开始发布包到 npm..." echo "📤 开始发布包到 npm..."
for package in "${PACKAGES[@]}"; do for package in "${PACKAGES[@]}"; do
if [ -d "$package" ]; then if [ -d "$package" ]; then
@@ -101,24 +72,19 @@ for package in "${PACKAGES[@]}"; do
fi fi
done done
# 8. 创建 Git 标签 # 5. 推送到远程仓库
echo "🏷️ 创建 Git 标签..."
git tag "v$COMMON_VERSION"
echo ""
# 9. 推送到远程仓库
echo "🔼 推送到远程仓库..." echo "🔼 推送到远程仓库..."
read -p "是否推送到远程仓库? (y/N) " -n 1 -r read -p "是否推送到远程仓库? (y/N) " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
git push origin master git push origin master
git push origin "v$COMMON_VERSION" git push origin --tags
echo "✅ 已推送到远程仓库" echo "✅ 已推送到远程仓库"
else else
echo "⚠️ 跳过推送,请手动执行:" echo "⚠️ 跳过推送,请手动执行:"
echo " git push origin master" echo " git push origin master"
echo " git push origin v$COMMON_VERSION" echo " git push origin --tags"
fi fi
echo "" echo ""
echo "🎉 所有包发布完成!版本: v$COMMON_VERSION" echo "🎉 所有包发布完成!版本: v$CURRENT_VERSION"
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.base.json",
"include": [
"vitest.config.ts",
"packages/*/src/**/*.ts",
"packages/*/src/**/*.js",
"packages/*/types/**/*.d.ts"
],
"exclude": ["**/node_modules/**", "**/dist/**"],
"compilerOptions": {
"noEmit": true
}
}
+10 -3
View File
@@ -1,8 +1,15 @@
{ {
"extends": "./tsconfig.base.json", "extends": "./tsconfig.base.json",
"exclude": ["node_modules", "dist", "packages"], "files": [],
"include": ["**/*.ts"], "references": [
{ "path": "./packages/common" },
{ "path": "./packages/uni-app" },
{ "path": "./packages/uview-plus" },
{ "path": "./packages/vue2" },
{ "path": "./packages/vue3" }
],
"exclude": ["**/node_modules", "**/dist"],
"compilerOptions": { "compilerOptions": {
"outDir": "./types" "noEmit": true
} }
} }
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
clearMocks: true,
coverage: {
provider: "v8",
reporter: ["text", "lcov"],
reportsDirectory: "coverage",
},
include: ["packages/*/test/**/*.test.ts"],
},
});