Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf33d1003 | ||
|
|
fc816e2d84 | ||
|
|
f0d1090932 |
@@ -0,0 +1,2 @@
|
||||
/cache
|
||||
/project.local.yml
|
||||
@@ -0,0 +1,154 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "timi-web"
|
||||
|
||||
|
||||
# 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
|
||||
# haxe 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:
|
||||
- vue
|
||||
|
||||
# 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 based on the project name or path.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||
# for example by saying that the information retrieved from a memory file is no longer correct
|
||||
# or no longer relevant for the project.
|
||||
# * `edit_memory`: Replaces content matching a regular expression in a memory.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_file`: Finds files in the given relative paths
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
|
||||
# * `find_symbol`: Performs a global (or local) search using the language server backend.
|
||||
# * `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`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
|
||||
# for clients that do not read the initial instructions when the MCP server is connected.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `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`: List available memories. Any memory can be read using the `read_memory` tool.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
|
||||
# is relevant to the current task. You can infer whether the information
|
||||
# is relevant from the memory file name.
|
||||
# You should not read the same memory file multiple times in the same conversation.
|
||||
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
|
||||
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
|
||||
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
|
||||
# For JB, we use a separate tool.
|
||||
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
|
||||
# * `safe_delete_symbol`:
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
|
||||
# The memory name should be meaningful.
|
||||
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: []
|
||||
+5
-4
@@ -5,7 +5,7 @@
|
||||
"module": "./dist/timi-web.mjs",
|
||||
"style": "./dist/timi-web.css",
|
||||
"private": false,
|
||||
"version": "0.0.22",
|
||||
"version": "0.0.23",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -39,8 +39,9 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.13.5",
|
||||
"axios": "1.18.0",
|
||||
"less": "4.5.1",
|
||||
"pinia": "^3.0.2",
|
||||
"marked": "^17.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.3",
|
||||
"marked-highlight": "^2.2.3",
|
||||
@@ -49,7 +50,6 @@
|
||||
"terser": "^5.44.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/node": "^25.0.8",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||
@@ -73,6 +73,7 @@
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vitest": "^4.1.4",
|
||||
"vue": "^3.5.26",
|
||||
"vue-tsc": "^3.2.2"
|
||||
"vue-tsc": "^3.2.2",
|
||||
"vue-eslint-parser": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+159
-59
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
axios:
|
||||
specifier: 1.13.5
|
||||
version: 1.13.5
|
||||
specifier: 1.18.0
|
||||
version: 1.18.0
|
||||
less:
|
||||
specifier: 4.5.1
|
||||
version: 4.5.1
|
||||
@@ -26,6 +26,9 @@ importers:
|
||||
marked-mangle:
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12(marked@17.0.1)
|
||||
pinia:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
||||
prismjs:
|
||||
specifier: 1.30.0
|
||||
version: 1.30.0
|
||||
@@ -33,9 +36,6 @@ importers:
|
||||
specifier: ^5.44.1
|
||||
version: 5.44.1
|
||||
devDependencies:
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/node':
|
||||
specifier: ^25.0.8
|
||||
version: 25.0.8
|
||||
@@ -54,9 +54,6 @@ importers:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4(vitest@4.1.4)
|
||||
'@vitest/mocker':
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.8)(less@4.5.1)(terser@5.44.1))
|
||||
'@vue/test-utils':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
@@ -74,7 +71,7 @@ importers:
|
||||
version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.2))(eslint@9.39.2)(prettier@3.8.0)
|
||||
eslint-plugin-vue:
|
||||
specifier: ^10.6.2
|
||||
version: 10.6.2(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@9.4.3(eslint@9.39.2))
|
||||
version: 10.6.2(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@10.4.1(eslint@9.39.2))
|
||||
happy-dom:
|
||||
specifier: ^20.8.9
|
||||
version: 20.8.9
|
||||
@@ -108,6 +105,9 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.26
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.0.0
|
||||
version: 10.4.1(eslint@9.39.2)
|
||||
vue-tsc:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(typescript@5.9.3)
|
||||
@@ -691,10 +691,6 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/marked@6.0.0':
|
||||
resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==}
|
||||
deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/node@25.0.8':
|
||||
resolution: {integrity: sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==}
|
||||
|
||||
@@ -862,6 +858,15 @@ packages:
|
||||
'@vue/compiler-vue2@2.7.16':
|
||||
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
||||
|
||||
'@vue/devtools-api@7.7.10':
|
||||
resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==}
|
||||
|
||||
'@vue/devtools-kit@7.7.10':
|
||||
resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==}
|
||||
|
||||
'@vue/devtools-shared@7.7.10':
|
||||
resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==}
|
||||
|
||||
'@vue/language-core@2.2.0':
|
||||
resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
|
||||
peerDependencies:
|
||||
@@ -918,6 +923,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
ajv-draft-04@1.0.0:
|
||||
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
|
||||
peerDependencies:
|
||||
@@ -985,8 +994,8 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.13.5:
|
||||
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
|
||||
axios@1.18.0:
|
||||
resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==}
|
||||
|
||||
babel-plugin-prismjs@2.1.0:
|
||||
resolution: {integrity: sha512-ehzSKYfeAz4U78zi/sfwsjDPlq0LvDKxNefcZTJ/iKBu+plsHsLqZhUeGf1+82LAcA35UZGbU6ksEx2Utphc/g==}
|
||||
@@ -996,6 +1005,9 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
birpc@2.9.0:
|
||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
@@ -1085,6 +1097,10 @@ packages:
|
||||
copy-anything@2.0.6:
|
||||
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
|
||||
|
||||
copy-anything@4.0.5:
|
||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1248,10 +1264,6 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
|
||||
eslint-scope@7.2.2:
|
||||
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1278,14 +1290,14 @@ packages:
|
||||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
espree@9.6.1:
|
||||
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
esquery@1.5.0:
|
||||
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
esquery@1.7.0:
|
||||
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
esrecurse@4.3.0:
|
||||
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -1350,8 +1362,8 @@ packages:
|
||||
flatted@3.3.1:
|
||||
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
follow-redirects@1.16.0:
|
||||
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
@@ -1446,9 +1458,16 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1499,6 +1518,10 @@ packages:
|
||||
is-what@3.14.1:
|
||||
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
|
||||
|
||||
is-what@5.5.0:
|
||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -1759,6 +1782,9 @@ packages:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mlly@1.7.4:
|
||||
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
|
||||
|
||||
@@ -1850,6 +1876,9 @@ packages:
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
|
||||
@@ -1872,6 +1901,15 @@ packages:
|
||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pinia@3.0.4:
|
||||
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.5.0'
|
||||
vue: ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
@@ -1917,8 +1955,9 @@ packages:
|
||||
proto-list@1.2.4:
|
||||
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
proxy-from-env@2.1.0:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
prr@1.0.1:
|
||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||
@@ -1949,6 +1988,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rolldown@1.0.0-rc.12:
|
||||
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -2025,6 +2067,10 @@ packages:
|
||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||
deprecated: Please use @jridgewell/sourcemap-codec instead
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
@@ -2061,6 +2107,10 @@ packages:
|
||||
strip-literal@3.1.0:
|
||||
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||
|
||||
superjson@2.2.6:
|
||||
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2295,11 +2345,11 @@ packages:
|
||||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-eslint-parser@9.4.3:
|
||||
resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
vue-eslint-parser@10.4.1:
|
||||
resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
|
||||
vue-tsc@3.2.2:
|
||||
resolution: {integrity: sha512-r9YSia/VgGwmbbfC06hDdAatH634XJ9nVl6Zrnz1iK4ucp8Wu78kawplXnIDa3MSu1XdQQePTHLXYwPDWn+nyQ==}
|
||||
@@ -2904,10 +2954,6 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/marked@6.0.0':
|
||||
dependencies:
|
||||
marked: 17.0.1
|
||||
|
||||
'@types/node@25.0.8':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
@@ -3174,6 +3220,24 @@ snapshots:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
|
||||
'@vue/devtools-api@7.7.10':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.10
|
||||
|
||||
'@vue/devtools-kit@7.7.10':
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 7.7.10
|
||||
birpc: 2.9.0
|
||||
hookable: 5.5.3
|
||||
mitt: 3.0.1
|
||||
perfect-debounce: 1.0.0
|
||||
speakingurl: 14.0.1
|
||||
superjson: 2.2.6
|
||||
|
||||
'@vue/devtools-shared@7.7.10':
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
|
||||
'@vue/language-core@2.2.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.11
|
||||
@@ -3240,6 +3304,12 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ajv-draft-04@1.0.0(ajv@8.13.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.13.0
|
||||
@@ -3303,13 +3373,15 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.13.5:
|
||||
axios@1.18.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 1.1.0
|
||||
https-proxy-agent: 5.0.1
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
babel-plugin-prismjs@2.1.0(prismjs@1.30.0):
|
||||
dependencies:
|
||||
@@ -3317,6 +3389,8 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
birpc@2.9.0: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
@@ -3402,6 +3476,10 @@ snapshots:
|
||||
dependencies:
|
||||
is-what: 3.14.1
|
||||
|
||||
copy-anything@4.0.5:
|
||||
dependencies:
|
||||
is-what: 5.5.0
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -3504,7 +3582,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.39.2)
|
||||
|
||||
eslint-plugin-vue@10.6.2(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@9.4.3(eslint@9.39.2)):
|
||||
eslint-plugin-vue@10.6.2(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@10.4.1(eslint@9.39.2)):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@9.39.2)
|
||||
eslint: 9.39.2
|
||||
@@ -3512,16 +3590,11 @@ snapshots:
|
||||
nth-check: 2.1.1
|
||||
postcss-selector-parser: 7.1.1
|
||||
semver: 7.7.1
|
||||
vue-eslint-parser: 9.4.3(eslint@9.39.2)
|
||||
vue-eslint-parser: 10.4.1(eslint@9.39.2)
|
||||
xml-name-validator: 4.0.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3)
|
||||
|
||||
eslint-scope@7.2.2:
|
||||
dependencies:
|
||||
esrecurse: 4.3.0
|
||||
estraverse: 5.3.0
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
dependencies:
|
||||
esrecurse: 4.3.0
|
||||
@@ -3576,16 +3649,14 @@ snapshots:
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
espree@9.6.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn-jsx: 5.3.2(acorn@8.15.0)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
esquery@1.5.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
|
||||
esquery@1.7.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
|
||||
esrecurse@4.3.0:
|
||||
dependencies:
|
||||
estraverse: 5.3.0
|
||||
@@ -3634,7 +3705,7 @@ snapshots:
|
||||
|
||||
flatted@3.3.1: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
follow-redirects@1.16.0: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
@@ -3731,8 +3802,17 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -3770,6 +3850,8 @@ snapshots:
|
||||
|
||||
is-what@3.14.1: {}
|
||||
|
||||
is-what@5.5.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
@@ -4007,6 +4089,8 @@ snapshots:
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mlly@1.7.4:
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
@@ -4091,6 +4175,8 @@ snapshots:
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
|
||||
picocolors@1.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -4104,6 +4190,13 @@ snapshots:
|
||||
pify@4.0.1:
|
||||
optional: true
|
||||
|
||||
pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.10
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
@@ -4157,7 +4250,7 @@ snapshots:
|
||||
|
||||
proto-list@1.2.4: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
prr@1.0.1:
|
||||
optional: true
|
||||
@@ -4180,6 +4273,8 @@ snapshots:
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.122.0
|
||||
@@ -4280,6 +4375,8 @@ snapshots:
|
||||
|
||||
sourcemap-codec@1.4.8: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
sprintf-js@1.0.3: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
@@ -4314,6 +4411,10 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
superjson@2.2.6:
|
||||
dependencies:
|
||||
copy-anything: 4.0.5
|
||||
|
||||
supports-color@5.5.0:
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
@@ -4519,15 +4620,14 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-eslint-parser@9.4.3(eslint@9.39.2):
|
||||
vue-eslint-parser@10.4.1(eslint@9.39.2):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.2
|
||||
eslint-scope: 7.2.2
|
||||
eslint-visitor-keys: 3.4.3
|
||||
espree: 9.6.1
|
||||
esquery: 1.5.0
|
||||
lodash: 4.17.21
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
espree: 10.4.0
|
||||
esquery: 1.7.0
|
||||
semver: 7.7.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -7,7 +7,7 @@ import { axios } from "./BaseAPI";
|
||||
* @param id 文章 ID
|
||||
* @returns 文章数据
|
||||
*/
|
||||
async function view(id: number): Promise<ArticleView<any>> {
|
||||
async function view(id: string): Promise<ArticleView<any>> {
|
||||
return axios.get(`/article/${id}`);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ async function view(id: number): Promise<ArticleView<any>> {
|
||||
* @param id 文章 ID
|
||||
* @returns 最新喜欢数量
|
||||
*/
|
||||
async function like(id: number): Promise<number> {
|
||||
async function like(id: string): Promise<number> {
|
||||
return axios.get(`/article/like/${id}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {Attachment, BizUpdateReq, TempFileResp} from "../types";
|
||||
import {axios} from "./BaseAPI";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
|
||||
const BASE_URI = "/attach";
|
||||
|
||||
/**
|
||||
* 上传临时附件
|
||||
*
|
||||
* @param file 文件或文件列表
|
||||
* @param ttl 有效期
|
||||
* @returns 临时附件列表
|
||||
*/
|
||||
async function uploadTemp(file: File | File[], ttl?: string): Promise<TempFileResp[]> {
|
||||
const formData = new FormData();
|
||||
const fileList = Array.isArray(file) ? file : [file];
|
||||
for (const item of fileList) {
|
||||
formData.append("file", item);
|
||||
}
|
||||
if (ttl) {
|
||||
formData.append("ttl", ttl);
|
||||
}
|
||||
return await axios.post(`${BASE_URI}/temp/upload`, formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按业务差分更新附件
|
||||
*
|
||||
* @param req 更新请求
|
||||
*/
|
||||
async function updateByBiz(req: BizUpdateReq): Promise<void> {
|
||||
return await axios.post(`${BASE_URI}/update/biz`, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按业务查询附件列表
|
||||
*
|
||||
* @param bizType 业务类型
|
||||
* @param bizId 业务 ID
|
||||
* @param attachTypeList 附件类型列表
|
||||
*/
|
||||
async function listByBiz(bizType: string, bizId: string, attachTypeList?: string[]): Promise<Attachment[]> {
|
||||
return await axios.get(`${BASE_URI}/list/biz`, {
|
||||
params: {
|
||||
bizType,
|
||||
bizId,
|
||||
attachTypeList
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
uploadTemp,
|
||||
updateByBiz,
|
||||
listByBiz,
|
||||
getReadURL: CommonAPI.getAttachmentReadAPI,
|
||||
getTempReadURL: CommonAPI.getAttachmentTempReadAPI
|
||||
};
|
||||
+19
-3
@@ -1,9 +1,23 @@
|
||||
import {Setting, TemplateBizType} from "../types";
|
||||
import {CaptchaResult, Setting, TemplateBizType} from "../types";
|
||||
import {axios} from "./BaseAPI";
|
||||
|
||||
const getCaptchaAPI = () => axios.defaults.baseURL + "/captcha";
|
||||
function getBaseURI(): string {
|
||||
const baseURL = axios.defaults.baseURL;
|
||||
if (!baseURL || baseURL === "undefined") {
|
||||
return "";
|
||||
}
|
||||
return baseURL.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
const getAttachmentReadAPI = (mongoId: string) => `${axios.defaults.baseURL}/attachment/read/${mongoId}`;
|
||||
const getCaptchaAPI = () => `${getBaseURI()}/captcha`;
|
||||
|
||||
async function captcha(width: number, height: number): Promise<CaptchaResult> {
|
||||
return await axios.get(`/captcha?width=${width}&height=${height}`);
|
||||
}
|
||||
|
||||
const getAttachmentReadAPI = (id: string) => `${getBaseURI()}/attach/read/${id}`;
|
||||
|
||||
const getAttachmentTempReadAPI = (id: string) => `${getBaseURI()}/attach/temp/read?id=${id}`;
|
||||
|
||||
async function getTemplate(bizType: TemplateBizType, code: string): Promise<string> {
|
||||
return axios.get(`/template?bizType=${bizType}&bizCode=${code}`);
|
||||
@@ -30,7 +44,9 @@ async function settingMap(map: Record<string, string[]>): Promise<Map<string, Ma
|
||||
|
||||
export default {
|
||||
getCaptchaAPI,
|
||||
captcha,
|
||||
getAttachmentReadAPI,
|
||||
getAttachmentTempReadAPI,
|
||||
getTemplate,
|
||||
settingMap
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { axios } from "./BaseAPI";
|
||||
import type { Page, PageResult, Permission, PermissionPayload } from "../types";
|
||||
|
||||
const BASE_URI = "/user/permission";
|
||||
|
||||
async function list(req: Page<Permission>): Promise<PageResult<Permission>> {
|
||||
return axios.post(`${BASE_URI}/list`, req);
|
||||
}
|
||||
|
||||
async function create(req: PermissionPayload): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/create`, req);
|
||||
}
|
||||
|
||||
async function update(req: PermissionPayload): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/update`, req);
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/delete`, undefined, {
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
list,
|
||||
create,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { axios } from "./BaseAPI";
|
||||
import type { Page, PageResult, Permission, Role, RolePayload, UserRoleAuthorizeReq } from "../types";
|
||||
|
||||
const BASE_URI = "/user/role";
|
||||
|
||||
async function list(req: Page<Role>): Promise<PageResult<Role>> {
|
||||
return axios.post(`${BASE_URI}/list`, req);
|
||||
}
|
||||
|
||||
async function detail(id: string): Promise<Role> {
|
||||
return axios.post(`${BASE_URI}/detail`, undefined, {
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
async function create(req: RolePayload): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/create`, req);
|
||||
}
|
||||
|
||||
async function update(req: RolePayload): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/update`, req);
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/delete`, undefined, {
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
|
||||
async function authorizedList(userId: string): Promise<Role[]> {
|
||||
return axios.post(`${BASE_URI}/authorized/list`, undefined, {
|
||||
params: { userId }
|
||||
});
|
||||
}
|
||||
|
||||
async function authorize(req: UserRoleAuthorizeReq): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/authorized/create`, req);
|
||||
}
|
||||
|
||||
async function authorizedPermission(userId: string): Promise<Permission[]> {
|
||||
return axios.post(`${BASE_URI}/authorized/permission`, undefined, {
|
||||
params: { userId }
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
list,
|
||||
detail,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
authorizedList,
|
||||
authorize,
|
||||
authorizedPermission
|
||||
};
|
||||
+38
-34
@@ -1,30 +1,25 @@
|
||||
import {
|
||||
Attachment,
|
||||
CaptchaData,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RegisterRequest,
|
||||
UserAttachType,
|
||||
UserProfileView,
|
||||
UserView
|
||||
} from "../types";
|
||||
import { axios } from "./BaseAPI";
|
||||
import type {Attachment, CaptchaData, LoginRequest, LoginResponse, RegisterRequest, UpdatePasswordRequest, User} from "../types";
|
||||
import {UserAttachType} from "../types";
|
||||
import {axios} from "./BaseAPI";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
|
||||
const BASE_URI = "/user";
|
||||
type UserAttachmentOwner = {
|
||||
attachmentList?: readonly Attachment[];
|
||||
};
|
||||
|
||||
async function register(captchaData: CaptchaData<RegisterRequest>): Promise<LoginResponse> {
|
||||
return axios.post(`${BASE_URI}/register`, captchaData);
|
||||
async function register(req: CaptchaData<RegisterRequest>): Promise<LoginResponse> {
|
||||
return await axios.post(`${BASE_URI}/register`, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param captchaData 验证码登录对象
|
||||
* @param req 验证码登录对象
|
||||
* @returns LoginResponse
|
||||
*/
|
||||
async function login(captchaData: CaptchaData<LoginRequest>): Promise<LoginResponse> {
|
||||
return axios.post(`${BASE_URI}/login`, captchaData);
|
||||
async function login(req: CaptchaData<LoginRequest>): Promise<LoginResponse> {
|
||||
return await axios.post(`${BASE_URI}/login`, req);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,12 +27,20 @@ async function login(captchaData: CaptchaData<LoginRequest>): Promise<LoginRespo
|
||||
*
|
||||
* @returns true 为已登录
|
||||
*/
|
||||
async function login4Token(): Promise<LoginResponse> {
|
||||
return axios.post(`${BASE_URI}/login/token`);
|
||||
async function loginByToken(): Promise<LoginResponse> {
|
||||
return await axios.post(`${BASE_URI}/login/token`);
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
return axios.post(`${BASE_URI}/logout`);
|
||||
return await axios.post(`${BASE_URI}/logout`);
|
||||
}
|
||||
|
||||
async function updateCurrent(req: Partial<User>): Promise<LoginResponse> {
|
||||
return await axios.post(`${BASE_URI}/current/update`, req);
|
||||
}
|
||||
|
||||
async function updatePassword(req: UpdatePasswordRequest): Promise<void> {
|
||||
return await axios.post(`${BASE_URI}/update/password`, req);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,29 +49,28 @@ async function logout(): Promise<void> {
|
||||
* @param id 用户 ID
|
||||
* @returns 用户数据
|
||||
*/
|
||||
async function view(id: number): Promise<UserView> {
|
||||
return axios.post(`${BASE_URI}/view/${id}`);
|
||||
async function view(id: string): Promise<User> {
|
||||
return await axios.post(`${BASE_URI}/view/${id}`);
|
||||
}
|
||||
|
||||
function getAvatarURL(profile?: UserProfileView) {
|
||||
if (profile && profile.attachmentList) {
|
||||
return findAttachmentByType(profile.attachmentList, [UserAttachType.AVATAR, UserAttachType.DEFAULT_AVATAR]);
|
||||
function getAvatarURL(user?: UserAttachmentOwner) {
|
||||
if (user?.attachmentList) {
|
||||
return findAttachmentByType(user.attachmentList, [UserAttachType.AVATAR, UserAttachType.DEFAULT_AVATAR]);
|
||||
}
|
||||
}
|
||||
|
||||
function getWrapperURL(profile?: UserProfileView) {
|
||||
if (profile && profile.attachmentList) {
|
||||
return findAttachmentByType(profile.attachmentList, [UserAttachType.WRAPPER, UserAttachType.DEFAULT_WRAPPER]);
|
||||
function getWrapperURL(user?: UserAttachmentOwner) {
|
||||
if (user?.attachmentList) {
|
||||
return findAttachmentByType(user.attachmentList, [UserAttachType.WRAPPER, UserAttachType.DEFAULT_WRAPPER]);
|
||||
}
|
||||
}
|
||||
|
||||
function findAttachmentByType(attachmentList: Attachment[], types: UserAttachType[]) {
|
||||
function findAttachmentByType(attachmentList: readonly Attachment[], types: UserAttachType[]) {
|
||||
for (let i = 0; i < attachmentList.length; i++) {
|
||||
const attachType = (<any>UserAttachType)[attachmentList[i].attachType!];
|
||||
for (let type of types) {
|
||||
if (attachType === type) {
|
||||
return CommonAPI.getAttachmentReadAPI(attachmentList[i].mongoId);
|
||||
}
|
||||
const attachType = attachmentList[i].attachType as UserAttachType | undefined;
|
||||
const id = attachmentList[i].id;
|
||||
if (id && attachType && types.includes(attachType)) {
|
||||
return CommonAPI.getAttachmentReadAPI(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,8 +78,10 @@ function findAttachmentByType(attachmentList: Attachment[], types: UserAttachTyp
|
||||
export default {
|
||||
register,
|
||||
login,
|
||||
login4Token,
|
||||
loginByToken,
|
||||
logout,
|
||||
updateCurrent,
|
||||
updatePassword,
|
||||
|
||||
view,
|
||||
getAvatarURL,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export { default as ArticleAPI } from "./ArticleAPI";
|
||||
export { default as AttachmentAPI } from "./AttachmentAPI";
|
||||
export { default as CommentAPI } from "./CommentAPI";
|
||||
export { default as CommonAPI } from "./CommonAPI";
|
||||
export { default as DeveloperAPI } from "./DeveloperAPI";
|
||||
export { default as PermissionAPI } from "./PermissionAPI";
|
||||
export { default as RoleAPI } from "./RoleAPI";
|
||||
export { default as UserAPI } from "./UserAPI";
|
||||
|
||||
@@ -1,46 +1,103 @@
|
||||
<template>
|
||||
<img
|
||||
<button
|
||||
class="tui-captcha ir-pixelated"
|
||||
v-if="src"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:src="src"
|
||||
alt="验证码"
|
||||
@click="update()"
|
||||
/>
|
||||
type="button"
|
||||
:style="captchaStyle"
|
||||
:disabled="isLoading"
|
||||
:title="title"
|
||||
@click="update"
|
||||
>
|
||||
<img v-if="src" :src="src" :alt="title" />
|
||||
<span v-else v-text="placeholder" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import {CommonAPI} from "../../api";
|
||||
import type {CaptchaResult} from "../../types";
|
||||
|
||||
defineOptions({
|
||||
name: "Captcha"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
width: number,
|
||||
height: number,
|
||||
from: string,
|
||||
api: string,
|
||||
}>(), {});
|
||||
const { width, height, from, api } = toRefs(props);
|
||||
width?: number,
|
||||
height?: number,
|
||||
title?: string,
|
||||
placeholder?: string,
|
||||
}>(), {
|
||||
width: 90,
|
||||
height: 40,
|
||||
title: "验证码",
|
||||
placeholder: "刷新"
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
update: [result: CaptchaResult],
|
||||
error: [error: unknown]
|
||||
}>();
|
||||
|
||||
const { width, height, title, placeholder } = toRefs(props);
|
||||
|
||||
const src = ref("");
|
||||
function update() {
|
||||
src.value = `${api.value}?from=${from.value}&width=${width.value}&height=${height.value}&r=${Toolkit.random(0, 999999)}`;
|
||||
const captchaId = ref("");
|
||||
const isLoading = ref(false);
|
||||
const captchaStyle = computed(() => {
|
||||
return {
|
||||
width: `${width.value / 16}rem`,
|
||||
height: `${height.value / 16}rem`
|
||||
};
|
||||
});
|
||||
|
||||
async function update(): Promise<void> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const result = await CommonAPI.captcha(width.value, height.value);
|
||||
captchaId.value = result.id;
|
||||
src.value = result.data;
|
||||
emit("update", result);
|
||||
} catch (error) {
|
||||
emit("error", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
onMounted(update);
|
||||
|
||||
onMounted(async () => {
|
||||
await update();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
captchaId,
|
||||
src,
|
||||
isLoading,
|
||||
update
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-captcha {
|
||||
cursor: var(--tui-cur-pointer);
|
||||
border: 1px solid gray;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
flex: 0 0 auto;
|
||||
pointer-events: all;
|
||||
place-items: center;
|
||||
border-radius: .5rem;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .64;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+203
-64
@@ -1,79 +1,218 @@
|
||||
import { LoginResponse, LoginToken, LoginUser, UserToken } from "../types";
|
||||
import {defineStore} from "pinia";
|
||||
import {computed, reactive, readonly, ref} from "vue";
|
||||
import {CaptchaData, LoginRequest, LoginResponse, LoginUser} from "../types";
|
||||
import Cooker from "../utils/Cooker";
|
||||
import Storage from "../utils/Storage";
|
||||
import UserAPI from "../api/UserAPI";
|
||||
import {UserAPI} from "../api";
|
||||
|
||||
const loginUser = reactive<LoginUser>({
|
||||
token: undefined,
|
||||
user: undefined
|
||||
});
|
||||
|
||||
function isLogged(): boolean {
|
||||
return !!loginUser.user;
|
||||
/** 用户 Store 配置项 */
|
||||
interface UserStoreOptions {
|
||||
/** localStorage 储存 key,默认 "loginUser" */
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
async function login4Token(token: LoginToken): Promise<LoginResponse | null> {
|
||||
loginUser.token = token;
|
||||
// 未过期
|
||||
try {
|
||||
const resp = await UserAPI.login4Token();
|
||||
await updateToken(resp);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
await logout();
|
||||
/** 访问控制匹配模式 */
|
||||
export type AccessMatchMode = "and" | "or";
|
||||
|
||||
let _storageKey = "loginUser";
|
||||
|
||||
type AccessItem = string | {
|
||||
moduleCode?: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
function normalizeAccessItem(item?: AccessItem): string {
|
||||
if (!item) {
|
||||
return "";
|
||||
}
|
||||
return null;
|
||||
|
||||
if (typeof item === "string") {
|
||||
return item.trim();
|
||||
}
|
||||
|
||||
const code = item.code?.trim();
|
||||
if (!code) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const moduleCode = item.moduleCode?.trim();
|
||||
return moduleCode && !code.includes(":") ? `${moduleCode}:${code}` : code;
|
||||
}
|
||||
|
||||
async function login4Storage(): Promise<LoginResponse | null> {
|
||||
if (!loginUser.user && (loginUser.token || Storage.has("token"))) {
|
||||
// 未登录,储存有令牌
|
||||
const token = Storage.getJSON("token") as UserToken;
|
||||
if (new Date().getTime() < token.expireAt) {
|
||||
return await login4Token(token);
|
||||
function normalizeAccessList(list?: AccessItem[]): string[] {
|
||||
if (!list?.length) {
|
||||
return [];
|
||||
}
|
||||
return list
|
||||
.map(normalizeAccessItem)
|
||||
.filter((item): item is string => !!item);
|
||||
}
|
||||
|
||||
function matchAccess(sourceList?: string[], targetList?: string | string[], mode: AccessMatchMode = "or"): boolean {
|
||||
const normalizedTargetList = normalizeAccessList(Array.isArray(targetList) ? targetList : [targetList || ""]);
|
||||
if (!normalizedTargetList.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const sourceSet = new Set(normalizeAccessList(sourceList));
|
||||
if (!sourceSet.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode === "and") {
|
||||
return normalizedTargetList.every(item => sourceSet.has(item));
|
||||
}
|
||||
return normalizedTargetList.some(item => sourceSet.has(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置用户 Store,应在使用 useUserStore 之前调用
|
||||
* @param options 配置项
|
||||
*/
|
||||
function configureUserStore(options: UserStoreOptions): void {
|
||||
if (options.storageKey) {
|
||||
_storageKey = options.storageKey;
|
||||
}
|
||||
}
|
||||
|
||||
const useUserStore = defineStore("user", () => {
|
||||
type LoginUserState = LoginUser & Record<string, unknown>;
|
||||
|
||||
const loginUser = reactive<LoginUserState>({});
|
||||
const isRestored = ref(false);
|
||||
|
||||
const token = computed(() => loginUser.token ?? "");
|
||||
const isLoggedIn = computed(() => isValidLoginUser(loginUser));
|
||||
|
||||
function resetLoginUser(nextUser?: LoginUserState): void {
|
||||
Object.keys(loginUser).forEach((key) => {
|
||||
delete loginUser[key];
|
||||
});
|
||||
if (nextUser) {
|
||||
Object.assign(loginUser, nextUser);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updateToken(loginResponse: LoginResponse): Promise<void> {
|
||||
loginUser.token = {
|
||||
id: loginResponse.id,
|
||||
value: loginResponse.token,
|
||||
expireAt: loginResponse.expireAt
|
||||
};
|
||||
loginUser.user = await UserAPI.view(loginResponse.id);
|
||||
|
||||
Storage.setJSON("token", {
|
||||
value: loginUser.token.value,
|
||||
expireAt: loginUser.token.expireAt
|
||||
} as UserToken);
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
await UserAPI.logout();
|
||||
|
||||
loginUser.token = undefined;
|
||||
loginUser.user = undefined;
|
||||
|
||||
Storage.remove("token");
|
||||
}
|
||||
|
||||
async function reloadProfile() {
|
||||
if (loginUser.token && loginUser.token.id) {
|
||||
loginUser.user = await UserAPI.view(loginUser.token.id);
|
||||
function isValidLoginUser(user?: LoginUserState | null): user is LoginResponse {
|
||||
return !!user?.token && !!user.expireAt && new Date().getTime() < user.expireAt;
|
||||
}
|
||||
}
|
||||
|
||||
const userStore = {
|
||||
loginUser,
|
||||
isLogged,
|
||||
login4Token,
|
||||
login4Storage,
|
||||
updateToken,
|
||||
logout,
|
||||
reloadProfile
|
||||
};
|
||||
function isLogged(): boolean {
|
||||
return isLoggedIn.value;
|
||||
}
|
||||
|
||||
function getLoginUser<T extends LoginUser = LoginUser>(): T | null {
|
||||
if (!isLogged()) {
|
||||
return null;
|
||||
}
|
||||
return loginUser as T;
|
||||
}
|
||||
|
||||
function getRoleList(): string[] {
|
||||
return normalizeAccessList(loginUser.user?.roleList);
|
||||
}
|
||||
|
||||
function getPermissionList(): string[] {
|
||||
return normalizeAccessList(loginUser.user?.permissionList);
|
||||
}
|
||||
|
||||
function hasRoles(roleList: string | string[], mode: AccessMatchMode = "or"): boolean {
|
||||
return matchAccess(getRoleList(), roleList, mode);
|
||||
}
|
||||
|
||||
function hasPermissions(permissionList: string | string[], mode: AccessMatchMode = "or"): boolean {
|
||||
return matchAccess(getPermissionList(), permissionList, mode);
|
||||
}
|
||||
|
||||
async function login<T extends LoginUser = LoginUser>(req: CaptchaData<LoginRequest>): Promise<T> {
|
||||
const result = await UserAPI.login(req);
|
||||
const nextUser = result as unknown as T;
|
||||
await updateLoginUser(nextUser);
|
||||
return nextUser;
|
||||
}
|
||||
|
||||
async function loginByToken<T extends LoginUser = LoginUser>(token: string): Promise<T | null> {
|
||||
loginUser.token = token;
|
||||
// 未过期
|
||||
try {
|
||||
const result = await UserAPI.loginByToken();
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const nextUser = result as unknown as T;
|
||||
await updateLoginUser(nextUser);
|
||||
return nextUser;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
clearLoginUser();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loginByStorage<T extends LoginUser = LoginUser>(): Promise<T | null> {
|
||||
if (loginUser.token) {
|
||||
isRestored.value = true;
|
||||
return getLoginUser<T>();
|
||||
}
|
||||
|
||||
if (!Storage.has(_storageKey)) {
|
||||
isRestored.value = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 未登录,储存有令牌。
|
||||
const storageUser = Storage.getJSON(_storageKey) as T;
|
||||
if (isValidLoginUser(storageUser)) {
|
||||
const result = await loginByToken<T>(storageUser.token);
|
||||
isRestored.value = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
clearLoginUser();
|
||||
isRestored.value = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updateLoginUser<T extends LoginUser = LoginUser>(user: T): Promise<void> {
|
||||
resetLoginUser(user as LoginUserState);
|
||||
Storage.setJSON(_storageKey, user);
|
||||
}
|
||||
|
||||
function clearLoginUser(): void {
|
||||
resetLoginUser();
|
||||
Cooker.remove("Token");
|
||||
Storage.remove(_storageKey);
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
try {
|
||||
await UserAPI.logout();
|
||||
} finally {
|
||||
clearLoginUser();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
isLoggedIn,
|
||||
isRestored,
|
||||
getRoleList,
|
||||
getPermissionList,
|
||||
hasRoles,
|
||||
hasPermissions,
|
||||
isLogged,
|
||||
loginUser: readonly(loginUser),
|
||||
getLoginUser,
|
||||
login,
|
||||
loginByToken,
|
||||
loginByStorage,
|
||||
updateLoginUser,
|
||||
clearLoginUser,
|
||||
logout
|
||||
};
|
||||
});
|
||||
|
||||
export {
|
||||
userStore
|
||||
useUserStore,
|
||||
configureUserStore
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AttachmentView } from "./Attachment";
|
||||
import type { Attachment } from "./Attachment";
|
||||
import type { Model } from "./Model";
|
||||
|
||||
// 文章
|
||||
@@ -17,7 +17,7 @@ export type Article<E extends ArticleMusicExtendData | ArticleSoftwareExtendData
|
||||
|
||||
export type ArticleView<E extends ArticleMusicExtendData | ArticleSoftwareExtendData> = {
|
||||
comments?: number;
|
||||
attachmentList: AttachmentView[];
|
||||
attachmentList: Attachment[];
|
||||
} & Article<E>;
|
||||
|
||||
export enum ArticleType {
|
||||
|
||||
+29
-11
@@ -2,23 +2,41 @@ import { Model } from "./Model";
|
||||
|
||||
export type Attachment = {
|
||||
bizType: AttachmentBizType;
|
||||
bizId: number;
|
||||
bizId: string;
|
||||
attachType?: string;
|
||||
mongoId: string;
|
||||
lidTitle?: number;
|
||||
name?: string;
|
||||
size: number;
|
||||
} & Model
|
||||
|
||||
export type AttachmentView = {
|
||||
title?: string;
|
||||
} & Attachment
|
||||
name?: string;
|
||||
mimeType?: string;
|
||||
metadata?: unknown;
|
||||
size?: number;
|
||||
md5?: string;
|
||||
uploaderIp?: string;
|
||||
isDestroyed?: boolean;
|
||||
destroyAt?: number;
|
||||
tempFileId?: string;
|
||||
} & Model;
|
||||
|
||||
export type TempFileResp = {
|
||||
id: string;
|
||||
expireAt?: number;
|
||||
};
|
||||
|
||||
export type BizUpdateReq = {
|
||||
bizType: AttachmentBizType;
|
||||
bizId: string;
|
||||
items: Partial<Attachment>[];
|
||||
};
|
||||
|
||||
export enum AttachmentBizType {
|
||||
|
||||
GIT_ISSUE,
|
||||
USER = "USER",
|
||||
|
||||
GIT_MERGE,
|
||||
GAO_CUSTOMER = "GAO_CUSTOMER",
|
||||
|
||||
GIT_RELEASE
|
||||
GAO_REGISTER_RECORD = "GAO_REGISTER_RECORD",
|
||||
|
||||
TEMP_FILE = "TEMP_FILE",
|
||||
|
||||
MIRROR = "MIRROR"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Model, Page } from "./Model";
|
||||
import { UserView } from "./User";
|
||||
import { User } from "./User";
|
||||
|
||||
/** 评论 */
|
||||
export type Comment = {
|
||||
@@ -10,7 +10,7 @@ export type Comment = {
|
||||
content?: string;
|
||||
|
||||
/** 所属用户 */
|
||||
user?: UserView;
|
||||
user?: User;
|
||||
|
||||
/** 回复列表 */
|
||||
replies?: CommentReply[];
|
||||
@@ -41,8 +41,8 @@ export type CommentReply = {
|
||||
content?: string;
|
||||
|
||||
comment?: Comment;
|
||||
sender?: UserView;
|
||||
receiver?: UserView;
|
||||
sender?: User;
|
||||
receiver?: User;
|
||||
} & Model;
|
||||
|
||||
export enum CommentReplyBizType {
|
||||
|
||||
+8
-21
@@ -6,7 +6,7 @@ export enum RunEnv {
|
||||
|
||||
// 基本实体模型
|
||||
export type Model = {
|
||||
id?: number;
|
||||
id?: string;
|
||||
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
@@ -23,8 +23,8 @@ export type Page<T> = {
|
||||
index: number;
|
||||
size: number;
|
||||
orderMap?: { [K in keyof T]?: OrderType };
|
||||
equalsExample?: T;
|
||||
likesExample?: T;
|
||||
equalsExample?: Partial<T>;
|
||||
likesExample?: Partial<T>;
|
||||
}
|
||||
|
||||
export enum OrderType {
|
||||
@@ -39,28 +39,15 @@ export type PageResult<T> = {
|
||||
|
||||
// 携带验证码的请求体
|
||||
export type CaptchaData<T> = {
|
||||
from: string;
|
||||
captchaId: string;
|
||||
captcha: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export enum CaptchaFrom {
|
||||
|
||||
LOGIN = "LOGIN",
|
||||
|
||||
REGISTER = "REGISTER",
|
||||
|
||||
/** 评论 */
|
||||
COMMENT = "COMMENT",
|
||||
|
||||
/** 评论回复 */
|
||||
COMMENT_REPLY = "COMMENT_REPLY",
|
||||
|
||||
/** Git 反馈 */
|
||||
GIT_ISSUE = "GIT_ISSUE",
|
||||
|
||||
/** Git 合并请求 */
|
||||
GIT_MERGE = "GIT_MERGE",
|
||||
// 图形验证码
|
||||
export type CaptchaResult = {
|
||||
id: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export enum ImageType {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export type ModuleCode = "CORE" | "GAO";
|
||||
|
||||
export interface Permission {
|
||||
id: string;
|
||||
moduleCode: ModuleCode;
|
||||
code: string;
|
||||
nameLangId?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
deletedAt?: number;
|
||||
}
|
||||
|
||||
export type PermissionPayload = Pick<Permission, "moduleCode" | "code" | "nameLangId" | "name" | "description"> & {
|
||||
id?: string;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ModuleCode, Permission } from "./Permission";
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
moduleCode: ModuleCode;
|
||||
code: string;
|
||||
nameLangId?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
permissionList?: Permission[];
|
||||
allPermissionList?: Permission[];
|
||||
childRoleList?: Role[];
|
||||
permissionIdList?: string[];
|
||||
childRoleIdList?: string[];
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
deletedAt?: number;
|
||||
}
|
||||
|
||||
export type RolePayload = Pick<Role, "moduleCode" | "code" | "nameLangId" | "name" | "description"> & {
|
||||
id?: string;
|
||||
permissionIdList?: string[];
|
||||
childRoleIdList?: string[];
|
||||
};
|
||||
|
||||
export interface UserRoleAuthorizeReq {
|
||||
userId: string;
|
||||
moduleCode?: ModuleCode;
|
||||
roleIdList?: string[];
|
||||
roleCodeList?: string[];
|
||||
}
|
||||
+48
-47
@@ -1,79 +1,80 @@
|
||||
import { ImageType, Model } from "./Model";
|
||||
import { AttachmentView } from "./Attachment";
|
||||
import { Attachment } from "./Attachment";
|
||||
import type { Setting } from "./Setting";
|
||||
|
||||
export type User = {
|
||||
name: string;
|
||||
nick?: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
emailVerifyAt: number;
|
||||
unmuteAt?: number;
|
||||
unbanAt?: number;
|
||||
} & Model;
|
||||
|
||||
export type UserView = {
|
||||
profile: UserProfileView
|
||||
} & User;
|
||||
|
||||
export enum UserAttachType {
|
||||
|
||||
AVATAR,
|
||||
|
||||
WRAPPER,
|
||||
|
||||
DEFAULT_AVATAR,
|
||||
|
||||
DEFAULT_WRAPPER,
|
||||
}
|
||||
|
||||
export type UserProfile = {
|
||||
userId: number;
|
||||
avatarType: ImageType;
|
||||
wrapperType: ImageType;
|
||||
|
||||
exp: number;
|
||||
emailVerifyAt?: number;
|
||||
phoneNo?: string;
|
||||
phoneNoVerifyAt?: number;
|
||||
wrapperType?: ImageType;
|
||||
avatarType?: ImageType;
|
||||
exp?: number;
|
||||
sex?: number;
|
||||
birthdate?: number;
|
||||
qq?: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
lastLoginIP?: string;
|
||||
lastLoginAt?: number;
|
||||
updatedAt?: number;
|
||||
}
|
||||
unmuteAt?: number;
|
||||
unbanAt?: number;
|
||||
roleList?: string[];
|
||||
permissionList?: string[];
|
||||
attachmentList?: Attachment[];
|
||||
settingList?: Setting[];
|
||||
tempFileIdList?: string[];
|
||||
muting?: boolean;
|
||||
banning?: boolean;
|
||||
emailVerified?: boolean;
|
||||
phoneNoVerified?: boolean;
|
||||
} & Model;
|
||||
|
||||
export type UserProfileView = {
|
||||
attachmentList?: AttachmentView[]
|
||||
} & UserProfile
|
||||
export enum UserAttachType {
|
||||
|
||||
export type UserToken = {
|
||||
value: string;
|
||||
expireAt: number;
|
||||
AVATAR = "AVATAR",
|
||||
|
||||
WRAPPER = "WRAPPER",
|
||||
|
||||
LICENSE = "LICENSE",
|
||||
|
||||
DEFAULT_AVATAR = "DEFAULT_AVATAR",
|
||||
|
||||
DEFAULT_WRAPPER = "DEFAULT_WRAPPER"
|
||||
}
|
||||
|
||||
export type RegisterRequest = {
|
||||
name: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type LoginRequest = {
|
||||
user: string;
|
||||
password: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type UpdatePasswordRequest = {
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
};
|
||||
|
||||
// 登录返回
|
||||
export type LoginResponse = {
|
||||
id: number;
|
||||
id: string;
|
||||
token: string;
|
||||
expireAt: number;
|
||||
}
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export type LoginUser = {
|
||||
token?: LoginToken;
|
||||
user?: UserView
|
||||
}
|
||||
|
||||
export type LoginToken = {
|
||||
id?: number;
|
||||
} & UserToken;
|
||||
id?: string;
|
||||
token?: string;
|
||||
expireAt?: number;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export enum LoginType {
|
||||
ALERT,
|
||||
|
||||
@@ -5,6 +5,8 @@ export * from "./Article";
|
||||
export * from "./Setting";
|
||||
export * from "./Attachment";
|
||||
export * from "./Model";
|
||||
export * from "./Permission";
|
||||
export * from "./Role";
|
||||
export * from "./User";
|
||||
export * from "./Template";
|
||||
export * from "./Comment";
|
||||
|
||||
@@ -7,6 +7,15 @@ export type ScrollListener = {
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export type ScrollBottomListenerOptions = {
|
||||
|
||||
/** 距离底部小于等于该值时触发 */
|
||||
distance?: number;
|
||||
|
||||
/** 返回 true 时才触发 */
|
||||
canLoad?: () => boolean;
|
||||
}
|
||||
|
||||
export default class Scroller {
|
||||
|
||||
private static instance: Scroller;
|
||||
@@ -53,6 +62,26 @@ export default class Scroller {
|
||||
Scroller.getInstance().listeners.set(name, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加触底监听
|
||||
*
|
||||
* @param name 事件名
|
||||
* @param listener 触底回调
|
||||
* @param options 触底配置
|
||||
*/
|
||||
public static addBottomListener(name: string, listener: (event: ScrollListener) => void | Promise<void>, options: ScrollBottomListenerOptions = {}) {
|
||||
const distance = options.distance ?? 120;
|
||||
Scroller.addListener(name, event => {
|
||||
if (distance < event.bottom) {
|
||||
return;
|
||||
}
|
||||
if (options.canLoad && !options.canLoad()) {
|
||||
return;
|
||||
}
|
||||
void listener(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除监听
|
||||
*
|
||||
|
||||
@@ -30,6 +30,10 @@ export default class Text {
|
||||
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
|
||||
}
|
||||
|
||||
public static pad(value: number | string, length = 2, fill = "0"): string {
|
||||
return value.toString().padStart(length, fill);
|
||||
}
|
||||
|
||||
public static unitCompact(val: number | string | null | undefined, unit: string, fixed = 0): string {
|
||||
return this.unit(val, unit, fixed, true);
|
||||
};
|
||||
|
||||
+25
-6
@@ -1,4 +1,5 @@
|
||||
import Toolkit from "./Toolkit";
|
||||
import Text from "./Text";
|
||||
|
||||
export default class Time {
|
||||
|
||||
@@ -15,6 +16,25 @@ export default class Time {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
public static toTimestamp(date: Date): number {
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
public static toDateObject(unix?: number): Date | undefined {
|
||||
if (!unix) {
|
||||
return undefined;
|
||||
}
|
||||
return new Date(unix);
|
||||
}
|
||||
|
||||
public static formatDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${Text.pad(date.getMonth() + 1)}-${Text.pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
public static formatDateTime(date: Date): string {
|
||||
return `${this.formatDate(date)} ${Text.pad(date.getHours())}:${Text.pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期
|
||||
*
|
||||
@@ -22,8 +42,7 @@ export default class Time {
|
||||
*/
|
||||
public static toDate(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`;
|
||||
return this.formatDate(new Date(unix));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,13 +53,13 @@ export default class Time {
|
||||
public static toTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
||||
return `${Text.pad(d.getHours())}:${Text.pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
public static toShortTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}`;
|
||||
return `${Text.pad(d.getMinutes())}:${Text.pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,9 +150,9 @@ export default class Time {
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const second = seconds % 60;
|
||||
if (0 < hours) {
|
||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
||||
return `${hours}:${Text.pad(minutes)}:${Text.pad(second)}`;
|
||||
}
|
||||
return `${minutes.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
||||
return `${Text.pad(minutes)}:${Text.pad(second)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -398,4 +398,15 @@ export default class Toolkit {
|
||||
}
|
||||
return JSON.stringify(JSON.parse(jsonStr), null, 4);
|
||||
}
|
||||
|
||||
public static downloadBlob(blob: Blob, fileName: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import type { Directive, DirectiveBinding, EffectScope } from "vue";
|
||||
import { effectScope, watch } from "vue";
|
||||
import { AccessMatchMode, useUserStore } from "../../store";
|
||||
|
||||
/** 访问控制类型 */
|
||||
export type AccessControlType = "permission" | "role";
|
||||
/** 指令结果生效方式 */
|
||||
export type AccessEffectMode = "if" | "show";
|
||||
|
||||
/** 自定义回调参数 */
|
||||
export type AccessDirectiveCallbackPayload = {
|
||||
type: AccessControlType;
|
||||
granted: boolean;
|
||||
valueList: string[];
|
||||
mode: AccessMatchMode;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问控制指令配置
|
||||
* `value` 支持单个值或值数组
|
||||
* 数组默认按 `or` 逻辑处理,需要 `and` 时显式传 `mode`
|
||||
* `callback` 存在时优先执行回调,不再使用 `effect`
|
||||
*/
|
||||
export type AccessDirectiveConfig = {
|
||||
value: string | string[];
|
||||
mode?: AccessMatchMode;
|
||||
effect?: AccessEffectMode;
|
||||
reverse?: boolean;
|
||||
display?: string;
|
||||
callback?: (payload: AccessDirectiveCallbackPayload) => void;
|
||||
}
|
||||
|
||||
type AccessDirectiveValue = string | string[] | AccessDirectiveConfig;
|
||||
|
||||
type AccessDirectiveState = {
|
||||
scope: EffectScope;
|
||||
placeholder: Comment | null;
|
||||
display: string;
|
||||
config: AccessDirectiveConfig;
|
||||
lastGranted?: boolean;
|
||||
}
|
||||
|
||||
type AccessMatcher = (store: ReturnType<typeof useUserStore>, valueList: string[], mode: AccessMatchMode) => boolean;
|
||||
|
||||
const ACCESS_STATE_KEY = "__vAccessState";
|
||||
|
||||
/** 过滤空字符串并统一裁剪空白 */
|
||||
function normalizeValueList(value: string | string[]): string[] {
|
||||
return (Array.isArray(value) ? value : [value])
|
||||
.map(item => item?.trim())
|
||||
.filter((item): item is string => !!item);
|
||||
}
|
||||
|
||||
/** 统一把指令值转成标准配置 */
|
||||
function normalizeConfig(value: AccessDirectiveValue): AccessDirectiveConfig {
|
||||
if (typeof value === "string" || Array.isArray(value)) {
|
||||
return {
|
||||
value,
|
||||
mode: "or",
|
||||
effect: "if",
|
||||
reverse: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: value?.value || [],
|
||||
mode: value?.mode || "or",
|
||||
effect: value?.effect || "if",
|
||||
reverse: !!value?.reverse,
|
||||
display: value?.display,
|
||||
callback: value?.callback
|
||||
};
|
||||
}
|
||||
|
||||
/** 恢复被 `if` 模式替换掉的真实元素 */
|
||||
function restoreElement(el: HTMLElement, state: AccessDirectiveState): void {
|
||||
if (state.placeholder?.parentNode) {
|
||||
state.placeholder.parentNode.replaceChild(el, state.placeholder);
|
||||
}
|
||||
state.placeholder = null;
|
||||
}
|
||||
|
||||
/** `if` 模式:无权限时直接移出 DOM */
|
||||
function applyIfEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
|
||||
if (granted) {
|
||||
restoreElement(el, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.placeholder && el.parentNode) {
|
||||
state.placeholder = document.createComment("v-access");
|
||||
el.parentNode.replaceChild(state.placeholder, el);
|
||||
}
|
||||
}
|
||||
|
||||
/** `show` 模式:保留 DOM,仅切换 display */
|
||||
function applyShowEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
|
||||
restoreElement(el, state);
|
||||
el.style.display = granted ? state.display : "none";
|
||||
}
|
||||
|
||||
/** 自定义模式:交给调用方自己处理样式或行为 */
|
||||
function applyCustomCallback(
|
||||
el: HTMLElement,
|
||||
state: AccessDirectiveState,
|
||||
granted: boolean,
|
||||
type: AccessControlType,
|
||||
valueList: string[],
|
||||
mode: AccessMatchMode
|
||||
): void {
|
||||
restoreElement(el, state);
|
||||
if (state.lastGranted === granted) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.config.callback?.({
|
||||
type,
|
||||
granted,
|
||||
valueList,
|
||||
mode,
|
||||
el
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据当前登录用户状态刷新指令效果 */
|
||||
function updateAccess(
|
||||
el: HTMLElement,
|
||||
state: AccessDirectiveState,
|
||||
type: AccessControlType,
|
||||
matcher: AccessMatcher
|
||||
): void {
|
||||
const store = useUserStore();
|
||||
const config = state.config;
|
||||
const mode = config.mode || "or";
|
||||
const valueList = normalizeValueList(config.value);
|
||||
const granted = config.reverse ? !matcher(store, valueList, mode) : matcher(store, valueList, mode);
|
||||
|
||||
if (config.callback) {
|
||||
applyCustomCallback(el, state, granted, type, valueList, mode);
|
||||
} else if (config.effect === "show") {
|
||||
applyShowEffect(el, state, granted);
|
||||
} else {
|
||||
applyIfEffect(el, state, granted);
|
||||
}
|
||||
|
||||
state.lastGranted = granted;
|
||||
}
|
||||
|
||||
function getState(el: HTMLElement): AccessDirectiveState | undefined {
|
||||
return (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
|
||||
}
|
||||
|
||||
function setState(el: HTMLElement, state: AccessDirectiveState): void {
|
||||
(el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY] = state;
|
||||
}
|
||||
|
||||
function clearState(el: HTMLElement): void {
|
||||
delete (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
|
||||
}
|
||||
|
||||
/** 卸载时清掉占位注释,避免遗留无用节点 */
|
||||
function clearPlaceholder(state: AccessDirectiveState): void {
|
||||
if (state.placeholder?.parentNode) {
|
||||
state.placeholder.parentNode.removeChild(state.placeholder);
|
||||
}
|
||||
state.placeholder = null;
|
||||
}
|
||||
|
||||
/** 创建角色/权限通用指令 */
|
||||
function createAccessDirective(type: AccessControlType, matcher: AccessMatcher): Directive<HTMLElement, AccessDirectiveValue> {
|
||||
return {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<AccessDirectiveValue>) {
|
||||
const state: AccessDirectiveState = {
|
||||
scope: effectScope(),
|
||||
placeholder: null,
|
||||
display: el.style.display,
|
||||
config: normalizeConfig(binding.value)
|
||||
};
|
||||
|
||||
setState(el, state);
|
||||
|
||||
state.scope.run(() => {
|
||||
const store = useUserStore();
|
||||
watch(
|
||||
() => {
|
||||
return type === "permission" ? store.getPermissionList().join("|") : store.getRoleList().join("|");
|
||||
},
|
||||
() => updateAccess(el, state, type, matcher),
|
||||
{ immediate: true }
|
||||
);
|
||||
});
|
||||
},
|
||||
updated(el: HTMLElement, binding: DirectiveBinding<AccessDirectiveValue>) {
|
||||
const state = getState(el);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.config = normalizeConfig(binding.value);
|
||||
updateAccess(el, state, type, matcher);
|
||||
},
|
||||
unmounted(el: HTMLElement) {
|
||||
const state = getState(el);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.scope.stop();
|
||||
clearPlaceholder(state);
|
||||
clearState(el);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 权限指令:`v-can` */
|
||||
export const VCan = createAccessDirective("permission", (store, valueList, mode) => {
|
||||
return store.hasPermissions(valueList, mode);
|
||||
});
|
||||
|
||||
/** 角色指令:`v-role` */
|
||||
export const VRole = createAccessDirective("role", (store, valueList, mode) => {
|
||||
return store.hasRoles(valueList, mode);
|
||||
});
|
||||
|
||||
export default VCan;
|
||||
@@ -13,11 +13,13 @@ export { default as Toolkit } from "./Toolkit";
|
||||
export { default as IconMapper } from "./IconMapper";
|
||||
export { default as SettingMapper } from "./SettingMapper";
|
||||
|
||||
export { default as VCan } from "./directives/Access";
|
||||
export { default as VDraggable } from "./directives/Draggable";
|
||||
export { default as VPopup } from "./directives/Popup";
|
||||
|
||||
export * from "./MethodLocker";
|
||||
export * from "./Network";
|
||||
export * from "./Prismjs";
|
||||
export * from "./directives/Access";
|
||||
export * from "./directives/Draggable";
|
||||
export * from "./directives/Popup";
|
||||
|
||||
Reference in New Issue
Block a user