From f0d1090932a15f6ac80d72825e3926424793277d Mon Sep 17 00:00:00 2001 From: Timi Date: Thu, 30 Jul 2026 18:28:25 +0800 Subject: [PATCH] update --- .serena/.gitignore | 2 + .serena/project.yml | 154 +++++++++++++++++++ package.json | 7 +- pnpm-lock.yaml | 218 +++++++++++++++++++-------- src/api/ArticleAPI.ts | 4 +- src/api/AttachmentAPI.ts | 58 ++++++++ src/api/CommonAPI.ts | 22 ++- src/api/PermissionAPI.ts | 29 ++++ src/api/RoleAPI.ts | 55 +++++++ src/api/UserAPI.ts | 66 ++++----- src/api/index.ts | 3 + src/components/captcha/index.vue | 99 ++++++++++--- src/store/userStore.ts | 244 +++++++++++++++++++++++-------- src/types/Article.ts | 4 +- src/types/Attachment.ts | 40 +++-- src/types/Comment.ts | 8 +- src/types/Model.ts | 25 +--- src/types/Permission.ts | 17 +++ src/types/Role.ts | 31 ++++ src/types/User.ts | 95 ++++++------ src/types/index.ts | 2 + 21 files changed, 915 insertions(+), 268 deletions(-) create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 src/api/AttachmentAPI.ts create mode 100644 src/api/PermissionAPI.ts create mode 100644 src/api/RoleAPI.ts create mode 100644 src/types/Permission.ts create mode 100644 src/types/Role.ts diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..2e510af --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..b95d03c --- /dev/null +++ b/.serena/project.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: [] diff --git a/package.json b/package.json index 3b2b71e..6f00cff 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183e10e..272f7ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/api/ArticleAPI.ts b/src/api/ArticleAPI.ts index 597d918..9e03b27 100644 --- a/src/api/ArticleAPI.ts +++ b/src/api/ArticleAPI.ts @@ -7,7 +7,7 @@ import { axios } from "./BaseAPI"; * @param id 文章 ID * @returns 文章数据 */ -async function view(id: number): Promise> { +async function view(id: string): Promise> { return axios.get(`/article/${id}`); } @@ -17,7 +17,7 @@ async function view(id: number): Promise> { * @param id 文章 ID * @returns 最新喜欢数量 */ -async function like(id: number): Promise { +async function like(id: string): Promise { return axios.get(`/article/like/${id}`); } diff --git a/src/api/AttachmentAPI.ts b/src/api/AttachmentAPI.ts new file mode 100644 index 0000000..d8348d2 --- /dev/null +++ b/src/api/AttachmentAPI.ts @@ -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 { + 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 { + 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 { + return await axios.get(`${BASE_URI}/list/biz`, { + params: { + bizType, + bizId, + attachTypeList + } + }); +} + +export default { + uploadTemp, + updateByBiz, + listByBiz, + getReadURL: CommonAPI.getAttachmentReadAPI, + getTempReadURL: CommonAPI.getAttachmentTempReadAPI +}; diff --git a/src/api/CommonAPI.ts b/src/api/CommonAPI.ts index 6bd31c1..6b6e3c3 100644 --- a/src/api/CommonAPI.ts +++ b/src/api/CommonAPI.ts @@ -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 { + 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 { return axios.get(`/template?bizType=${bizType}&bizCode=${code}`); @@ -30,7 +44,9 @@ async function settingMap(map: Record): Promise): Promise> { + return axios.post(`${BASE_URI}/list`, req); +} + +async function create(req: PermissionPayload): Promise { + return axios.post(`${BASE_URI}/create`, req); +} + +async function update(req: PermissionPayload): Promise { + return axios.post(`${BASE_URI}/update`, req); +} + +async function remove(id: string): Promise { + return axios.post(`${BASE_URI}/delete`, undefined, { + params: { id } + }); +} + +export default { + list, + create, + update, + remove +}; diff --git a/src/api/RoleAPI.ts b/src/api/RoleAPI.ts new file mode 100644 index 0000000..dbce1bd --- /dev/null +++ b/src/api/RoleAPI.ts @@ -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): Promise> { + return axios.post(`${BASE_URI}/list`, req); +} + +async function detail(id: string): Promise { + return axios.post(`${BASE_URI}/detail`, undefined, { + params: { id } + }); +} + +async function create(req: RolePayload): Promise { + return axios.post(`${BASE_URI}/create`, req); +} + +async function update(req: RolePayload): Promise { + return axios.post(`${BASE_URI}/update`, req); +} + +async function remove(id: string): Promise { + return axios.post(`${BASE_URI}/delete`, undefined, { + params: { id } + }); +} + +async function authorizedList(userId: string): Promise { + return axios.post(`${BASE_URI}/authorized/list`, undefined, { + params: { userId } + }); +} + +async function authorize(req: UserRoleAuthorizeReq): Promise { + return axios.post(`${BASE_URI}/authorized/create`, req); +} + +async function authorizedPermission(userId: string): Promise { + return axios.post(`${BASE_URI}/authorized/permission`, undefined, { + params: { userId } + }); +} + +export default { + list, + detail, + create, + update, + remove, + authorizedList, + authorize, + authorizedPermission +}; diff --git a/src/api/UserAPI.ts b/src/api/UserAPI.ts index 50f4564..1456bcc 100644 --- a/src/api/UserAPI.ts +++ b/src/api/UserAPI.ts @@ -1,30 +1,21 @@ -import { - Attachment, - CaptchaData, - LoginRequest, - LoginResponse, - RegisterRequest, - UserAttachType, - UserProfileView, - UserView -} from "../types"; -import { axios } from "./BaseAPI"; +import {Attachment, CaptchaData, LoginRequest, LoginResponse, RegisterRequest, UpdatePasswordRequest, User, UserAttachType} from "../types"; +import {axios} from "./BaseAPI"; import CommonAPI from "./CommonAPI"; const BASE_URI = "/user"; -async function register(captchaData: CaptchaData): Promise { - return axios.post(`${BASE_URI}/register`, captchaData); +async function register(req: CaptchaData): Promise { + return await axios.post(`${BASE_URI}/register`, req); } /** * 登录 * - * @param captchaData 验证码登录对象 + * @param req 验证码登录对象 * @returns LoginResponse */ -async function login(captchaData: CaptchaData): Promise { - return axios.post(`${BASE_URI}/login`, captchaData); +async function login(req: CaptchaData): Promise { + return await axios.post(`${BASE_URI}/login`, req); } /** @@ -32,12 +23,20 @@ async function login(captchaData: CaptchaData): Promise { - return axios.post(`${BASE_URI}/login/token`); +async function loginByToken(): Promise { + return await axios.post(`${BASE_URI}/login/token`); } async function logout(): Promise { - return axios.post(`${BASE_URI}/logout`); + return await axios.post(`${BASE_URI}/logout`); +} + +async function updateCurrent(req: User): Promise { + return await axios.post(`${BASE_URI}/current/update`, req); +} + +async function updatePassword(req: UpdatePasswordRequest): Promise { + return await axios.post(`${BASE_URI}/update/password`, req); } /** @@ -46,29 +45,28 @@ async function logout(): Promise { * @param id 用户 ID * @returns 用户数据 */ -async function view(id: number): Promise { - return axios.post(`${BASE_URI}/view/${id}`); +async function view(id: string): Promise { + 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?: User) { + 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?: User) { + if (user?.attachmentList) { + return findAttachmentByType(user.attachmentList, [UserAttachType.WRAPPER, UserAttachType.DEFAULT_WRAPPER]); } } function findAttachmentByType(attachmentList: Attachment[], types: UserAttachType[]) { for (let i = 0; i < attachmentList.length; i++) { - const attachType = (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 +74,10 @@ function findAttachmentByType(attachmentList: Attachment[], types: UserAttachTyp export default { register, login, - login4Token, + loginByToken, logout, + updateCurrent, + updatePassword, view, getAvatarURL, diff --git a/src/api/index.ts b/src/api/index.ts index 7350688..ce114c8 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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"; diff --git a/src/components/captcha/index.vue b/src/components/captcha/index.vue index 57036d4..275aad3 100644 --- a/src/components/captcha/index.vue +++ b/src/components/captcha/index.vue @@ -1,46 +1,103 @@ diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 9bc627b..faf40dc 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -1,79 +1,195 @@ -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({ - token: undefined, - user: undefined -}); - -function isLogged(): boolean { - return !!loginUser.user; +/** 用户 Store 配置项 */ +interface UserStoreOptions { + /** localStorage 储存 key,默认 "loginUser" */ + storageKey?: string; } -async function login4Token(token: LoginToken): Promise { - 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"; + +function normalizeAccessList(list?: string[]): string[] { + if (!list?.length) { + return []; } - return null; + return list + .map(item => item?.trim()) + .filter((item): item is string => !!item); } -async function login4Storage(): Promise { - 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 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; + + const loginUser = reactive({}); + 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 { - 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 { - 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 | 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(req: CaptchaData): Promise { + const result = await UserAPI.login(req); + const nextUser = result as unknown as T; + await updateLoginUser(nextUser); + return nextUser; + } + + async function loginByToken(token: string): Promise { + 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(): Promise { + if (loginUser.token) { + isRestored.value = true; + return getLoginUser(); + } + + if (!Storage.has(_storageKey)) { + isRestored.value = true; + return null; + } + + // 未登录,储存有令牌。 + const storageUser = Storage.getJSON(_storageKey) as T; + if (isValidLoginUser(storageUser)) { + const result = await loginByToken(storageUser.token); + isRestored.value = true; + return result; + } + + clearLoginUser(); + isRestored.value = true; + return null; + } + + async function updateLoginUser(user: T): Promise { + resetLoginUser(user as LoginUserState); + Storage.setJSON(_storageKey, user); + } + + function clearLoginUser(): void { + resetLoginUser(); + Cooker.remove("Token"); + Storage.remove(_storageKey); + } + + async function logout(): Promise { + 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 }; diff --git a/src/types/Article.ts b/src/types/Article.ts index b976b09..0fd4300 100644 --- a/src/types/Article.ts +++ b/src/types/Article.ts @@ -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 = { comments?: number; - attachmentList: AttachmentView[]; + attachmentList: Attachment[]; } & Article; export enum ArticleType { diff --git a/src/types/Attachment.ts b/src/types/Attachment.ts index 575a718..531e813 100644 --- a/src/types/Attachment.ts +++ b/src/types/Attachment.ts @@ -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[]; +}; 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" } diff --git a/src/types/Comment.ts b/src/types/Comment.ts index 659721c..5faa8c4 100644 --- a/src/types/Comment.ts +++ b/src/types/Comment.ts @@ -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 { diff --git a/src/types/Model.ts b/src/types/Model.ts index ab28e74..7b5ad88 100644 --- a/src/types/Model.ts +++ b/src/types/Model.ts @@ -6,7 +6,7 @@ export enum RunEnv { // 基本实体模型 export type Model = { - id?: number; + id?: string; createdAt?: number; updatedAt?: number; @@ -39,28 +39,15 @@ export type PageResult = { // 携带验证码的请求体 export type CaptchaData = { - 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 { diff --git a/src/types/Permission.ts b/src/types/Permission.ts new file mode 100644 index 0000000..fc12fa6 --- /dev/null +++ b/src/types/Permission.ts @@ -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 & { + id?: string; +}; diff --git a/src/types/Role.ts b/src/types/Role.ts new file mode 100644 index 0000000..6f99c23 --- /dev/null +++ b/src/types/Role.ts @@ -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 & { + id?: string; + permissionIdList?: string[]; + childRoleIdList?: string[]; +}; + +export interface UserRoleAuthorizeReq { + userId: string; + moduleCode?: ModuleCode; + roleIdList?: string[]; + roleCodeList?: string[]; +} diff --git a/src/types/User.ts b/src/types/User.ts index 4b57d45..90f3975 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -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, diff --git a/src/types/index.ts b/src/types/index.ts index 0523d5c..2896bf1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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";