feat(common): ✨ 添加工具
This commit is contained in:
parent
7ff223fa32
commit
b10d613987
|
|
@ -11,8 +11,9 @@
|
|||
"Bash(npm run build *)",
|
||||
"Bash(npm view *)",
|
||||
"Bash(npm list *)",
|
||||
"Bash(pnpm install)",
|
||||
"Bash(pnpm build)",
|
||||
"Bash(pnpm install *)",
|
||||
"Bash(pnpm add *)",
|
||||
"Bash(pnpm build *)",
|
||||
"Bash(pnpm run *)",
|
||||
"Bash(pnpm test *)",
|
||||
"Bash(pnpm run build *)",
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
*.bak
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"src/*.{js,jsx,ts,tsx,vue}": "npm run lint"
|
||||
"packages/*/src/**/*.{js,jsx,ts,tsx,vue}": "eslint --fix"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
/cache
|
||||
/project.local.yml
|
||||
|
|
@ -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 read‑only.
|
||||
# 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: []
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
"psioniq.psi-header",
|
||||
"aaron-bond.better-comments",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint"
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
"psi-header.changes-tracking": {
|
||||
"isActive": true,
|
||||
"autoHeader": "autoSave",
|
||||
"modDate": "@lastModified:",
|
||||
"modDate": "@lastModified",
|
||||
"modDateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||
"modAuthor": "@lastModifiedBy:",
|
||||
"modAuthor": "@lastModifiedBy",
|
||||
"exclude": ["markdown", "json"],
|
||||
"excludeGlob": ["**/settings.json"]
|
||||
},
|
||||
|
|
@ -17,14 +17,22 @@
|
|||
{
|
||||
"language": "*",
|
||||
"template": [
|
||||
"@file: <<filerelativepath>>",
|
||||
"@description: ",
|
||||
"@author: <<author>> (<<authoremail>>)",
|
||||
"@date: <<filecreated('YYYY-MM-DD HH:mm:ss')>>",
|
||||
"@lastModified: <<dateformat('YYYY-MM-DD HH:mm:ss')>>",
|
||||
"@lastModifiedBy: <<author>> (<<authoremail>>)"
|
||||
"@file <<filerelativepath>>",
|
||||
"@description ",
|
||||
"@author <<author>> (<<authoremail>>)",
|
||||
"@date <<filecreated('YYYY-MM-DD HH:mm:ss')>>",
|
||||
"@lastModified <<dateformat('YYYY-MM-DD HH:mm:ss')>>",
|
||||
"@lastModifiedBy <<author>> (<<authoremail>>)"
|
||||
],
|
||||
"changeLogNaturalOrder": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"workbench.editor.limit.value": 3,
|
||||
"cSpell.words": [
|
||||
"Codice",
|
||||
"commitlint",
|
||||
"dcloudio",
|
||||
"Fabbrica"
|
||||
],
|
||||
"js/ts.tsdk.path": "node_modules\\typescript\\lib"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
| `@r-utils/common` | `packages/common` | 通用 JS/TS 工具(权限、倒计时、打印、时间等) |
|
||||
| `@r-utils/vue3` | `packages/vue3` | Vue3 工具(class 处理、dispatch 等) |
|
||||
| `@r-utils/uni-app` | `packages/uni-app` | uni-app 工具(请求封装、蓝牙、NFC、打印等) |
|
||||
| `@r-utils/uview-plus` | `packages/uview-plus` | uview-plus 组合式 API Hooks(Picker、Calendar 等) |
|
||||
| `@r-utils/vue2` | `packages/vue2` | Vue2 工具(visibility 插件) |
|
||||
|
||||
## 环境要求
|
||||
|
|
@ -74,6 +75,9 @@ import { mergeClass, dispatch } from '@r-utils/vue3';
|
|||
// uni-app 工具
|
||||
import { Request } from '@r-utils/uni-app';
|
||||
|
||||
// uview-plus Hooks
|
||||
import { usePickerSingle, usePicker, useCalendar } from '@r-utils/uview-plus';
|
||||
|
||||
// Vue2 插件
|
||||
import { VisibilityPlugin } from '@r-utils/vue2';
|
||||
Vue.use(VisibilityPlugin);
|
||||
|
|
@ -189,6 +193,7 @@ echo "registry=http://npm.nps.yunvip123.cn" > .npmrc
|
|||
pnpm add @r-utils/common
|
||||
pnpm add @r-utils/vue3
|
||||
pnpm add @r-utils/uni-app
|
||||
pnpm add @r-utils/uview-plus
|
||||
pnpm add @r-utils/vue2
|
||||
```
|
||||
|
||||
|
|
@ -208,6 +213,7 @@ r-util-js/
|
|||
│ │ └── tsconfig.json
|
||||
│ ├── vue3/ # @r-utils/vue3
|
||||
│ ├── uni-app/ # @r-utils/uni-app
|
||||
│ ├── uview-plus/ # @r-utils/uview-plus
|
||||
│ └── vue2/ # @r-utils/vue2
|
||||
├── eslint.config.js # ESLint v9 flat config
|
||||
├── prettier.config.js
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
};
|
||||
|
|
@ -33,12 +33,8 @@ module.exports = {
|
|||
"revert",
|
||||
],
|
||||
],
|
||||
// scope 可选,但如果提供必须是以下之一
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
["common", "vue3", "vue2", "uni-app", "deps", "release"],
|
||||
],
|
||||
// scope 可自定义(保留 prompt 的推荐列表,不做白名单限制)
|
||||
"scope-enum": [0],
|
||||
// subject 不能为空
|
||||
"subject-empty": [2, "never"],
|
||||
// subject 不能以句号结尾
|
||||
|
|
@ -74,8 +70,10 @@ module.exports = {
|
|||
{ value: "deps", name: "deps: 依赖更新" },
|
||||
{ value: "release", name: "release: 版本发布" },
|
||||
],
|
||||
allowCustomScopes: false,
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
// 破坏性变更默认跳过;如需声明可手动在提交信息中添加 BREAKING CHANGE
|
||||
allowBreakingChanges: [],
|
||||
messages: {
|
||||
type: '选择你要提交的类型:',
|
||||
scope: '选择一个提交范围(可选):',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import js from "@eslint/js";
|
||||
import ts from "typescript-eslint";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
import globals from "globals";
|
||||
import importPlugin from "eslint-plugin-import-x";
|
||||
|
||||
export default [
|
||||
// 应用推荐规则
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
prettier,
|
||||
|
||||
// 基础配置
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,vue}"],
|
||||
|
|
@ -29,6 +36,15 @@ export default [
|
|||
"lib/",
|
||||
"scripts/",
|
||||
],
|
||||
plugins: { prettier: prettierPlugin, "import-x": importPlugin },
|
||||
settings: {
|
||||
"import-x/resolver": {
|
||||
typescript: {
|
||||
project: "tsconfig.eslint.json",
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
|
|
@ -43,16 +59,47 @@ export default [
|
|||
},
|
||||
},
|
||||
rules: {
|
||||
...importPlugin.flatConfigs.recommended.rules,
|
||||
|
||||
// Prettier 规则
|
||||
"prettier/prettier": "warn",
|
||||
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
|
||||
// Import 规则
|
||||
"import-x/no-self-import": "error",
|
||||
"import-x/no-cycle": "error",
|
||||
"import-x/no-useless-path-segments": "error",
|
||||
"import-x/no-extraneous-dependencies": "error",
|
||||
"import-x/no-mutable-exports": "error",
|
||||
"import-x/no-named-as-default": "error",
|
||||
"import-x/no-named-as-default-member": "error",
|
||||
"import-x/no-duplicates": "warn",
|
||||
"import-x/no-deprecated": "warn",
|
||||
"import-x/newline-after-import": "warn",
|
||||
"import-x/first": "warn",
|
||||
"import-x/order": [
|
||||
"warn",
|
||||
{
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"object",
|
||||
"type",
|
||||
],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// 应用推荐规则
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
prettier,
|
||||
|
||||
// 测试文件覆盖配置
|
||||
{
|
||||
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
|
||||
|
|
|
|||
198
jest.config.ts
198
jest.config.ts
|
|
@ -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']
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
|
@ -44,18 +44,16 @@
|
|||
"release": "standard-version",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@jest/globals": "^30.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
||||
"@typescript-eslint/parser": "^8.57.1",
|
||||
"babel-jest": "^30.3.0",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-git": "^1.12.0",
|
||||
"eslint": "^10.0.3",
|
||||
|
|
@ -67,8 +65,8 @@
|
|||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"inquirer": "^9.3.8",
|
||||
"jest": "^30.3.0",
|
||||
"lint-staged": "^16.4.0",
|
||||
"vitest": "catalog:",
|
||||
"prettier": "^3.8.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"tslib": "^2.8.1",
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import rootConfig from "../../eslint.config.js";
|
||||
|
||||
export default [...rootConfig];
|
||||
|
|
@ -49,18 +49,19 @@
|
|||
"release": "standard-version",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"js-easing-functions": "^1.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "catalog:",
|
||||
"text-encoding": "^0.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/lodash-es": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-dts": "catalog:"
|
||||
"vite-plugin-dts": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export * from "./printer";
|
|||
export * from "./time";
|
||||
export * from "./timer";
|
||||
export * from "./input-rule";
|
||||
export * from "./ui";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect, describe } from "@jest/globals";
|
||||
import { test, expect, describe } from "vitest";
|
||||
import { getValueOfRule, InputOptions } from "../src/input-rule";
|
||||
|
||||
describe("getValueOfRule - 文本模式 (text: true)", () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from "@jest/globals";
|
||||
import { test, expect } from "vitest";
|
||||
import { Permission } from "../src/permission";
|
||||
|
||||
test("测试 Permission#isValid()", () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@
|
|||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
formats: ["es", "cjs"],
|
||||
fileName: (format) => `index.${format === "es" ? "mjs" : "cjs"}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['dayjs', 'lodash-es', 'text-encoding', 'tslib'],
|
||||
external: ["dayjs", "lodash-es", "text-encoding", "tslib"],
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
include: ["src"],
|
||||
outDir: "dist",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@
|
|||
"release": "standard-version",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@r-utils/common": "workspace:^",
|
||||
"chroma-js": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"qs": "^6.15.0"
|
||||
"chroma-js": "catalog:",
|
||||
"lodash-es": "catalog:",
|
||||
"qs": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dcloudio/uni-app": ">=3.0.0",
|
||||
|
|
@ -67,9 +67,9 @@
|
|||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.0.7",
|
||||
"@dcloudio/uni-app": "3.0.0-4070520250711001",
|
||||
"@types/chroma-js": "^3.1.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/qs": "^6.15.0",
|
||||
"@types/chroma-js": "catalog:",
|
||||
"@types/lodash-es": "catalog:",
|
||||
"@types/qs": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-dts": "catalog:",
|
||||
"vue": "^3.3.0"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export function getCurrentPage() {
|
|||
return pages[pages.length - 1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用 TypeScript 工具类型实现自动类型推断的 toPromise
|
||||
*
|
||||
|
|
@ -39,10 +38,17 @@ export function getCurrentPage() {
|
|||
// type ExtractFirstParameter<T> = Parameters<T>[0]; // 这样也可以,但需要T是函数类型
|
||||
|
||||
// 方法2:使用条件类型实现(更灵活,等价于 Parameters<T>[0])
|
||||
type ExtractFirstParameter<T> = T extends (arg: infer P, ...args: unknown[]) => unknown ? P : never;
|
||||
type ExtractFirstParameter<T> = T extends (
|
||||
arg: infer P,
|
||||
...args: unknown[]
|
||||
) => unknown
|
||||
? P
|
||||
: never;
|
||||
|
||||
// 提取 success 回调的参数类型(类似 ReturnType 的思路,但提取回调参数)
|
||||
type ExtractSuccessResult<T> = T extends { success?: (result: infer R) => void } ? R : never;
|
||||
type ExtractSuccessResult<T> = T extends { success?: (result: infer R) => void }
|
||||
? R
|
||||
: never;
|
||||
|
||||
// 提取函数参数类型并排除回调函数(用于args参数)
|
||||
type ExtractOptions<T> = T extends (options: infer P) => unknown
|
||||
|
|
@ -103,7 +109,6 @@ export function toPromise<F extends (options: Record<string, unknown>) => void>(
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
export function showToast(
|
||||
title: string,
|
||||
icon: "success" | "loading" | "error" | "none" = "none",
|
||||
|
|
@ -139,12 +144,9 @@ export async function getCanvasImageData(
|
|||
) {
|
||||
const context = uni.createCanvasContext(canvasId, thisArg);
|
||||
|
||||
const imgRes = await toPromise(
|
||||
uni.downloadFile,
|
||||
{
|
||||
url,
|
||||
},
|
||||
);
|
||||
const imgRes = await toPromise(uni.downloadFile, {
|
||||
url,
|
||||
});
|
||||
|
||||
console.log(imgRes);
|
||||
context.drawImage(imgRes.tempFilePath, x, y, width, height);
|
||||
|
|
@ -243,3 +245,45 @@ export function checkAndroidPermission(permissionList: string[]) {
|
|||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步计算元素距离
|
||||
*/
|
||||
export async function calculateDistanceBySelectorAsync(
|
||||
scrollViewSelector: string,
|
||||
selector: string,
|
||||
offsetTop = 0,
|
||||
): Promise<number> {
|
||||
const scrollViewScrollOffsetPromise = new Promise<UniApp.NodeInfo>(
|
||||
(resolve) => {
|
||||
const query = uni.createSelectorQuery();
|
||||
query.select(scrollViewSelector).scrollOffset((res) => {
|
||||
resolve(res as UniApp.NodeInfo);
|
||||
});
|
||||
query.exec();
|
||||
},
|
||||
);
|
||||
|
||||
const targetElementPromise = new Promise<UniApp.NodeInfo>((resolve) => {
|
||||
const query = uni.createSelectorQuery();
|
||||
query.select(selector).boundingClientRect((rect) => {
|
||||
resolve(rect as UniApp.NodeInfo);
|
||||
});
|
||||
query.exec();
|
||||
});
|
||||
|
||||
const [scrollViewScrollOffset, targetElementRect] = await Promise.all([
|
||||
scrollViewScrollOffsetPromise,
|
||||
targetElementPromise,
|
||||
]);
|
||||
|
||||
if (!scrollViewScrollOffset || !targetElementRect) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const distance =
|
||||
(targetElementRect.top ?? 0) +
|
||||
(scrollViewScrollOffset.scrollTop ?? 0) -
|
||||
offsetTop;
|
||||
return Math.max(0, Math.round(distance));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { onReady } from "@dcloudio/uni-app";
|
||||
import qs from "qs";
|
||||
|
||||
/**
|
||||
* 分享配置
|
||||
* NOTE 待优化,title,query应该与withShareTicket,menus分开配置
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -8,6 +8,5 @@
|
|||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/// <reference types="@dcloudio/types" />
|
||||
|
||||
declare namespace UniNamespace {
|
||||
type LocaldataItem = {
|
||||
type LocaldataItem<T=string> = {
|
||||
text: string;
|
||||
value: string;
|
||||
children?: LocaldataItem[];
|
||||
value: T;
|
||||
children?: LocaldataItem<T>[];
|
||||
};
|
||||
type LocaldataNodeclickValue = {
|
||||
text: string;
|
||||
|
|
@ -15,7 +15,7 @@ declare namespace UniNamespace {
|
|||
type Event<T> = { detail: T };
|
||||
type InputEvent = Event<{ value: string }>;
|
||||
type PickerEvent = Event<{ value: string }>;
|
||||
type PickerViewEvent = Event<{ value: unknown[] }>;
|
||||
type PickerViewEvent<T> = Event<{ value: T[] }>;
|
||||
type ScrollEvent = Event<{ scrollTop: number }>;
|
||||
type SwiperEvent = Event<{ current: number }>;
|
||||
type UniDataPickerEvent = Event<{ value: LocaldataChangeValue[] }>;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./picker-single";
|
||||
export * from "./picker";
|
||||
export * from "./datetime-picker";
|
||||
export * from "./calendar";
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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("modelValue:get/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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "types/**/*.d.ts"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
"release": "standard-version",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.7.0"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@
|
|||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
"release": "standard-version",
|
||||
"commit": "cz",
|
||||
"lint-staged": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -7,6 +7,5 @@
|
|||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
10822
pnpm-lock.yaml
10822
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -3,3 +3,13 @@ packages:
|
|||
|
||||
catalog:
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default {
|
|||
{
|
||||
files: [
|
||||
"*.js",
|
||||
"*.cjs",
|
||||
"*.mjs",
|
||||
"*.jsx",
|
||||
"*.ts",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ $PACKAGES = @(
|
|||
"packages/common",
|
||||
"packages/vue3",
|
||||
"packages/uni-app",
|
||||
"packages/uview-plus",
|
||||
"packages/vue2"
|
||||
)
|
||||
|
||||
|
|
@ -74,18 +75,6 @@ foreach ($package in $PACKAGES) {
|
|||
}
|
||||
}
|
||||
|
||||
# 5. 推送到远程仓库
|
||||
Write-Host "🔼 推送到远程仓库..." -ForegroundColor Cyan
|
||||
$push = Read-Host "是否推送到远程仓库? (y/N)"
|
||||
if ($push -eq "y" -or $push -eq "Y") {
|
||||
git push origin master
|
||||
git push origin --tags
|
||||
Write-Host "✅ 已推送到远程仓库" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ 跳过推送,请手动执行:" -ForegroundColor Yellow
|
||||
Write-Host " git push origin master" -ForegroundColor Gray
|
||||
Write-Host " git push origin --tags" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
|
||||
Write-Host "🎉 所有包发布完成!版本: v$CURRENT_VERSION" -ForegroundColor Green
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"exclude": ["node_modules", "dist", "packages"],
|
||||
"include": ["**/*.ts"],
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./packages/common" },
|
||||
{ "path": "./packages/uni-app" },
|
||||
{ "path": "./packages/uview-plus" },
|
||||
{ "path": "./packages/vue2" },
|
||||
{ "path": "./packages/vue3" }
|
||||
],
|
||||
"exclude": ["**/node_modules", "**/dist"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./types"
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue