初始化
This commit is contained in:
commit
039eeb6670
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
||||||
1
.commitlintrc.js
Normal file
1
.commitlintrc.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/commitlint-config';
|
||||||
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
dist
|
||||||
|
.turbo
|
||||||
|
dist.zip
|
||||||
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=lf
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
max_line_length = 100
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.{yml,yaml,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
|
||||||
|
|
||||||
|
# Automatically normalize line endings (to LF) for all text-based files.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Declare files that will always have CRLF line endings on checkout.
|
||||||
|
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||||
|
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||||
|
|
||||||
|
# Denote all files that are truly binary and should not be modified.
|
||||||
|
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
|
||||||
2
.gitconfig
Normal file
2
.gitconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[core]
|
||||||
|
ignorecase = false
|
||||||
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
dist.zip
|
||||||
|
dist.tar
|
||||||
|
dist.war
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
*-dist.zip
|
||||||
|
*-dist.tar
|
||||||
|
*-dist.war
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
**/.vitepress/cache
|
||||||
|
.cache
|
||||||
|
.turbo
|
||||||
|
.temp
|
||||||
|
dev-dist
|
||||||
|
.stylelintcache
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
.VSCodeCounter
|
||||||
|
**/backend-mock/data
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
vite.config.mts.*
|
||||||
|
vite.config.mjs.*
|
||||||
|
vite.config.js.*
|
||||||
|
vite.config.ts.*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
# .vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.history
|
||||||
|
/docs
|
||||||
|
/playground
|
||||||
|
/apps/backend-mock
|
||||||
|
/playground
|
||||||
|
/docs
|
||||||
6
.gitpod.yml
Normal file
6
.gitpod.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
ports:
|
||||||
|
- port: 5555
|
||||||
|
onOpen: open-preview
|
||||||
|
tasks:
|
||||||
|
- init: corepack enable && pnpm install
|
||||||
|
command: pnpm run dev:play
|
||||||
20
.lintstagedrc.mjs
Normal file
20
.lintstagedrc.mjs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
'*.md': ['prettier --cache --ignore-unknown --write'],
|
||||||
|
'*.vue': [
|
||||||
|
'prettier --write',
|
||||||
|
'eslint --cache --fix',
|
||||||
|
'stylelint --fix --allow-empty-input',
|
||||||
|
],
|
||||||
|
'*.{js,jsx,ts,tsx}': [
|
||||||
|
'prettier --cache --ignore-unknown --write',
|
||||||
|
'eslint --cache --fix',
|
||||||
|
],
|
||||||
|
'*.{scss,less,styl,html,vue,css}': [
|
||||||
|
'prettier --cache --ignore-unknown --write',
|
||||||
|
'stylelint --fix --allow-empty-input',
|
||||||
|
],
|
||||||
|
'package.json': ['prettier --cache --write'],
|
||||||
|
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
|
||||||
|
'prettier --cache --write--parser json',
|
||||||
|
],
|
||||||
|
};
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
20.14.0
|
||||||
13
.npmrc
Normal file
13
.npmrc
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
registry = "https://registry.npmmirror.com"
|
||||||
|
public-hoist-pattern[]=husky
|
||||||
|
public-hoist-pattern[]=eslint
|
||||||
|
public-hoist-pattern[]=prettier
|
||||||
|
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||||
|
public-hoist-pattern[]=stylelint
|
||||||
|
public-hoist-pattern[]=*postcss*
|
||||||
|
public-hoist-pattern[]=@commitlint/*
|
||||||
|
public-hoist-pattern[]=czg
|
||||||
|
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
auto-install-peers=true
|
||||||
|
dedupe-peer-dependents=true
|
||||||
18
.prettierignore
Normal file
18
.prettierignore
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
dist
|
||||||
|
dev-dist
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
node_modules
|
||||||
|
.nvmrc
|
||||||
|
coverage
|
||||||
|
CODEOWNERS
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
||||||
|
|
||||||
|
public
|
||||||
|
.npmrc
|
||||||
|
*-lock.yaml
|
||||||
1
.prettierrc.mjs
Normal file
1
.prettierrc.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/prettier-config';
|
||||||
4
.stylelintignore
Normal file
4
.stylelintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
public
|
||||||
|
__tests__
|
||||||
|
coverage
|
||||||
30
.vscode/extensions.json
vendored
Normal file
30
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Vue 3 的语言支持
|
||||||
|
"Vue.volar",
|
||||||
|
// 将 ESLint JavaScript 集成到 VS Code 中。
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
// Visual Studio Code 的官方 Stylelint 扩展
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
// 使用 Prettier 的代码格式化程序
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
// 支持 dotenv 文件语法
|
||||||
|
"mikestead.dotenv",
|
||||||
|
// 源代码的拼写检查器
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
// Tailwind CSS 的官方 VS Code 插件
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
// iconify 图标插件
|
||||||
|
"antfu.iconify",
|
||||||
|
// i18n 插件
|
||||||
|
"Lokalise.i18n-ally",
|
||||||
|
// CSS 变量提示
|
||||||
|
"vunguyentuan.vscode-css-variables",
|
||||||
|
// 在 package.json 中显示 PNPM catalog 的版本
|
||||||
|
"antfu.pnpm-catalog-lens"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
// 和 volar 冲突
|
||||||
|
"octref.vetur"
|
||||||
|
]
|
||||||
|
}
|
||||||
37
.vscode/global.code-snippets
vendored
Normal file
37
.vscode/global.code-snippets
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"import": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "im",
|
||||||
|
"body": ["import { $2 } from '$1';"],
|
||||||
|
"description": "Import a module",
|
||||||
|
},
|
||||||
|
"export-all": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "ex",
|
||||||
|
"body": ["export * from '$1';"],
|
||||||
|
"description": "Export a module",
|
||||||
|
},
|
||||||
|
"vue-script-setup": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "<sc",
|
||||||
|
"body": [
|
||||||
|
"<script setup lang=\"ts\">",
|
||||||
|
"const props = defineProps<{",
|
||||||
|
" modelValue?: boolean,",
|
||||||
|
"}>()",
|
||||||
|
"$1",
|
||||||
|
"</script>",
|
||||||
|
"",
|
||||||
|
"<template>",
|
||||||
|
" <div>",
|
||||||
|
" <slot/>",
|
||||||
|
" </div>",
|
||||||
|
"</template>",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"vue-computed": {
|
||||||
|
"scope": "javascript,typescript,vue",
|
||||||
|
"prefix": "com",
|
||||||
|
"body": ["computed(() => { $1 })"],
|
||||||
|
},
|
||||||
|
}
|
||||||
42
.vscode/launch.json
vendored
Normal file
42
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin playground dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5555",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/playground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin antd dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5666",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/apps/web-antd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin ele dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5777",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/apps/web-ele"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin naive dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5888",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/apps/web-naive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
227
.vscode/settings.json
vendored
Normal file
227
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
{
|
||||||
|
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
|
||||||
|
// workbench
|
||||||
|
"workbench.list.smoothScrolling": true,
|
||||||
|
"workbench.startupEditor": "newUntitledFile",
|
||||||
|
"workbench.tree.indent": 10,
|
||||||
|
"workbench.editor.highlightModifiedTabs": true,
|
||||||
|
"workbench.editor.closeOnFileDelete": true,
|
||||||
|
"workbench.editor.limit.enabled": true,
|
||||||
|
"workbench.editor.limit.perEditorGroup": true,
|
||||||
|
"workbench.editor.limit.value": 5,
|
||||||
|
|
||||||
|
// editor
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.cursorBlinking": "expand",
|
||||||
|
"editor.largeFileOptimizations": false,
|
||||||
|
"editor.accessibilitySupport": "off",
|
||||||
|
"editor.cursorSmoothCaretAnimation": "on",
|
||||||
|
"editor.guides.bracketPairs": "active",
|
||||||
|
"editor.inlineSuggest.enabled": true,
|
||||||
|
"editor.suggestSelection": "recentlyUsedByPrefix",
|
||||||
|
"editor.acceptSuggestionOnEnter": "smart",
|
||||||
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
|
"editor.stickyScroll.enabled": true,
|
||||||
|
"editor.hover.sticky": true,
|
||||||
|
"editor.suggest.insertMode": "replace",
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.autoClosingBrackets": "beforeWhitespace",
|
||||||
|
"editor.autoClosingDelete": "always",
|
||||||
|
"editor.autoClosingOvertype": "always",
|
||||||
|
"editor.autoClosingQuotes": "beforeWhitespace",
|
||||||
|
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.stylelint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "Vue.volar"
|
||||||
|
},
|
||||||
|
// extensions
|
||||||
|
"extensions.ignoreRecommendations": true,
|
||||||
|
|
||||||
|
// terminal
|
||||||
|
"terminal.integrated.cursorBlinking": true,
|
||||||
|
"terminal.integrated.persistentSessionReviveProcess": "never",
|
||||||
|
"terminal.integrated.tabs.enabled": true,
|
||||||
|
"terminal.integrated.scrollback": 10000,
|
||||||
|
"terminal.integrated.stickyScroll.enabled": true,
|
||||||
|
|
||||||
|
// files
|
||||||
|
"files.eol": "\n",
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.simpleDialog.enable": true,
|
||||||
|
"files.associations": {
|
||||||
|
"*.ejs": "html",
|
||||||
|
"*.art": "html",
|
||||||
|
"**/tsconfig.json": "jsonc",
|
||||||
|
"*.json": "jsonc",
|
||||||
|
"package.json": "json"
|
||||||
|
},
|
||||||
|
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.eslintcache": true,
|
||||||
|
"**/bower_components": true,
|
||||||
|
"**/.turbo": true,
|
||||||
|
"**/.idea": true,
|
||||||
|
"**/tmp": true,
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/CVS": true,
|
||||||
|
"**/.stylelintcache": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/vite.config.mts.*": true,
|
||||||
|
"**/tea.yaml": true
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/.git/objects/**": true,
|
||||||
|
"**/.git/subtree-cache/**": true,
|
||||||
|
"**/.vscode/**": true,
|
||||||
|
"**/node_modules/**": true,
|
||||||
|
"**/tmp/**": true,
|
||||||
|
"**/bower_components/**": true,
|
||||||
|
"**/dist/**": true,
|
||||||
|
"**/yarn.lock": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// search
|
||||||
|
"search.searchEditor.singleClickBehaviour": "peekDefinition",
|
||||||
|
"search.followSymlinks": false,
|
||||||
|
// 在使用搜索功能时,将这些文件夹/文件排除在外
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/*.log": true,
|
||||||
|
"**/*.log*": true,
|
||||||
|
"**/bower_components": true,
|
||||||
|
"**/dist": true,
|
||||||
|
"**/elehukouben": true,
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.github": true,
|
||||||
|
"**/.gitignore": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/.vitepress/cache": true,
|
||||||
|
"**/.idea": true,
|
||||||
|
"**/.vscode": false,
|
||||||
|
"**/.yarn": true,
|
||||||
|
"**/tmp": true,
|
||||||
|
"*.xml": true,
|
||||||
|
"out": true,
|
||||||
|
"dist": true,
|
||||||
|
"node_modules": true,
|
||||||
|
"CHANGELOG.md": true,
|
||||||
|
"**/pnpm-lock.yaml": true,
|
||||||
|
"**/yarn.lock": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug.onTaskErrors": "debugAnyway",
|
||||||
|
"diffEditor.ignoreTrimWhitespace": false,
|
||||||
|
"npm.packageManager": "pnpm",
|
||||||
|
|
||||||
|
"css.validate": false,
|
||||||
|
"less.validate": false,
|
||||||
|
"scss.validate": false,
|
||||||
|
|
||||||
|
// extension
|
||||||
|
"emmet.showSuggestionsAsSnippets": true,
|
||||||
|
"emmet.triggerExpansionOnTab": false,
|
||||||
|
|
||||||
|
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
|
||||||
|
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
|
||||||
|
|
||||||
|
"stylelint.enable": true,
|
||||||
|
"stylelint.packageManager": "pnpm",
|
||||||
|
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
|
||||||
|
"stylelint.customSyntax": "postcss-html",
|
||||||
|
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
|
||||||
|
|
||||||
|
"typescript.inlayHints.enumMemberValues.enabled": true,
|
||||||
|
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
||||||
|
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||||
|
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"json5"
|
||||||
|
],
|
||||||
|
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||||
|
],
|
||||||
|
|
||||||
|
"github.copilot.enable": {
|
||||||
|
"*": true,
|
||||||
|
"markdown": true,
|
||||||
|
"plaintext": false,
|
||||||
|
"yaml": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
|
||||||
|
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"packages/locales/src/langs",
|
||||||
|
"playground/src/locales/langs",
|
||||||
|
"apps/*/src/locales/langs"
|
||||||
|
],
|
||||||
|
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}",
|
||||||
|
"i18n-ally.enabledParsers": ["json"],
|
||||||
|
"i18n-ally.sourceLanguage": "en",
|
||||||
|
"i18n-ally.displayLanguage": "zh-CN",
|
||||||
|
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.sortKeys": true,
|
||||||
|
"i18n-ally.namespace": true,
|
||||||
|
|
||||||
|
// 控制相关文件嵌套展示
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.expand": false,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
|
||||||
|
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
|
||||||
|
"*.env": "$(capture).env.*",
|
||||||
|
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||||
|
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||||
|
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
|
||||||
|
"tailwind.config.mjs": "postcss.*"
|
||||||
|
},
|
||||||
|
"commentTranslate.hover.enabled": false,
|
||||||
|
"commentTranslate.multiLineMerge": true,
|
||||||
|
"vue.server.hybridMode": true,
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"oxc.enable": false
|
||||||
|
}
|
||||||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
WORKDIR /vben5
|
||||||
|
|
||||||
|
# copy package.json and pnpm-lock.yaml to workspace
|
||||||
|
COPY . /vben5
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
# 如果你的项目中使用了 antd,需要执行下面的命令
|
||||||
|
RUN pnpm build:antd
|
||||||
|
|
||||||
|
# 如果你的项目中使用了 element-ui,需要执行下面的命令
|
||||||
|
#RUN pnpm build:ele
|
||||||
|
|
||||||
|
# 如果你的项目中使用了 naive,需要执行下面的命令
|
||||||
|
#RUN pnpm build:naive
|
||||||
|
|
||||||
|
RUN echo "Builder Success 🎉"
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine AS production
|
||||||
|
|
||||||
|
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf
|
||||||
|
|
||||||
|
# 如果你的项目中使用了 antd,需要执行下面的命令
|
||||||
|
COPY --from=builder /vben5/apps/web-antd/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 如果你的项目中使用了 element-ui,需要执行下面的命令
|
||||||
|
#COPY --from=builder /vben5/apps/web-ele/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 如果你的项目中使用了 naive,需要执行下面的命令
|
||||||
|
#COPY --from=builder /vben5/apps/web-naive/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
COPY --from=builder /vben5/_nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
BSD 2-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2024, WangJunZzz
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
109
README.md
Normal file
109
README.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/WangJunZzz/abp-vnext-pro">
|
||||||
|
<img src="https://blog-resouce.oss-cn-shenzhen.aliyuncs.com/images/abp/06.jpg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">Abp Vnext Pro Vben5 ...</h1>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🔗 链接
|
||||||
|
|
||||||
|
- [AbpPro Vben2预览](http://182.43.18.151:44318/)
|
||||||
|
- [AbpPro Vben5预览](http://182.43.18.151:44320/)
|
||||||
|
- [代码生成器预览](http://182.43.18.151:44311/)
|
||||||
|
- [文档地址](http://doc.cncore.club/)
|
||||||
|
- [国内文档地址](http://doc.china.cncore.club:81/)
|
||||||
|
- [视频教程](https://www.bilibili.com/video/BV1pt4y1E7aZ)
|
||||||
|
- [代码生成器仓库地址](https://github.com/WangJunZzz/abp-vnext-pro-suite)
|
||||||
|
|
||||||
|
## 📦 快速开始
|
||||||
|
|
||||||
|
- 安装Cli
|
||||||
|
```bash
|
||||||
|
dotnet tool install Lion.AbpPro.Cli -g
|
||||||
|
```
|
||||||
|
|
||||||
|
- 更新Cli
|
||||||
|
```bash
|
||||||
|
dotnet tool update Lion.AbpPro.Cli -g
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三个项目模板
|
||||||
|
- 源码版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lion.abp new -t pro -c 公司名称 -p 项目名称 -v 版本(默认LastRelease) -o 默认当前控制台执行目录
|
||||||
|
```
|
||||||
|
|
||||||
|
- nuget版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lion.abp new -t pro.all -c 公司名称 -p 项目名称 -v 版本(默认LastRelease) -o 默认当前控制台执行目录
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- 模块
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lion.abp new -t pro.module -c 公司名称 -p 项目名称 -v 版本(默认LastRelease) -o 默认当前控制台执行目录
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## ✨ 系统功能
|
||||||
|
|
||||||
|
- [x] 用户管理
|
||||||
|
- [x] 角色管理
|
||||||
|
- [x] 审计日志
|
||||||
|
- [x] 后台任务
|
||||||
|
- [x] 集成事件
|
||||||
|
- [x] SinglaR 消息通知(站内信)
|
||||||
|
- [x] 多语言
|
||||||
|
- [x] 数据字典
|
||||||
|
- [x] 容器化部署
|
||||||
|
- [x] 单元测试
|
||||||
|
- [x] ES 日志
|
||||||
|
- [x] Setting 管理
|
||||||
|
- [x] 多租户
|
||||||
|
- [x] 文件管理
|
||||||
|
|
||||||
|
|
||||||
|
## 🤝 如何贡献
|
||||||
|
|
||||||
|
非常欢迎你的加入!提一个 Issue 或者提交一个 Pull Request。
|
||||||
|
|
||||||
|
**Pull Request:**
|
||||||
|
|
||||||
|
1. Fork 代码!
|
||||||
|
2. 创建自己的分支: `git checkout -b feat/xxxx`
|
||||||
|
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
|
||||||
|
4. 推送您的分支: `git push origin feat/xxxx`
|
||||||
|
5. 提交`pull request`
|
||||||
|
|
||||||
|
## Git 贡献提交规范
|
||||||
|
|
||||||
|
- 参考
|
||||||
|
- `feat` 增加新功能
|
||||||
|
- `fix` 修复问题/BUG
|
||||||
|
- `style` 代码风格相关无影响运行结果的
|
||||||
|
- `perf` 优化/性能提升
|
||||||
|
- `refactor` 重构
|
||||||
|
- `revert` 撤销修改
|
||||||
|
- `test` 测试相关
|
||||||
|
- `docs` 文档/注释
|
||||||
|
- `chore` 依赖更新/脚手架配置修改等
|
||||||
|
- `workflow` 工作流改进
|
||||||
|
- `ci` 持续集成
|
||||||
|
- `types` 类型定义文件更改
|
||||||
|
- `wip` 开发中
|
||||||
|
|
||||||
|
## ✒️交流
|
||||||
|
- QQ 1群:<s>686933575(已满)</s>
|
||||||
|
- QQ 2群:862717726
|
||||||
|
|
||||||
|
## 💖赞助
|
||||||
|
- Star就是对该项目的最大肯定!
|
||||||
|
- 如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||||
|

|
||||||
|
|
||||||
75
_nginx/nginx.conf
Normal file
75
_nginx/nginx.conf
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
#user nobody;
|
||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
#error_log logs/error.log;
|
||||||
|
#error_log logs/error.log notice;
|
||||||
|
#error_log logs/error.log info;
|
||||||
|
|
||||||
|
#pid logs/nginx.pid;
|
||||||
|
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
types {
|
||||||
|
application/javascript js mjs;
|
||||||
|
text/css css;
|
||||||
|
text/html html;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
# tcp_nopush on;
|
||||||
|
|
||||||
|
#keepalive_timeout 0;
|
||||||
|
# keepalive_timeout 65;
|
||||||
|
|
||||||
|
# gzip on;
|
||||||
|
# gzip_buffers 32 16k;
|
||||||
|
# gzip_comp_level 6;
|
||||||
|
# gzip_min_length 1k;
|
||||||
|
# gzip_static on;
|
||||||
|
# gzip_types text/plain
|
||||||
|
# text/css
|
||||||
|
# application/javascript
|
||||||
|
# application/json
|
||||||
|
# application/x-javascript
|
||||||
|
# text/xml
|
||||||
|
# application/xml
|
||||||
|
# application/xml+rss
|
||||||
|
# text/javascript; #设置压缩的文件类型
|
||||||
|
# gzip_vary on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
index index.html;
|
||||||
|
# Enable CORS
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/web-antd/.env
Normal file
5
apps/web-antd/.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=采集端综合管理
|
||||||
|
|
||||||
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
|
VITE_APP_NAMESPACE=abp-vnext-pro-vben5-antd
|
||||||
7
apps/web-antd/.env.analyze
Normal file
7
apps/web-antd/.env.analyze
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# public path
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# Basic interface address SPA
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_VISUALIZER=true
|
||||||
29
apps/web-antd/.env.development
Normal file
29
apps/web-antd/.env.development
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 端口号
|
||||||
|
VITE_PORT=4200
|
||||||
|
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||||
|
VITE_NITRO_MOCK=true
|
||||||
|
|
||||||
|
# vue-router 的模式
|
||||||
|
VITE_ROUTER_HISTORY=history
|
||||||
|
|
||||||
|
|
||||||
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
||||||
|
|
||||||
|
# 是否打开刷新浏览器检查用户角色信息
|
||||||
|
VITE_REFRESH_ROLE = true
|
||||||
|
|
||||||
|
# 后端接口地址
|
||||||
|
VITE_APP_API_ADDRESS=http://localhost:44315/
|
||||||
|
|
||||||
|
# websocket地址
|
||||||
|
VITE_WEBSOCKET_URL=http://localhost:44315/signalr/notification
|
||||||
28
apps/web-antd/.env.production
Normal file
28
apps/web-antd/.env.production
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||||
|
|
||||||
|
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||||
|
VITE_COMPRESS=none
|
||||||
|
|
||||||
|
# 是否开启 PWA
|
||||||
|
VITE_PWA=false
|
||||||
|
|
||||||
|
# vue-router 的模式
|
||||||
|
VITE_ROUTER_HISTORY=history
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
||||||
|
|
||||||
|
# 打包后是否生成dist.zip
|
||||||
|
VITE_ARCHIVER=true
|
||||||
|
|
||||||
|
# 是否打开刷新浏览器检查用户角色信息
|
||||||
|
VITE_REFRESH_ROLE = true
|
||||||
|
|
||||||
|
# 后端接口地址
|
||||||
|
VITE_APP_API_ADDRESS=http://118.190.144.92:9110/
|
||||||
|
|
||||||
|
# websocket地址
|
||||||
|
VITE_WEBSOCKET_URL=http://118.190.144.92:9110/signalr/notification
|
||||||
35
apps/web-antd/index.html
Normal file
35
apps/web-antd/index.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta name="description" content="A Modern Back-end Management System" />
|
||||||
|
<meta name="keywords" content="Vben Admin Vue3 Vite" />
|
||||||
|
<meta name="author" content="Vben" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||||
|
/>
|
||||||
|
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||||
|
<title><%= VITE_APP_TITLE %></title>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
// 生产环境下注入百度统计
|
||||||
|
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||||
|
var _hmt = _hmt || [];
|
||||||
|
(function () {
|
||||||
|
var hm = document.createElement('script');
|
||||||
|
hm.src =
|
||||||
|
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
|
||||||
|
var s = document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
apps/web-antd/package.json
Normal file
61
apps/web-antd/package.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben/web-antd",
|
||||||
|
"version": "5.5.3",
|
||||||
|
"homepage": "https://vben.pro",
|
||||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
|
"directory": "apps/web-antd"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "vben",
|
||||||
|
"email": "ann.vben@gmail.com",
|
||||||
|
"url": "https://github.com/anncwb"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"nswag": "openapi-ts -f ./src/api-client-config/config.ts",
|
||||||
|
"build": "pnpm vite build --mode production",
|
||||||
|
"build:analyze": "pnpm vite build --mode analyze",
|
||||||
|
"dev": "pnpm vite --mode development",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"#/*": "./src/*"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/signalr": "^8.0.7",
|
||||||
|
"@vben/access": "workspace:*",
|
||||||
|
"@vben/common-ui": "workspace:*",
|
||||||
|
"@vben/constants": "workspace:*",
|
||||||
|
"@vben/hooks": "workspace:*",
|
||||||
|
"@vben/icons": "workspace:*",
|
||||||
|
"@vben/layouts": "workspace:*",
|
||||||
|
"@vben/locales": "workspace:*",
|
||||||
|
"@vben/plugins": "workspace:*",
|
||||||
|
"@vben/preferences": "workspace:*",
|
||||||
|
"@vben/request": "workspace:*",
|
||||||
|
"@vben/stores": "workspace:*",
|
||||||
|
"@vben/styles": "workspace:*",
|
||||||
|
"@vben/types": "workspace:*",
|
||||||
|
"@vben/utils": "workspace:*",
|
||||||
|
"@vueuse/core": "catalog:",
|
||||||
|
"ant-design-vue": "catalog:",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"clipboard": "^2.0.11",
|
||||||
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
|
"dayjs": "catalog:",
|
||||||
|
"pinia": "catalog:",
|
||||||
|
"vue": "catalog:",
|
||||||
|
"vue-request": "^2.0.4",
|
||||||
|
"vue-router": "catalog:",
|
||||||
|
"vue3-json-viewer": "^2.2.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@hey-api/client-axios": "^0.2.10",
|
||||||
|
"@hey-api/openapi-ts": "^0.55.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/web-antd/postcss.config.mjs
Normal file
1
apps/web-antd/postcss.config.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config/postcss';
|
||||||
BIN
apps/web-antd/public/favicon.ico
Normal file
BIN
apps/web-antd/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
169
apps/web-antd/src/adapter/component/index.ts
Normal file
169
apps/web-antd/src/adapter/component/index.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||||
|
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Component, SetupContext } from 'vue';
|
||||||
|
|
||||||
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AutoComplete,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
DatePicker,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
InputPassword,
|
||||||
|
Mentions,
|
||||||
|
notification,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
RangePicker,
|
||||||
|
Rate,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Textarea,
|
||||||
|
TimePicker,
|
||||||
|
TreeSelect,
|
||||||
|
Upload,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
const withDefaultPlaceholder = <T extends Component>(
|
||||||
|
component: T,
|
||||||
|
type: 'input' | 'select',
|
||||||
|
) => {
|
||||||
|
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||||
|
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
|
||||||
|
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||||
|
export type ComponentType =
|
||||||
|
| 'ApiSelect'
|
||||||
|
| 'ApiTreeSelect'
|
||||||
|
| 'AutoComplete'
|
||||||
|
| 'Checkbox'
|
||||||
|
| 'CheckboxGroup'
|
||||||
|
| 'DatePicker'
|
||||||
|
| 'DefaultButton'
|
||||||
|
| 'Divider'
|
||||||
|
| 'IconPicker'
|
||||||
|
| 'Input'
|
||||||
|
| 'InputNumber'
|
||||||
|
| 'InputPassword'
|
||||||
|
| 'Mentions'
|
||||||
|
| 'PrimaryButton'
|
||||||
|
| 'Radio'
|
||||||
|
| 'RadioGroup'
|
||||||
|
| 'RangePicker'
|
||||||
|
| 'Rate'
|
||||||
|
| 'Select'
|
||||||
|
| 'Space'
|
||||||
|
| 'Switch'
|
||||||
|
| 'Textarea'
|
||||||
|
| 'TimePicker'
|
||||||
|
| 'TreeSelect'
|
||||||
|
| 'Upload'
|
||||||
|
| BaseFormComponentType;
|
||||||
|
|
||||||
|
async function initComponentAdapter() {
|
||||||
|
const components: Partial<Record<ComponentType, Component>> = {
|
||||||
|
// 如果你的组件体积比较大,可以使用异步加载
|
||||||
|
// Button: () =>
|
||||||
|
// import('xxx').then((res) => res.Button),
|
||||||
|
ApiSelect: (props, { attrs, slots }) => {
|
||||||
|
return h(
|
||||||
|
ApiComponent,
|
||||||
|
{
|
||||||
|
placeholder: $t('ui.placeholder.select'),
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
component: Select,
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
visibleEvent: 'onDropdownVisibleChange',
|
||||||
|
modelPropName: 'value',
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||||
|
return h(
|
||||||
|
ApiComponent,
|
||||||
|
{
|
||||||
|
placeholder: $t('ui.placeholder.select'),
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
component: TreeSelect,
|
||||||
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
modelPropName: 'value',
|
||||||
|
optionsPropName: 'treeData',
|
||||||
|
visibleEvent: 'onVisibleChange',
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
AutoComplete,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
DatePicker,
|
||||||
|
// 自定义默认按钮
|
||||||
|
DefaultButton: (props, { attrs, slots }) => {
|
||||||
|
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||||
|
},
|
||||||
|
Divider,
|
||||||
|
IconPicker: (props, { attrs, slots }) => {
|
||||||
|
return h(
|
||||||
|
IconPicker,
|
||||||
|
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Input: withDefaultPlaceholder(Input, 'input'),
|
||||||
|
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||||
|
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||||
|
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||||
|
// 自定义主要按钮
|
||||||
|
PrimaryButton: (props, { attrs, slots }) => {
|
||||||
|
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||||
|
},
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
RangePicker,
|
||||||
|
Rate,
|
||||||
|
Select: withDefaultPlaceholder(Select, 'select'),
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||||
|
TimePicker,
|
||||||
|
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||||
|
Upload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将组件注册到全局共享状态中
|
||||||
|
globalShareState.setComponents(components);
|
||||||
|
|
||||||
|
// 定义全局共享状态中的消息提示
|
||||||
|
globalShareState.defineMessage({
|
||||||
|
// 复制成功消息提示
|
||||||
|
copyPreferencesSuccess: (title, content) => {
|
||||||
|
notification.success({
|
||||||
|
description: content,
|
||||||
|
message: title,
|
||||||
|
placement: 'bottomRight',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initComponentAdapter };
|
||||||
47
apps/web-antd/src/adapter/form.ts
Normal file
47
apps/web-antd/src/adapter/form.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type {
|
||||||
|
VbenFormSchema as FormSchema,
|
||||||
|
VbenFormProps,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
|
||||||
|
import type { ComponentType } from './component';
|
||||||
|
|
||||||
|
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
setupVbenForm<ComponentType>({
|
||||||
|
config: {
|
||||||
|
// ant design vue组件库默认都是 v-model:value
|
||||||
|
baseModelPropName: 'value',
|
||||||
|
|
||||||
|
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||||
|
modelPropNameMap: {
|
||||||
|
Checkbox: 'checked',
|
||||||
|
Radio: 'checked',
|
||||||
|
Switch: 'checked',
|
||||||
|
Upload: 'fileList',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defineRules: {
|
||||||
|
// 输入项目必填国际化适配
|
||||||
|
required: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null || value.length === 0) {
|
||||||
|
return $t('ui.formRules.required', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// 选择项目必填国际化适配
|
||||||
|
selectRequired: (value, _params, ctx) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const useVbenForm = useForm<ComponentType>;
|
||||||
|
|
||||||
|
export { useVbenForm, z };
|
||||||
|
|
||||||
|
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||||
|
export type { VbenFormProps };
|
||||||
67
apps/web-antd/src/adapter/vxe-table.ts
Normal file
67
apps/web-antd/src/adapter/vxe-table.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
|
import { Button, Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from './form';
|
||||||
|
|
||||||
|
setupVbenVxeTable({
|
||||||
|
configVxeTable: (vxeUI) => {
|
||||||
|
vxeUI.setConfig({
|
||||||
|
grid: {
|
||||||
|
align: 'center',
|
||||||
|
border: true,
|
||||||
|
columnConfig: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
minHeight: 180,
|
||||||
|
formConfig: {
|
||||||
|
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
autoLoad: true,
|
||||||
|
response: {
|
||||||
|
result: 'items',
|
||||||
|
total: 'totalCount',
|
||||||
|
list: 'items',
|
||||||
|
},
|
||||||
|
showActiveMsg: true,
|
||||||
|
showResponseMsg: false,
|
||||||
|
},
|
||||||
|
round: true,
|
||||||
|
showOverflow: true,
|
||||||
|
size: 'small', // medium / small / mini
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
|
vxeUI.renderer.add('CellImage', {
|
||||||
|
renderTableDefault(_renderOpts, params) {
|
||||||
|
const { column, row } = params;
|
||||||
|
return h(Image, { src: row[column.field] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||||
|
vxeUI.renderer.add('CellLink', {
|
||||||
|
renderTableDefault(renderOpts) {
|
||||||
|
const { props } = renderOpts;
|
||||||
|
return h(
|
||||||
|
Button,
|
||||||
|
{ size: 'small', type: 'link' },
|
||||||
|
{ default: () => props?.text },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||||
|
// vxeUI.formats.add
|
||||||
|
},
|
||||||
|
useVbenForm,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { useVbenVxeGrid };
|
||||||
|
|
||||||
|
export type * from '@vben/plugins/vxe-table';
|
||||||
7
apps/web-antd/src/api-client-config/config.ts
Normal file
7
apps/web-antd/src/api-client-config/config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from '@hey-api/openapi-ts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
client: '@hey-api/client-axios',
|
||||||
|
input: 'http://localhost:44315/swagger/IOT/swagger.json',
|
||||||
|
output: 'src/api-client',
|
||||||
|
});
|
||||||
102
apps/web-antd/src/api-client-config/index-blob.ts
Normal file
102
apps/web-antd/src/api-client-config/index-blob.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message as Message } from 'ant-design-vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { antdLocale } from '#/locales/index';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: import.meta.env.DEV
|
||||||
|
? '/proxy/'
|
||||||
|
: import.meta.env.VITE_APP_API_ADDRESS,
|
||||||
|
timeout: 1000 * 60,
|
||||||
|
responseType: 'blob', // 设置响应数据类型为blob
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.request.use((request) => {
|
||||||
|
// 全局拦截请求发送前提交的参数
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const token = userStore.userInfo?.token;
|
||||||
|
// 设置请求头
|
||||||
|
if (request.headers) {
|
||||||
|
request.headers.__tenant = userStore.tenant?.tenantId;
|
||||||
|
request.headers['accept-language'] = antdLocale.value.locale;
|
||||||
|
}
|
||||||
|
// 如果token过期,则跳转到登录页面
|
||||||
|
if (token && userStore.checkUserLoginExpire()) {
|
||||||
|
authStore.logout();
|
||||||
|
|
||||||
|
return Promise.reject($t('common.mesage401'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
if (request.headers) {
|
||||||
|
request.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
/**
|
||||||
|
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||||
|
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||||
|
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||||
|
* 请求出错时 error 会返回错误信息
|
||||||
|
*/
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
if (response.data.error !== '') {
|
||||||
|
// 错误提示
|
||||||
|
Message.error(response.data.error);
|
||||||
|
return Promise.reject(response.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// useUserStore().logout()
|
||||||
|
}
|
||||||
|
return Promise.resolve(response);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
let message = error.message;
|
||||||
|
if (message === 'Network Error') {
|
||||||
|
message = $t('common.mesage500');
|
||||||
|
} else if (message.includes('timeout')) {
|
||||||
|
message = $t('common.timeOut');
|
||||||
|
} else
|
||||||
|
switch (error.status) {
|
||||||
|
case 400: {
|
||||||
|
message = error.response.data.error?.validationErrors[0].message;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 401: {
|
||||||
|
message = $t('common.mesage401');
|
||||||
|
// useUserStore().logout()
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 403: {
|
||||||
|
message = $t('common.mesage403');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 500: {
|
||||||
|
message = error.response.data.error?.message;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (message.includes('Request failed with status code')) {
|
||||||
|
message = $t('common.mesage500');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message.error(message);
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
114
apps/web-antd/src/api-client-config/index.ts
Normal file
114
apps/web-antd/src/api-client-config/index.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message as Message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { antdLocale } from '#/locales/index';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import { client } from '../api-client/services.gen';
|
||||||
|
|
||||||
|
client.setConfig({
|
||||||
|
baseURL: import.meta.env.DEV
|
||||||
|
? '/proxy/'
|
||||||
|
: import.meta.env.VITE_APP_API_ADDRESS,
|
||||||
|
timeout: 1000 * 60,
|
||||||
|
responseType: 'json',
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.instance.interceptors.request.use((request) => {
|
||||||
|
// 全局拦截请求发送前提交的参数
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const token = userStore.userInfo?.token;
|
||||||
|
// 设置请求头
|
||||||
|
if (request.headers) {
|
||||||
|
request.headers.__tenant = userStore.tenant?.tenantId;
|
||||||
|
// todo vben5 没有提供统一获取当前语言的方式
|
||||||
|
request.headers['accept-language'] = antdLocale.value.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果token过期,则跳转到登录页面
|
||||||
|
if (
|
||||||
|
request.url !== undefined &&
|
||||||
|
request.url.includes('/api/app/account/login')
|
||||||
|
) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token && userStore.checkUserLoginExpire()) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
authStore.logout();
|
||||||
|
Message.warn($t('common.mesage401'));
|
||||||
|
|
||||||
|
return Promise.reject($t('common.mesage401'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
if (request.headers) {
|
||||||
|
request.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.instance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
/**
|
||||||
|
* 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
|
||||||
|
* 假设返回数据格式为:{ status: 1, error: '', data: '' }
|
||||||
|
* 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录
|
||||||
|
* 请求出错时 error 会返回错误信息
|
||||||
|
*/
|
||||||
|
if (response.data.status === 1) {
|
||||||
|
if (response.data.error !== '') {
|
||||||
|
// 错误提示
|
||||||
|
Message.error(response.data.error);
|
||||||
|
return Promise.reject(response.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// useUserStore().logout()
|
||||||
|
}
|
||||||
|
return Promise.resolve(response);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
let message = error.message;
|
||||||
|
if (message === 'Network Error') {
|
||||||
|
message = $t('common.mesage500');
|
||||||
|
} else if (message.includes('timeout')) {
|
||||||
|
message = $t('common.timeOut');
|
||||||
|
} else
|
||||||
|
switch (error.status) {
|
||||||
|
case 400: {
|
||||||
|
message = error.response.data.error?.validationErrors[0].message;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 401: {
|
||||||
|
message = $t('common.mesage401');
|
||||||
|
// useUserStore().logout()
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 403: {
|
||||||
|
message = $t('common.mesage403');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 500: {
|
||||||
|
message = error.response.data.error?.message;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (message.includes('Request failed with status code')) {
|
||||||
|
message = $t('common.mesage500');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message.error(message);
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default client;
|
||||||
4
apps/web-antd/src/api-client/index.ts
Normal file
4
apps/web-antd/src/api-client/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
export * from './schemas.gen';
|
||||||
|
export * from './services.gen';
|
||||||
|
export * from './types.gen';
|
||||||
4927
apps/web-antd/src/api-client/schemas.gen.ts
Normal file
4927
apps/web-antd/src/api-client/schemas.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
777
apps/web-antd/src/api-client/services.gen.ts
Normal file
777
apps/web-antd/src/api-client/services.gen.ts
Normal file
File diff suppressed because one or more lines are too long
2080
apps/web-antd/src/api-client/types.gen.ts
Normal file
2080
apps/web-antd/src/api-client/types.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
51
apps/web-antd/src/api/core/auth.ts
Normal file
51
apps/web-antd/src/api/core/auth.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { baseRequestClient, requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace AuthApi {
|
||||||
|
/** 登录接口参数 */
|
||||||
|
export interface LoginParams {
|
||||||
|
password?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录接口返回值 */
|
||||||
|
export interface LoginResult {
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenResult {
|
||||||
|
data: string;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
export async function loginApi(data: AuthApi.LoginParams) {
|
||||||
|
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新accessToken
|
||||||
|
*/
|
||||||
|
export async function refreshTokenApi() {
|
||||||
|
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
export async function logoutApi() {
|
||||||
|
return baseRequestClient.post('/auth/logout', {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户权限码
|
||||||
|
*/
|
||||||
|
export async function getAccessCodesApi() {
|
||||||
|
return requestClient.get<string[]>('/auth/codes');
|
||||||
|
}
|
||||||
3
apps/web-antd/src/api/core/index.ts
Normal file
3
apps/web-antd/src/api/core/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './menu';
|
||||||
|
export * from './user';
|
||||||
10
apps/web-antd/src/api/core/menu.ts
Normal file
10
apps/web-antd/src/api/core/menu.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { RouteRecordStringComponent } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户所有菜单
|
||||||
|
*/
|
||||||
|
export async function getAllMenusApi() {
|
||||||
|
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||||
|
}
|
||||||
10
apps/web-antd/src/api/core/user.ts
Normal file
10
apps/web-antd/src/api/core/user.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
export async function getUserInfoApi() {
|
||||||
|
return requestClient.get<UserInfo>('/user/info');
|
||||||
|
}
|
||||||
1
apps/web-antd/src/api/index.ts
Normal file
1
apps/web-antd/src/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './core';
|
||||||
113
apps/web-antd/src/api/request.ts
Normal file
113
apps/web-antd/src/api/request.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* 该文件可自行根据业务逻辑进行调整
|
||||||
|
*/
|
||||||
|
import type { RequestClientOptions } from '@vben/request';
|
||||||
|
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import {
|
||||||
|
authenticateResponseInterceptor,
|
||||||
|
defaultResponseInterceptor,
|
||||||
|
errorMessageResponseInterceptor,
|
||||||
|
RequestClient,
|
||||||
|
} from '@vben/request';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import { refreshTokenApi } from './core';
|
||||||
|
|
||||||
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
|
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||||
|
const client = new RequestClient({
|
||||||
|
...options,
|
||||||
|
baseURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新认证逻辑
|
||||||
|
*/
|
||||||
|
async function doReAuthenticate() {
|
||||||
|
console.warn('Access token or refresh token is invalid or expired. ');
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
accessStore.setAccessToken(null);
|
||||||
|
if (
|
||||||
|
preferences.app.loginExpiredMode === 'modal' &&
|
||||||
|
accessStore.isAccessChecked
|
||||||
|
) {
|
||||||
|
accessStore.setLoginExpired(true);
|
||||||
|
} else {
|
||||||
|
await authStore.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新token逻辑
|
||||||
|
*/
|
||||||
|
async function doRefreshToken() {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const resp = await refreshTokenApi();
|
||||||
|
const newToken = resp.data;
|
||||||
|
accessStore.setAccessToken(newToken);
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatToken(token: null | string) {
|
||||||
|
return token ? `Bearer ${token}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求头处理
|
||||||
|
client.addRequestInterceptor({
|
||||||
|
fulfilled: async (config) => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
|
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||||||
|
config.headers['Accept-Language'] = preferences.app.locale;
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理返回的响应数据格式
|
||||||
|
client.addResponseInterceptor(
|
||||||
|
defaultResponseInterceptor({
|
||||||
|
codeField: 'code',
|
||||||
|
dataField: 'data',
|
||||||
|
successCode: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// token过期的处理
|
||||||
|
client.addResponseInterceptor(
|
||||||
|
authenticateResponseInterceptor({
|
||||||
|
client,
|
||||||
|
doReAuthenticate,
|
||||||
|
doRefreshToken,
|
||||||
|
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||||
|
formatToken,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||||
|
client.addResponseInterceptor(
|
||||||
|
errorMessageResponseInterceptor((msg: string, error) => {
|
||||||
|
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||||
|
// 当前mock接口返回的错误字段是 error 或者 message
|
||||||
|
const responseData = error?.response?.data ?? {};
|
||||||
|
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
||||||
|
// 如果没有错误信息,则会根据状态码进行提示
|
||||||
|
message.error(errorMessage || msg);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestClient = createRequestClient(apiURL, {
|
||||||
|
responseReturn: 'data',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||||
39
apps/web-antd/src/app.vue
Normal file
39
apps/web-antd/src/app.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useAntdDesignTokens } from '@vben/hooks';
|
||||||
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { App, ConfigProvider, theme } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { antdLocale } from '#/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'App' });
|
||||||
|
|
||||||
|
const { isDark } = usePreferences();
|
||||||
|
const { tokens } = useAntdDesignTokens();
|
||||||
|
|
||||||
|
const tokenTheme = computed(() => {
|
||||||
|
const algorithm = isDark.value
|
||||||
|
? [theme.darkAlgorithm]
|
||||||
|
: [theme.defaultAlgorithm];
|
||||||
|
|
||||||
|
// antd 紧凑模式算法
|
||||||
|
if (preferences.app.compact) {
|
||||||
|
algorithm.push(theme.compactAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
algorithm,
|
||||||
|
token: tokens,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
||||||
|
<App>
|
||||||
|
<RouterView />
|
||||||
|
</App>
|
||||||
|
</ConfigProvider>
|
||||||
|
</template>
|
||||||
68
apps/web-antd/src/bootstrap.ts
Normal file
68
apps/web-antd/src/bootstrap.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { createApp, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
import { registerAccessDirective } from '@vben/access';
|
||||||
|
import { initTippy } from '@vben/common-ui';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { initStores } from '@vben/stores';
|
||||||
|
import '@vben/styles';
|
||||||
|
import '@vben/styles/antd';
|
||||||
|
|
||||||
|
import { useTitle } from '@vueuse/core';
|
||||||
|
// https://github.com/rennzhang/codemirror-editor-vue3
|
||||||
|
import { InstallCodeMirror } from 'codemirror-editor-vue3';
|
||||||
|
import JsonViewer from 'vue3-json-viewer';
|
||||||
|
|
||||||
|
import { $t, setupI18n } from '#/locales';
|
||||||
|
|
||||||
|
import { initComponentAdapter } from './adapter/component';
|
||||||
|
import App from './app.vue';
|
||||||
|
import { router } from './router';
|
||||||
|
|
||||||
|
import 'vue3-json-viewer/dist/index.css';
|
||||||
|
|
||||||
|
async function bootstrap(namespace: string) {
|
||||||
|
// 初始化组件适配器
|
||||||
|
await initComponentAdapter();
|
||||||
|
|
||||||
|
// // 设置弹窗的默认配置
|
||||||
|
// setDefaultModalProps({
|
||||||
|
// fullscreenButton: false,
|
||||||
|
// });
|
||||||
|
// // 设置抽屉的默认配置
|
||||||
|
// setDefaultDrawerProps({
|
||||||
|
// zIndex: 1020,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
// 国际化 i18n 配置
|
||||||
|
await setupI18n(app);
|
||||||
|
|
||||||
|
// 配置 pinia-tore
|
||||||
|
await initStores(app, { namespace });
|
||||||
|
|
||||||
|
// 安装权限指令
|
||||||
|
registerAccessDirective(app);
|
||||||
|
|
||||||
|
// 初始化 tippy
|
||||||
|
initTippy(app);
|
||||||
|
|
||||||
|
// 配置路由及路由守卫
|
||||||
|
app.use(router);
|
||||||
|
// 配置 json-viewer
|
||||||
|
app.use(JsonViewer);
|
||||||
|
|
||||||
|
// 动态更新标题
|
||||||
|
watchEffect(() => {
|
||||||
|
if (preferences.app.dynamicTitle) {
|
||||||
|
const routeTitle = router.currentRoute.value.meta?.title;
|
||||||
|
const pageTitle =
|
||||||
|
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||||
|
useTitle(pageTitle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.use(InstallCodeMirror);
|
||||||
|
app.mount('#app');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { bootstrap };
|
||||||
4
apps/web-antd/src/components/Loading/index.ts
Normal file
4
apps/web-antd/src/components/Loading/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { createLoading } from './src/createLoading';
|
||||||
|
export { default as Loading } from './src/Loading.vue';
|
||||||
|
|
||||||
|
export { useLoading } from './src/useLoading';
|
||||||
78
apps/web-antd/src/components/Loading/src/Loading.vue
Normal file
78
apps/web-antd/src/components/Loading/src/Loading.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { SizeEnum } from './typing';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Loading' });
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
tip: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String as PropType<SizeEnum>,
|
||||||
|
default: SizeEnum.LARGE,
|
||||||
|
validator: (v: SizeEnum): boolean => {
|
||||||
|
return [SizeEnum.DEFAULT, SizeEnum.LARGE, SizeEnum.SMALL].includes(v);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
absolute: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String as PropType<'dark' | 'light'>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
v-show="loading"
|
||||||
|
:class="{ absolute, [`${theme}`]: !!theme }"
|
||||||
|
:style="[background ? `background-color: ${background}` : '']"
|
||||||
|
class="full-loading"
|
||||||
|
>
|
||||||
|
<Spin v-bind="$attrs" :size="size" :spinning="loading" :tip="tip" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.full-loading {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f0f2f566;
|
||||||
|
|
||||||
|
&.absolute {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 300;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
.full-loading:not(.light) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-loading.dark {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
apps/web-antd/src/components/Loading/src/createLoading.ts
Normal file
84
apps/web-antd/src/components/Loading/src/createLoading.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { LoadingProps } from './typing';
|
||||||
|
|
||||||
|
import { createVNode, defineComponent, h, reactive, render } from 'vue';
|
||||||
|
|
||||||
|
import Loading from './Loading.vue';
|
||||||
|
|
||||||
|
// 创建一个加载组件的函数
|
||||||
|
export function createLoading(
|
||||||
|
props?: Partial<LoadingProps>,
|
||||||
|
target?: HTMLElement,
|
||||||
|
wait = false,
|
||||||
|
) {
|
||||||
|
let vm: any = null;
|
||||||
|
const data = reactive({
|
||||||
|
tip: '',
|
||||||
|
loading: true,
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义加载组件的包装
|
||||||
|
const LoadingWrap = defineComponent({
|
||||||
|
render() {
|
||||||
|
return h(Loading, { ...data });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
vm = createVNode(LoadingWrap);
|
||||||
|
|
||||||
|
let container: any = null;
|
||||||
|
// 根据wait参数决定何时渲染Loading组件
|
||||||
|
if (wait) {
|
||||||
|
setTimeout(() => {
|
||||||
|
render(vm, (container = document.createElement('div')));
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
render(vm, (container = document.createElement('div')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭加载组件的函数
|
||||||
|
function close() {
|
||||||
|
if (vm?.el && vm.el.parentNode) {
|
||||||
|
vm.el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开加载组件并将其附加到目标元素上的函数
|
||||||
|
function open(target: HTMLElement = document.body) {
|
||||||
|
if (!vm || !vm.el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.append(vm.el as HTMLElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁加载组件的函数
|
||||||
|
function destroy() {
|
||||||
|
container && render(null, container);
|
||||||
|
container = vm = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了目标元素,则打开加载组件
|
||||||
|
if (target) {
|
||||||
|
open(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回加载组件的实例及控制方法
|
||||||
|
return {
|
||||||
|
vm,
|
||||||
|
close,
|
||||||
|
open,
|
||||||
|
destroy,
|
||||||
|
setTip: (tip: string) => {
|
||||||
|
data.tip = tip;
|
||||||
|
},
|
||||||
|
setLoading: (loading: boolean) => {
|
||||||
|
data.loading = loading;
|
||||||
|
},
|
||||||
|
get loading() {
|
||||||
|
return data.loading;
|
||||||
|
},
|
||||||
|
get $el() {
|
||||||
|
return vm?.el as HTMLElement;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
14
apps/web-antd/src/components/Loading/src/typing.ts
Normal file
14
apps/web-antd/src/components/Loading/src/typing.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export enum SizeEnum {
|
||||||
|
DEFAULT = 'default',
|
||||||
|
LARGE = 'large',
|
||||||
|
SMALL = 'small',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadingProps {
|
||||||
|
tip: string;
|
||||||
|
size: SizeEnum;
|
||||||
|
absolute: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
background: string;
|
||||||
|
theme: 'dark' | 'light';
|
||||||
|
}
|
||||||
61
apps/web-antd/src/components/Loading/src/useLoading.ts
Normal file
61
apps/web-antd/src/components/Loading/src/useLoading.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import type { LoadingProps } from './typing';
|
||||||
|
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { unref } from 'vue';
|
||||||
|
|
||||||
|
import { tryOnUnmounted } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { createLoading } from './createLoading';
|
||||||
|
|
||||||
|
export interface UseLoadingOptions {
|
||||||
|
target?: any;
|
||||||
|
props?: Partial<LoadingProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Fn {
|
||||||
|
(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLoading(
|
||||||
|
props: Partial<LoadingProps>,
|
||||||
|
): [Fn, Fn, (arg0: string) => void];
|
||||||
|
export function useLoading(
|
||||||
|
opt: Partial<UseLoadingOptions>,
|
||||||
|
): [Fn, Fn, (arg0: string) => void];
|
||||||
|
|
||||||
|
export function useLoading(
|
||||||
|
opt: Partial<LoadingProps> | Partial<UseLoadingOptions>,
|
||||||
|
): [Fn, Fn, (arg0: string) => void] {
|
||||||
|
let props: Partial<LoadingProps>;
|
||||||
|
let target: HTMLElement | Ref<any> = document.body;
|
||||||
|
|
||||||
|
if (Reflect.has(opt, 'target') || Reflect.has(opt, 'props')) {
|
||||||
|
const options = opt as Partial<UseLoadingOptions>;
|
||||||
|
props = options.props || {};
|
||||||
|
target = options.target || document.body;
|
||||||
|
} else {
|
||||||
|
props = opt as Partial<LoadingProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = createLoading(props, undefined, false);
|
||||||
|
|
||||||
|
const open = (): void => {
|
||||||
|
const t = unref(target as Ref<any>);
|
||||||
|
if (!t) return;
|
||||||
|
instance.open(t);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = (): void => {
|
||||||
|
instance.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTip = (tip: string) => {
|
||||||
|
instance.setTip(tip);
|
||||||
|
};
|
||||||
|
|
||||||
|
tryOnUnmounted(() => {
|
||||||
|
instance.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
return [open, close, setTip];
|
||||||
|
}
|
||||||
51
apps/web-antd/src/components/icon/icon.vue
Normal file
51
apps/web-antd/src/components/icon/icon.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '16px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const iconComp = computed(() => {
|
||||||
|
if (props.icon.startsWith('http')) {
|
||||||
|
return () => h('img', { src: props.icon, class: 'm-icon__' });
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
const styles = computed(() => {
|
||||||
|
return {
|
||||||
|
fontSize: props.size.toString().endsWith('px')
|
||||||
|
? props.size
|
||||||
|
: `${props.size}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="iconComp" v-if="iconComp" :style="styles" />
|
||||||
|
<VbenIcon v-else :icon="props.icon" :style="styles" class="m-icon__" />
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-icon__ {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: none;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
apps/web-antd/src/components/icon/index.ts
Normal file
1
apps/web-antd/src/components/icon/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Icon } from './icon.vue';
|
||||||
2
apps/web-antd/src/components/table-action/index.ts
Normal file
2
apps/web-antd/src/components/table-action/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as TableAction } from './table-action.vue';
|
||||||
|
export type * from './types.d.ts';
|
||||||
225
apps/web-antd/src/components/table-action/table-action.vue
Normal file
225
apps/web-antd/src/components/table-action/table-action.vue
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ButtonType } from 'ant-design-vue/es/button';
|
||||||
|
|
||||||
|
import type { ActionItem, PopConfirm } from './types';
|
||||||
|
|
||||||
|
import { computed, type PropType, toRaw } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { isBoolean, isFunction } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Dropdown, Menu, Popconfirm, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dropDownActions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MenuItem = Menu.Item;
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
function isIfShow(action: ActionItem): boolean {
|
||||||
|
const ifShow = action.ifShow;
|
||||||
|
|
||||||
|
let isIfShow = true;
|
||||||
|
|
||||||
|
if (isBoolean(ifShow)) {
|
||||||
|
isIfShow = ifShow;
|
||||||
|
}
|
||||||
|
if (isFunction(ifShow)) {
|
||||||
|
isIfShow = ifShow(action);
|
||||||
|
}
|
||||||
|
return isIfShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActions = computed(() => {
|
||||||
|
return (toRaw(props.actions) || [])
|
||||||
|
.filter((action) => {
|
||||||
|
return (
|
||||||
|
(hasAccessByCodes(action.auth || []) ||
|
||||||
|
(action.auth || []).length === 0) &&
|
||||||
|
isIfShow(action)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((action) => {
|
||||||
|
const { popConfirm } = action;
|
||||||
|
return {
|
||||||
|
// getPopupContainer: document.body,
|
||||||
|
type: 'link' as ButtonType,
|
||||||
|
...action,
|
||||||
|
...popConfirm,
|
||||||
|
onConfirm: popConfirm?.confirm,
|
||||||
|
onCancel: popConfirm?.cancel,
|
||||||
|
enable: !!popConfirm,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const getDropdownList = computed((): any[] => {
|
||||||
|
return (toRaw(props.dropDownActions) || [])
|
||||||
|
.filter((action) => {
|
||||||
|
return (
|
||||||
|
(hasAccessByCodes(action.auth || []) ||
|
||||||
|
(action.auth || []).length === 0) &&
|
||||||
|
isIfShow(action)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((action, index) => {
|
||||||
|
const { label, popConfirm } = action;
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
...popConfirm,
|
||||||
|
onConfirm: popConfirm?.confirm,
|
||||||
|
onCancel: popConfirm?.cancel,
|
||||||
|
text: label,
|
||||||
|
divider:
|
||||||
|
index < props.dropDownActions.length - 1 ? props.divider : false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const getPopConfirmProps = (attrs: PopConfirm) => {
|
||||||
|
const originAttrs: any = attrs;
|
||||||
|
delete originAttrs.icon;
|
||||||
|
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||||
|
originAttrs.onConfirm = attrs.confirm;
|
||||||
|
delete originAttrs.confirm;
|
||||||
|
}
|
||||||
|
if (attrs.cancel && isFunction(attrs.cancel)) {
|
||||||
|
originAttrs.onCancel = attrs.cancel;
|
||||||
|
delete originAttrs.cancel;
|
||||||
|
}
|
||||||
|
return originAttrs;
|
||||||
|
};
|
||||||
|
const getButtonProps = (action: ActionItem) => {
|
||||||
|
const res = {
|
||||||
|
type: action.type || 'primary',
|
||||||
|
...action,
|
||||||
|
};
|
||||||
|
delete res.icon;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
const handleMenuClick = (e: any) => {
|
||||||
|
const action = getDropdownList.value[e.key];
|
||||||
|
if (action.onClick && isFunction(action.onClick)) {
|
||||||
|
action.onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="m-table-action">
|
||||||
|
<Space
|
||||||
|
:size="
|
||||||
|
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-for="(action, index) in getActions" :key="index">
|
||||||
|
<Popconfirm
|
||||||
|
v-if="action.popConfirm"
|
||||||
|
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||||
|
>
|
||||||
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
|
<Icon :icon="action.popConfirm.icon" />
|
||||||
|
</template>
|
||||||
|
<Button v-bind="getButtonProps(action)">
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<Icon :icon="action.icon" />
|
||||||
|
</template>
|
||||||
|
{{ action.label }}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
<Button v-else v-bind="getButtonProps(action)" @click="action.onClick">
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<Icon :icon="action.icon" />
|
||||||
|
</template>
|
||||||
|
{{ action.label }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||||
|
<slot name="more">
|
||||||
|
<Button size="small" type="link">
|
||||||
|
<template #icon>
|
||||||
|
<Icon class="icon-more" icon="ant-design:more-outlined" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</slot>
|
||||||
|
<template #overlay>
|
||||||
|
<Menu @click="handleMenuClick">
|
||||||
|
<MenuItem v-for="(action, index) in getDropdownList" :key="index">
|
||||||
|
<template v-if="action.popConfirm">
|
||||||
|
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||||
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
|
<Icon :icon="action.popConfirm.icon" />
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
action.disabled === true
|
||||||
|
? 'cursor-not-allowed text-gray-300'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon v-if="action.icon" :icon="action.icon" />
|
||||||
|
<span class="ml-1">{{ action.text }}</span>
|
||||||
|
</div>
|
||||||
|
</Popconfirm>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
action.disabled === true
|
||||||
|
? 'cursor-not-allowed text-gray-300'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon v-if="action.icon" :icon="action.icon" />
|
||||||
|
{{ action.label }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less">
|
||||||
|
/** 修复 iconify 位置问题 **/
|
||||||
|
.m-table-action {
|
||||||
|
.ant-btn > .iconify + span,
|
||||||
|
.ant-btn > span + .iconify {
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn > .iconify {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: none;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||||
|
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
|
||||||
|
|
||||||
|
export interface PopConfirm {
|
||||||
|
title: string;
|
||||||
|
okText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
confirm: Fn;
|
||||||
|
cancel?: Fn;
|
||||||
|
icon?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
export interface ActionItem extends ButtonProps {
|
||||||
|
onClick?: Fn;
|
||||||
|
label?: string;
|
||||||
|
color?: 'error' | 'success' | 'warning';
|
||||||
|
icon?: string;
|
||||||
|
popConfirm?: PopConfirm;
|
||||||
|
disabled?: boolean;
|
||||||
|
divider?: boolean;
|
||||||
|
// 权限编码控制是否显示
|
||||||
|
auth?: string[];
|
||||||
|
// 业务控制是否显示
|
||||||
|
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||||
|
tooltip?: string | TooltipProps;
|
||||||
|
}
|
||||||
125
apps/web-antd/src/hooks/useSignalR.ts
Normal file
125
apps/web-antd/src/hooks/useSignalR.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { useEventbus } from '@vben/hooks';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import * as signalR from '@microsoft/signalr';
|
||||||
|
import { notification } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const eventbus = useEventbus();
|
||||||
|
let connection: signalR.HubConnection;
|
||||||
|
export function useSignalR() {
|
||||||
|
/**
|
||||||
|
* 开始连接SignalR
|
||||||
|
*/
|
||||||
|
async function startConnect() {
|
||||||
|
try {
|
||||||
|
connectionsignalR();
|
||||||
|
await connection.start();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setTimeout(() => startConnect(), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭SignalR连接
|
||||||
|
*/
|
||||||
|
function closeConnect(): void {
|
||||||
|
8;
|
||||||
|
if (connection) {
|
||||||
|
connection.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectionsignalR() {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const token = userStore.userInfo?.token;
|
||||||
|
connection = new signalR.HubConnectionBuilder()
|
||||||
|
.withUrl(import.meta.env.VITE_WEBSOCKET_URL, {
|
||||||
|
accessTokenFactory: () => token,
|
||||||
|
skipNegotiation: true,
|
||||||
|
transport: signalR.HttpTransportType.WebSockets,
|
||||||
|
})
|
||||||
|
.withAutomaticReconnect({
|
||||||
|
nextRetryDelayInMilliseconds: (retryContext) => {
|
||||||
|
// 重连规则:重连次数<300:间隔1s;重试次数<3000:间隔3s;重试次数>3000:间隔30s
|
||||||
|
const count = retryContext.previousRetryCount / 300;
|
||||||
|
if (count < 1) {
|
||||||
|
// 重试次数<300,间隔1s
|
||||||
|
return 1000;
|
||||||
|
} else if (count < 10) {
|
||||||
|
// 重试次数>300:间隔5s
|
||||||
|
return 1000 * 5;
|
||||||
|
} // 重试次数>3000:间隔30s
|
||||||
|
else {
|
||||||
|
return 1000 * 30;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.configureLogging(signalR.LogLevel.Debug)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 接收普通文本消息
|
||||||
|
connection.on('ReceiveTextMessageAsync', ReceiveTextMessageHandlerAsync);
|
||||||
|
// 接收广播消息
|
||||||
|
connection.on(
|
||||||
|
'ReceiveBroadCastMessageAsync',
|
||||||
|
ReceiveBroadCastMessageHandlerAsync,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收文本消息
|
||||||
|
* @param message 消息体
|
||||||
|
*/
|
||||||
|
function ReceiveTextMessageHandlerAsync(message: any) {
|
||||||
|
// 发布事件
|
||||||
|
eventbus.publish('ReceiveTextMessageHandlerAsync', message);
|
||||||
|
if (message.messageLevel === 10) {
|
||||||
|
notification.warn({
|
||||||
|
description: message.content,
|
||||||
|
message: message.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (message.messageLevel === 20) {
|
||||||
|
notification.info({
|
||||||
|
message: message.title,
|
||||||
|
description: message.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (message.messageLevel === 30) {
|
||||||
|
notification.error({
|
||||||
|
message: message.title,
|
||||||
|
description: message.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收广播消息
|
||||||
|
* @param message 消息体
|
||||||
|
*/
|
||||||
|
function ReceiveBroadCastMessageHandlerAsync(message: any) {
|
||||||
|
// 发布事件
|
||||||
|
eventbus.publish('ReceiveTextMessageHandlerAsync', message);
|
||||||
|
if (message.messageLevel === 10) {
|
||||||
|
notification.warn({
|
||||||
|
message: message.title,
|
||||||
|
description: message.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (message.messageLevel === 20) {
|
||||||
|
notification.info({
|
||||||
|
message: message.title,
|
||||||
|
description: message.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (message.messageLevel === 30) {
|
||||||
|
notification.error({
|
||||||
|
message: message.title,
|
||||||
|
description: message.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startConnect, closeConnect };
|
||||||
|
}
|
||||||
207
apps/web-antd/src/layouts/NotifyItem.vue
Normal file
207
apps/web-antd/src/layouts/NotifyItem.vue
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { Button, message as Message, Modal, Space, Tag } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
postNotificationNotificationPage,
|
||||||
|
postNotificationRead,
|
||||||
|
} from '#/api-client';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AbpNotifyItem',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formOptions: VbenFormProps = {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'messageType',
|
||||||
|
label: 'messageType',
|
||||||
|
defaultValue: 20,
|
||||||
|
dependencies: {
|
||||||
|
show: () => false,
|
||||||
|
triggerFields: ['messageType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'title',
|
||||||
|
label: $t('abp.message.title'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'content',
|
||||||
|
label: $t('abp.message.content'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'messageLevel',
|
||||||
|
label: $t('abp.message.level'),
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: $t('common.warning'),
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.info'),
|
||||||
|
value: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.error'),
|
||||||
|
value: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'read',
|
||||||
|
label: $t('abp.message.isRead'),
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: $t('common.yes'),
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.no'),
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
wrapperClass: 'grid-cols-5',
|
||||||
|
};
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const gridOptions: VxeGridProps<any> = {
|
||||||
|
checkboxConfig: {},
|
||||||
|
columns: [
|
||||||
|
{ title: $t('common.seq'), type: 'seq', width: 50 },
|
||||||
|
{ field: 'title', title: $t('abp.message.title'), minWidth: '150' },
|
||||||
|
{ field: 'content', title: $t('abp.message.content'), minWidth: '150' },
|
||||||
|
// { field: 'messageTypeName', title: '类型', minWidth: '150' },
|
||||||
|
{
|
||||||
|
field: 'messageLevelName',
|
||||||
|
title: $t('abp.message.level'),
|
||||||
|
minWidth: '150',
|
||||||
|
slots: { default: 'messageLevel' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'senderUserName',
|
||||||
|
title: $t('abp.message.sender'),
|
||||||
|
minWidth: '150',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'receiveUserName',
|
||||||
|
title: $t('abp.message.receiver'),
|
||||||
|
minWidth: '150',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'read',
|
||||||
|
title: $t('abp.message.isRead'),
|
||||||
|
minWidth: '150',
|
||||||
|
slots: { default: 'read' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creationTime',
|
||||||
|
title: $t('common.createTime'),
|
||||||
|
minWidth: '150',
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('common.action'),
|
||||||
|
field: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
minWidth: '150',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
toolbarConfig: {
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
customConfig: {
|
||||||
|
storage: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
const { data } = await postNotificationNotificationPage({
|
||||||
|
body: {
|
||||||
|
pageIndex: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
receiverUserId: userStore.userInfo?.id,
|
||||||
|
...formValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
|
||||||
|
const onRead = (row: any) => {
|
||||||
|
// if (row.read) {
|
||||||
|
// Message.info('该消息已读,不需要重复设置');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
Modal.confirm({
|
||||||
|
title: $t('abp.message.confirmRead'),
|
||||||
|
onOk: async () => {
|
||||||
|
await postNotificationRead({ body: { id: row.id } });
|
||||||
|
gridApi.reload();
|
||||||
|
Message.success($t('common.success'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<Grid>
|
||||||
|
<template #messageLevel="{ row }">
|
||||||
|
<Tag v-if="row.messageLevel === 10" color="yellow">
|
||||||
|
{{ row.messageLevelName }}
|
||||||
|
</Tag>
|
||||||
|
<Tag v-if="row.messageLevel === 20" color="green">
|
||||||
|
{{ row.messageLevelName }}
|
||||||
|
</Tag>
|
||||||
|
<Tag v-if="row.messageLevel === 30" color="red">
|
||||||
|
{{ row.messageLevelName }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #read="{ row }">
|
||||||
|
<Tag v-if="row.read" color="green">{{ $t('abp.message.read') }} </Tag>
|
||||||
|
<Tag v-else color="red"> {{ $t('abp.message.unread') }} </Tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ row }">
|
||||||
|
<Space>
|
||||||
|
<Button size="small" type="primary" @click="onRead(row)">
|
||||||
|
{{ $t('abp.message.setRead') }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
23
apps/web-antd/src/layouts/auth.vue
Normal file
23
apps/web-antd/src/layouts/auth.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { AuthPageLayout } from '@vben/layouts';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const appName = computed(() => preferences.app.name);
|
||||||
|
const logo = computed(() => preferences.logo.source);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthPageLayout
|
||||||
|
:app-name="appName"
|
||||||
|
:logo="logo"
|
||||||
|
:page-description="$t('authentication.pageDesc')"
|
||||||
|
:page-title="$t('authentication.pageTitle')"
|
||||||
|
>
|
||||||
|
<!-- 自定义工具栏 -->
|
||||||
|
<!-- <template #toolbar></template> -->
|
||||||
|
</AuthPageLayout>
|
||||||
|
</template>
|
||||||
256
apps/web-antd/src/layouts/basic.vue
Normal file
256
apps/web-antd/src/layouts/basic.vue
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { useEventbus, useRefresh, useWatermark } from '@vben/hooks';
|
||||||
|
import {
|
||||||
|
BasicLayout,
|
||||||
|
LockScreen,
|
||||||
|
Notification,
|
||||||
|
UserDropdown,
|
||||||
|
} from '@vben/layouts';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message as Message } from 'ant-design-vue/es/components';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
postNotificationBatchRead,
|
||||||
|
postNotificationNotificationPage,
|
||||||
|
postNotificationRead,
|
||||||
|
} from '#/api-client';
|
||||||
|
import { useSignalR } from '#/hooks/useSignalR';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
|
import MyProfile from './my-profile.vue';
|
||||||
|
import NotifyItem from './NotifyItem.vue';
|
||||||
|
|
||||||
|
const notifications = ref<NotificationItem[]>([]);
|
||||||
|
const { startConnect, closeConnect } = useSignalR();
|
||||||
|
function convertToNotificationItem(message: any): NotificationItem {
|
||||||
|
return {
|
||||||
|
avatar: '',
|
||||||
|
date: dayjs(message.creationTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
isRead: message.read,
|
||||||
|
message: message.content,
|
||||||
|
title: message.title,
|
||||||
|
id: message.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const eventbus = useEventbus();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const { refresh } = useRefresh();
|
||||||
|
onMounted(async () => {
|
||||||
|
// 判断是否登录
|
||||||
|
if (
|
||||||
|
userStore?.userInfo?.id ||
|
||||||
|
userStore?.userInfo?.token ||
|
||||||
|
userStore.checkUserLoginExpire()
|
||||||
|
) {
|
||||||
|
// 回登录页带上当前路由地址
|
||||||
|
await router.replace({
|
||||||
|
path: LOGIN_PATH,
|
||||||
|
query: {
|
||||||
|
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 开启SignalR
|
||||||
|
await startConnect();
|
||||||
|
let refreshCount = userStore?.applicationInfo?.refreshCount ?? -1;
|
||||||
|
if (refreshCount === -1) {
|
||||||
|
userStore.setApplicationInfo({ refreshCount: refreshCount + 1 });
|
||||||
|
} else {
|
||||||
|
refreshCount = refreshCount + 1;
|
||||||
|
// 获取刷新次数
|
||||||
|
userStore.setApplicationInfo({ refreshCount });
|
||||||
|
}
|
||||||
|
// 是否开启刷新浏览器,检查用户的角色信息
|
||||||
|
if (import.meta.env.VITE_REFRESH_ROLE && refreshCount > 0) {
|
||||||
|
// todo 这里直接调用一下方法刷新菜单,需要刷新2次浏览器才会有效果,问题暂时没有找到,但是在路由守卫可以,代码移动到路由守卫去了
|
||||||
|
// await authStore.getApplicationConfiguration();
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
eventbus.subscribe('ReceiveTextMessageHandlerAsync', (content) => {
|
||||||
|
const item: NotificationItem = {
|
||||||
|
avatar: '',
|
||||||
|
date: '',
|
||||||
|
message: content.contnet,
|
||||||
|
title: content.title,
|
||||||
|
id: content.id,
|
||||||
|
isRead: false,
|
||||||
|
};
|
||||||
|
notifications.value.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
await loadMessage();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(async () => {
|
||||||
|
await closeConnect();
|
||||||
|
});
|
||||||
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
|
const showDot = computed(() =>
|
||||||
|
notifications.value.some((item) => !item.isRead),
|
||||||
|
);
|
||||||
|
const [MyProfileModal, myProfileModalApi] = useVbenModal({
|
||||||
|
draggable: true,
|
||||||
|
onConfirm: () => {},
|
||||||
|
onBeforeClose: () => {},
|
||||||
|
});
|
||||||
|
const [NotifyItemModal, notifyItemModalApi] = useVbenModal({
|
||||||
|
draggable: true,
|
||||||
|
onConfirm: () => {},
|
||||||
|
onBeforeClose: () => {},
|
||||||
|
});
|
||||||
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
myProfileModalApi.open();
|
||||||
|
},
|
||||||
|
icon: 'ph:user',
|
||||||
|
text: $t('abp.user.myAccount'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const avatar = computed(() => {
|
||||||
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await authStore.logout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNoticeClear() {
|
||||||
|
notifications.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMakeAll() {
|
||||||
|
// 获取notifications 已读的数据,并且请求api 标记为已读
|
||||||
|
const readIds = notifications.value
|
||||||
|
.filter((item) => item.isRead)
|
||||||
|
.map((item) => item.id);
|
||||||
|
|
||||||
|
if (readIds.length > 0) {
|
||||||
|
await postNotificationBatchRead({ body: { ids: readIds } });
|
||||||
|
Message.success($t('common.editSuccess'));
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.value.forEach((item) => (item.isRead = true));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRead(row: NotificationItem) {
|
||||||
|
if (row.isRead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await postNotificationRead({ body: { id: row.id } });
|
||||||
|
|
||||||
|
notifications.value.forEach((item) => {
|
||||||
|
if (item.id === row.id) {
|
||||||
|
item.isRead = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function handleViewAll() {
|
||||||
|
notifyItemModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => preferences.app.watermark,
|
||||||
|
async (enable) => {
|
||||||
|
if (enable) {
|
||||||
|
await updateWatermark({
|
||||||
|
content: `${userStore.userInfo?.userName}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
destroyWatermark();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadMessage() {
|
||||||
|
notifications.value = [];
|
||||||
|
const message = await postNotificationNotificationPage({
|
||||||
|
body: {
|
||||||
|
pageIndex: 1,
|
||||||
|
pageSize: 4,
|
||||||
|
messageType: 20,
|
||||||
|
receiverUserId: userStore.userInfo?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.data?.items?.forEach((item) => {
|
||||||
|
notifications.value.push(convertToNotificationItem(item));
|
||||||
|
// 按照isRead属性进行排序
|
||||||
|
notifications.value.sort((a, b) => {
|
||||||
|
if (a.isRead === b.isRead) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return a.isRead ? 1 : -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||||
|
<template #user-dropdown>
|
||||||
|
<UserDropdown
|
||||||
|
:avatar
|
||||||
|
:menus
|
||||||
|
:tag-text="userStore.tenant?.name"
|
||||||
|
:text="userStore.userInfo?.name"
|
||||||
|
@logout="handleLogout"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #notification>
|
||||||
|
<Notification
|
||||||
|
:dot="showDot"
|
||||||
|
:notifications="notifications"
|
||||||
|
@clear="handleNoticeClear"
|
||||||
|
@icon-click="loadMessage"
|
||||||
|
@make-all="handleMakeAll"
|
||||||
|
@read="handleRead"
|
||||||
|
@view-all="handleViewAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<AuthenticationLoginExpiredModal
|
||||||
|
v-model:open="accessStore.loginExpired"
|
||||||
|
:avatar
|
||||||
|
>
|
||||||
|
<LoginForm />
|
||||||
|
</AuthenticationLoginExpiredModal>
|
||||||
|
</template>
|
||||||
|
<template #lock-screen>
|
||||||
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
</template>
|
||||||
|
</BasicLayout>
|
||||||
|
<MyProfileModal
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
:title="$t('abp.user.myAccount')"
|
||||||
|
class="h-[410px] w-[800px]"
|
||||||
|
>
|
||||||
|
<MyProfile />
|
||||||
|
</MyProfileModal>
|
||||||
|
|
||||||
|
<NotifyItemModal
|
||||||
|
:fullscreen="true"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<NotifyItem />
|
||||||
|
</NotifyItemModal>
|
||||||
|
</template>
|
||||||
6
apps/web-antd/src/layouts/index.ts
Normal file
6
apps/web-antd/src/layouts/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const BasicLayout = () => import('./basic.vue');
|
||||||
|
const AuthPageLayout = () => import('./auth.vue');
|
||||||
|
|
||||||
|
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||||
|
|
||||||
|
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||||
216
apps/web-antd/src/layouts/my-profile.vue
Normal file
216
apps/web-antd/src/layouts/my-profile.vue
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { z } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Image,
|
||||||
|
message as Message,
|
||||||
|
Row,
|
||||||
|
Spin,
|
||||||
|
Step,
|
||||||
|
Steps,
|
||||||
|
TabPane,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
postUsersChangePassword,
|
||||||
|
postUsersMyProfile,
|
||||||
|
} from '#/api-client';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MyProfile',
|
||||||
|
});
|
||||||
|
const activeName = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const [ProfileForm, profileFormApi] = useVbenForm({
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
// 所有表单项共用,可单独在表单内覆盖
|
||||||
|
commonConfig: {
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-4/5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 提交函数
|
||||||
|
handleSubmit: () => {},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.userName'),
|
||||||
|
},
|
||||||
|
fieldName: 'userName',
|
||||||
|
label: $t('abp.user.userName'),
|
||||||
|
rules: z.string().min(1, {
|
||||||
|
message: $t('common.pleaseInput') + $t('abp.user.userName'),
|
||||||
|
}),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.name'),
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
disabled: true,
|
||||||
|
label: $t('abp.user.name'),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// component: 'VbenInput',
|
||||||
|
// componentProps: {
|
||||||
|
// placeholder: $t('common.pleaseInput') + $t('abp.user.surname'),
|
||||||
|
// },
|
||||||
|
// fieldName: 'surname',
|
||||||
|
// label: $t('abp.user.surname'),
|
||||||
|
// rules: z.string().min(1, {
|
||||||
|
// message: $t('common.pleaseInput') + $t('abp.user.surname'),
|
||||||
|
// }),
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.email'),
|
||||||
|
},
|
||||||
|
fieldName: 'email',
|
||||||
|
disabled: true,
|
||||||
|
label: $t('abp.user.email'),
|
||||||
|
rules: z.string().min(1, {
|
||||||
|
message: $t('common.pleaseInput') + $t('abp.user.email'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.phone'),
|
||||||
|
},
|
||||||
|
fieldName: 'phoneNumber',
|
||||||
|
disabled: true,
|
||||||
|
label: $t('abp.user.phone'),
|
||||||
|
rules: z.string().min(1, {
|
||||||
|
message: $t('common.pleaseInput') + $t('abp.user.phone'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// showCollapseButton: false,
|
||||||
|
// showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-1',
|
||||||
|
resetButtonOptions: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
submitButtonOptions: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [ResetPasswordForm, resetPasswordApi] = useVbenForm({
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
// 所有表单项共用,可单独在表单内覆盖
|
||||||
|
commonConfig: {
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-4/5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 提交函数
|
||||||
|
handleSubmit: async () => {
|
||||||
|
// 表单校验
|
||||||
|
const { valid } = await resetPasswordApi.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
const formValues = await resetPasswordApi.getValues();
|
||||||
|
|
||||||
|
if (formValues.currentPassword === formValues.confirmPassword) {
|
||||||
|
Message.warn($t('abp.user.newPasswordAndCurrentPasswordNotAlike'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formValues.newPassword !== formValues.confirmPassword) {
|
||||||
|
Message.warn($t('abp.user.newPasswordAndConfirmPasswordNotMatch'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await postUsersChangePassword({ body: formValues });
|
||||||
|
Message.success($t('common.editSuccess'));
|
||||||
|
await resetPasswordApi.resetForm();
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
|
||||||
|
},
|
||||||
|
fieldName: 'currentPassword',
|
||||||
|
label: $t('abp.user.currentPassword'),
|
||||||
|
rules: z.string().min(1, {
|
||||||
|
message: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.newPassword'),
|
||||||
|
},
|
||||||
|
fieldName: 'newPassword',
|
||||||
|
label: $t('abp.user.newPassword'),
|
||||||
|
rules: z.string().min(1, {
|
||||||
|
message: $t('common.pleaseInput') + $t('abp.user.newPassword'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
|
||||||
|
},
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: $t('abp.user.comfirmPassword'),
|
||||||
|
rules: z.string().min(1, {
|
||||||
|
message: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resetButtonOptions: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
submitButtonOptions: {
|
||||||
|
content: '确认',
|
||||||
|
},
|
||||||
|
wrapperClass: 'grid-cols-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const resp = await postUsersMyProfile();
|
||||||
|
await profileFormApi.setValues({ ...resp.data });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Spin :spinning="loading" tip="loading...">
|
||||||
|
<div class="bg-card px-8">
|
||||||
|
<Tabs
|
||||||
|
v-model:active-key="activeName"
|
||||||
|
tab-position="left"
|
||||||
|
@change="activeChange"
|
||||||
|
>
|
||||||
|
<TabPane :key="0" :tab="$t('abp.user.myProfile')">
|
||||||
|
<ProfileForm />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane :key="1" :tab="$t('abp.user.changePassword')">
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
3
apps/web-antd/src/locales/README.md
Normal file
3
apps/web-antd/src/locales/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# locale
|
||||||
|
|
||||||
|
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||||
102
apps/web-antd/src/locales/index.ts
Normal file
102
apps/web-antd/src/locales/index.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import type { Locale } from 'ant-design-vue/es/locale';
|
||||||
|
|
||||||
|
import type { App } from 'vue';
|
||||||
|
|
||||||
|
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
$t,
|
||||||
|
setupI18n as coreSetup,
|
||||||
|
loadLocalesMapFromDir,
|
||||||
|
} from '@vben/locales';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
|
||||||
|
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||||
|
|
||||||
|
const modules = import.meta.glob('./langs/**/*.json');
|
||||||
|
|
||||||
|
const localesMap = loadLocalesMapFromDir(
|
||||||
|
/\.\/langs\/([^/]+)\/(.*)\.json$/,
|
||||||
|
modules,
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* 加载应用特有的语言包
|
||||||
|
* 这里也可以改造为从服务端获取翻译数据
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadMessages(lang: SupportedLanguagesType) {
|
||||||
|
const [appLocaleMessages] = await Promise.all([
|
||||||
|
localesMap[lang]?.(),
|
||||||
|
loadThirdPartyMessage(lang),
|
||||||
|
]);
|
||||||
|
return appLocaleMessages?.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载第三方组件库的语言包
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
|
||||||
|
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载dayjs的语言包
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadDayjsLocale(lang: SupportedLanguagesType) {
|
||||||
|
let locale;
|
||||||
|
switch (lang) {
|
||||||
|
case 'en-US': {
|
||||||
|
locale = await import('dayjs/locale/en');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'zh-CN': {
|
||||||
|
locale = await import('dayjs/locale/zh-cn');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 默认使用英语
|
||||||
|
default: {
|
||||||
|
locale = await import('dayjs/locale/en');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (locale) {
|
||||||
|
dayjs.locale(locale);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to load dayjs locale for ${lang}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载antd的语言包
|
||||||
|
* @param lang
|
||||||
|
*/
|
||||||
|
async function loadAntdLocale(lang: SupportedLanguagesType) {
|
||||||
|
switch (lang) {
|
||||||
|
case 'en-US': {
|
||||||
|
antdLocale.value = antdEnLocale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'zh-CN': {
|
||||||
|
antdLocale.value = antdDefaultLocale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
|
||||||
|
await coreSetup(app, {
|
||||||
|
defaultLocale: preferences.app.locale,
|
||||||
|
loadMessages,
|
||||||
|
missingWarn: !import.meta.env.PROD,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { $t, antdLocale, setupI18n };
|
||||||
131
apps/web-antd/src/locales/langs/en-US/abp.json
Normal file
131
apps/web-antd/src/locales/langs/en-US/abp.json
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"selectTenant": "Please select Tenant and ignore the non-tenant mode",
|
||||||
|
"oidcTip": "Login......"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"system": "SystemManagement",
|
||||||
|
"user": "UserManagement",
|
||||||
|
"role": "RoleManagement",
|
||||||
|
"tenant": "TenantManagement",
|
||||||
|
"tenantList": "TenantList",
|
||||||
|
"language": "LanguageManagement",
|
||||||
|
"auditLog": "AuditLog",
|
||||||
|
"loginLog": "LoginLog",
|
||||||
|
"feature": "FeatureManagement",
|
||||||
|
"setting": "SettingsManagement",
|
||||||
|
"organizationUnit": "OrganizationUnitManagement",
|
||||||
|
"languageText": "LanguageTextManagement",
|
||||||
|
"dataDictionary": "DataDictionaryManagement",
|
||||||
|
"notification": "NotificationManagement",
|
||||||
|
"message": "MessageManagement",
|
||||||
|
"code": "CodeManagement",
|
||||||
|
"code-project": "ProjectList",
|
||||||
|
"code-template": "TemplateList",
|
||||||
|
"code-genarate": "CodeGenarate",
|
||||||
|
"code-entity": "Entity",
|
||||||
|
"code-template-detail": "TemplateDetail",
|
||||||
|
"code-Preview": "Preview"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"user": "User",
|
||||||
|
"userName": "UserName",
|
||||||
|
"name": "Name",
|
||||||
|
"surname": "Surname",
|
||||||
|
"email": "Email",
|
||||||
|
"phone": "Phone",
|
||||||
|
"password": "Password",
|
||||||
|
"currentPassword": "CurrentPassword",
|
||||||
|
"newPassword": "NewPassword",
|
||||||
|
"confirmNewPassword": "ConfirmNewPassword",
|
||||||
|
"changePassword": "ChangePassword",
|
||||||
|
"status": "Status",
|
||||||
|
"comfirmPassword": "ComfirmPassword",
|
||||||
|
"comfirmDeleteUser": "Are you sure you want to delete the user",
|
||||||
|
"newPasswordAndConfirmPasswordNotMatch": "New password and confirm password not match",
|
||||||
|
"newPasswordAndCurrentPasswordNotAlike": "The old and new passwords cannot be the same",
|
||||||
|
"myProfile": "MyProfile",
|
||||||
|
"myAccount": "MyAccount",
|
||||||
|
"twoFactor": "TwoFactor",
|
||||||
|
"verifyAuthenticator": "Verify the Authenticator",
|
||||||
|
"twoFactorDesc": "Your two-factor authentication app will generate a code. Please enter the code and confirm it.",
|
||||||
|
"code": "TwoFactor Code",
|
||||||
|
"twoFactorEnabled": "Two-factor authentication for your account has been successfully enabled. You will be required to enter the code generated by the application when logging in.",
|
||||||
|
"twoFactorDisabled": "Two-factor authentication for your account has been turned off. You will no longer be required to enter the code generated by the application when logging in.",
|
||||||
|
"twoFactorEnabledDesc": "Two-factor authentication has been enabled for your account. If you need to turn it off, please enter the two-factor authentication code and then disable it.",
|
||||||
|
"resetTwoFactor": "ResetTwoFactor"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"role": "Role",
|
||||||
|
"roleName": "RoleName",
|
||||||
|
"isDefault": "IsDefault",
|
||||||
|
"permissions": "Permissions"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"loginTime": "LoginTime",
|
||||||
|
"userName": "UserName",
|
||||||
|
"tenant": "Tenant",
|
||||||
|
"executionTime": "ExecutionTime",
|
||||||
|
"responseTime": "ResponseTime(ms)",
|
||||||
|
"clientIp": "ClientIp",
|
||||||
|
"exception": "Exception",
|
||||||
|
"applicationName": "ApplicationName",
|
||||||
|
"loginMode": "LoginMode",
|
||||||
|
"loginUrl": "LoginUrl",
|
||||||
|
"detail": "Detail"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"title": "Title",
|
||||||
|
"content": "Content",
|
||||||
|
"type": "Type",
|
||||||
|
"level": "Level",
|
||||||
|
"sender": "Sender",
|
||||||
|
"receiver": "Receiver",
|
||||||
|
"isRead": "IsRead",
|
||||||
|
"sendMessage": "SendMessage",
|
||||||
|
"sendNotification": "SendNotification",
|
||||||
|
"setRead": "SetRead",
|
||||||
|
"confirmRead": "Are you sure you want to set the message as read?",
|
||||||
|
"read": "Read",
|
||||||
|
"unread": "UnRead"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"language": "LanguageName",
|
||||||
|
"showLanguage": "ShowLanguageName",
|
||||||
|
"icon": "Icon",
|
||||||
|
"resourceName": "ResourceName",
|
||||||
|
"value": "Value",
|
||||||
|
"name": "Name"
|
||||||
|
},
|
||||||
|
"dataDictionary": {
|
||||||
|
"codeName": "Code|Name",
|
||||||
|
"code": "Code",
|
||||||
|
"name": "Name",
|
||||||
|
"order": "Order",
|
||||||
|
"status": "Status",
|
||||||
|
"description": "Description",
|
||||||
|
"type": "Type"
|
||||||
|
},
|
||||||
|
"organizationunit": {
|
||||||
|
"organizationunit": "Organizationunit",
|
||||||
|
"add": "Add",
|
||||||
|
"member": "Member",
|
||||||
|
"role": "Role",
|
||||||
|
"userName": "UserName",
|
||||||
|
"email": "Email",
|
||||||
|
"name": "Name",
|
||||||
|
"selectNode": "Select a node to operate on"
|
||||||
|
},
|
||||||
|
"tenant": {
|
||||||
|
"tenant": "Tenant",
|
||||||
|
"notExist": "Tenant Does not exist",
|
||||||
|
"name": "Tenant Name",
|
||||||
|
"adminEmail": "Admin Email",
|
||||||
|
"adminPassword": "Admin Password",
|
||||||
|
"mangeConnectionString": "MangeConnectionString",
|
||||||
|
"addorEdit": "AddorEdit",
|
||||||
|
"featureManagement": "FeatureManagement",
|
||||||
|
"connectionStringName": "ConnectionStringName",
|
||||||
|
"connectionString": "ConnectionString"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
apps/web-antd/src/locales/langs/en-US/code.json
Normal file
37
apps/web-antd/src/locales/langs/en-US/code.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"companyName": "CompanyName",
|
||||||
|
"projectName": "ProjectName",
|
||||||
|
"projectEnglishName": "ProjectEnglishName",
|
||||||
|
"namespace": "Namespace",
|
||||||
|
"remark": "Remark",
|
||||||
|
"templateName": "TemplateName",
|
||||||
|
"model": "Model",
|
||||||
|
"property": "Property",
|
||||||
|
"enum": "Enum",
|
||||||
|
"isRequired": "IsRequired",
|
||||||
|
"maxLength": "MaxLength",
|
||||||
|
"minLength": "MinLength",
|
||||||
|
"decimalPrecision18": "Accuracy (18,6) of 18",
|
||||||
|
"decimalPrecision6": "Accuracy (18,6) of 6",
|
||||||
|
"pleaseSelectEntity": "Please select entity",
|
||||||
|
"pleaseSelectEnum": "Please select enum",
|
||||||
|
"detail": "Detail",
|
||||||
|
"copy": "Copy",
|
||||||
|
"addFolder": "AddFolder",
|
||||||
|
"addFile": "AddFile",
|
||||||
|
"desc": "Description",
|
||||||
|
"name": "Name",
|
||||||
|
"templateType": "TemplateType",
|
||||||
|
"supportTenant": "Support Multi-Tenant",
|
||||||
|
"project": "Project",
|
||||||
|
"template": "Template",
|
||||||
|
"preview": "Preview",
|
||||||
|
"download": "Download",
|
||||||
|
"description": "Please select the target and project to generate",
|
||||||
|
"autoGenerate": "Auto Generate Code",
|
||||||
|
"code": "Code",
|
||||||
|
"type": "Type",
|
||||||
|
"value": "Value",
|
||||||
|
"relational": "Relational",
|
||||||
|
"dataType": "DataType"
|
||||||
|
}
|
||||||
39
apps/web-antd/src/locales/langs/en-US/common.json
Normal file
39
apps/web-antd/src/locales/langs/en-US/common.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"add": "Add",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"search": "Search",
|
||||||
|
"export": "Export",
|
||||||
|
"save": "save",
|
||||||
|
"seq": "Seq",
|
||||||
|
"isEnable": "IsEnable",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"action": "Action",
|
||||||
|
"createTime": "CreationTime",
|
||||||
|
"pleaseInput": "Please input",
|
||||||
|
"addSuccess": "Add Success",
|
||||||
|
"editSuccess": "Edit Success",
|
||||||
|
"deleteSuccess": "Delete Success",
|
||||||
|
"yes": "yes",
|
||||||
|
"no": "no",
|
||||||
|
"confirmDelete": "Confirm to delete",
|
||||||
|
"askConfirmDelete": "Confirm to delete?",
|
||||||
|
"warning": "warning",
|
||||||
|
"info": "info",
|
||||||
|
"error": "error",
|
||||||
|
"success": "Success",
|
||||||
|
"keyword": "Keyword",
|
||||||
|
"mesage403": "You don't have permission to access this page or function!",
|
||||||
|
"mesage401": "You are not logged in or the login has timed out. Please log in again!",
|
||||||
|
"mesage500": "Internal server error!",
|
||||||
|
"mesage404": "The page you are trying to access does not exist!",
|
||||||
|
"mesage400": "Request error!",
|
||||||
|
"mesage405": "Incorrect request method!",
|
||||||
|
"timeOut": "Request timed out!",
|
||||||
|
"expandAll": "EexpandAll",
|
||||||
|
"collapseAll": "CollapseAll",
|
||||||
|
"description": "description",
|
||||||
|
"comfirm": "Comfirm",
|
||||||
|
"valid": "Valid"
|
||||||
|
}
|
||||||
15
apps/web-antd/src/locales/langs/en-US/page.json
Normal file
15
apps/web-antd/src/locales/langs/en-US/page.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": "Login",
|
||||||
|
"register": "Register",
|
||||||
|
"codeLogin": "Code Login",
|
||||||
|
"qrcodeLogin": "Qr Code Login",
|
||||||
|
"forgetPassword": "Forget Password",
|
||||||
|
"thirdPartyLogin": "Third Party Login"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"workspace": "Workspace"
|
||||||
|
}
|
||||||
|
}
|
||||||
131
apps/web-antd/src/locales/langs/zh-CN/abp.json
Normal file
131
apps/web-antd/src/locales/langs/zh-CN/abp.json
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"login": {
|
||||||
|
"selectTenant": "请选择租户,非租户模式请忽略",
|
||||||
|
"oidcTip": "登陆中......"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"system": "系统管理",
|
||||||
|
"user": "用户管理",
|
||||||
|
"role": "角色管理",
|
||||||
|
"tenant": "租户管理",
|
||||||
|
"tenantList": "租户列表",
|
||||||
|
"language": "语言管理",
|
||||||
|
"auditLog": "审计日志",
|
||||||
|
"loginLog": "登录日志",
|
||||||
|
"feature": "功能管理",
|
||||||
|
"setting": "设置管理",
|
||||||
|
"organizationUnit": "组织机构管理",
|
||||||
|
"languageText": "语言文本管理",
|
||||||
|
"dataDictionary": "数据字典管理",
|
||||||
|
"notification": "通告管理",
|
||||||
|
"message": "消息管理",
|
||||||
|
"code": "代码管理",
|
||||||
|
"code-project": "项目列表",
|
||||||
|
"code-template": "模板列表",
|
||||||
|
"code-genarate": "代码生成",
|
||||||
|
"code-entity": "实体",
|
||||||
|
"code-template-detail": "模板详情",
|
||||||
|
"code-Preview": "预览"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"user": "用户",
|
||||||
|
"userName": "用户名",
|
||||||
|
"name": "名称",
|
||||||
|
"surname": "姓氏",
|
||||||
|
"email": "邮箱",
|
||||||
|
"phone": "手机号",
|
||||||
|
"password": "密码",
|
||||||
|
"currentPassword": "当前密码",
|
||||||
|
"newPassword": "新密码",
|
||||||
|
"confirmNewPassword": "确认新密码",
|
||||||
|
"changePassword": "修改密码",
|
||||||
|
"status": "状态",
|
||||||
|
"comfirmPassword": "确认密码",
|
||||||
|
"comfirmDeleteUser": "确认删除用户",
|
||||||
|
"newPasswordAndConfirmPasswordNotMatch": "新密码与确认密码不匹配",
|
||||||
|
"newPasswordAndCurrentPasswordNotAlike": "新旧密码不能一样",
|
||||||
|
"myProfile": "个人信息",
|
||||||
|
"myAccount": "我的账户",
|
||||||
|
"twoFactor": "双因素验证",
|
||||||
|
"verifyAuthenticator": "验证身份验证器",
|
||||||
|
"twoFactorDesc": "您的双因素身份验证应用程序将生成一个代码,请输入该代码并确认.",
|
||||||
|
"code": "双因素验证码",
|
||||||
|
"twoFactorEnabled": "您的账户已经成功开启双因素验证,登录时要求输入应用程序生成的代码.",
|
||||||
|
"twoFactorDisabled": "您的账户已经关闭双因素验证,登录时不在要求输入应用程序生成的代码.",
|
||||||
|
"twoFactorEnabledDesc": "您的账户已经开启双因素验证,如果需要关闭,请输入双因素验证码,然后在禁用.如果忘记验证码,请联系管理员.",
|
||||||
|
"resetTwoFactor": "重置双因素验证"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"role": "角色",
|
||||||
|
"roleName": "角色名称",
|
||||||
|
"isDefault": "是否默认",
|
||||||
|
"permissions": "授权"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"loginTime": "登录时间",
|
||||||
|
"userName": "用户名",
|
||||||
|
"tenant": "租户",
|
||||||
|
"executionTime": "执行时间",
|
||||||
|
"responseTime": "响应时间(毫秒)",
|
||||||
|
"clientIp": "客户端Ip",
|
||||||
|
"exception": "异常",
|
||||||
|
"applicationName": "应用名称",
|
||||||
|
"loginMode": "登录方式",
|
||||||
|
"loginUrl": "登录地址",
|
||||||
|
"detail": "详情"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"title": "标题",
|
||||||
|
"content": "内容",
|
||||||
|
"type": "类型",
|
||||||
|
"level": "级别",
|
||||||
|
"sender": "发送人",
|
||||||
|
"receiver": "接收人",
|
||||||
|
"isRead": "是否已读",
|
||||||
|
"sendMessage": "发送消息",
|
||||||
|
"sendNotification": "发送通告",
|
||||||
|
"setRead": "设置已读",
|
||||||
|
"confirmRead": "确认设置已读?",
|
||||||
|
"read": "已读",
|
||||||
|
"unread": "未读"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"language": "语言名称",
|
||||||
|
"showLanguage": "显示名称",
|
||||||
|
"icon": "图标",
|
||||||
|
"resourceName": "资源名称",
|
||||||
|
"value": "值",
|
||||||
|
"name": "名称"
|
||||||
|
},
|
||||||
|
"dataDictionary": {
|
||||||
|
"codeName": "编码|名称",
|
||||||
|
"code": "编码",
|
||||||
|
"name": "名称",
|
||||||
|
"order": "排序",
|
||||||
|
"status": "状态",
|
||||||
|
"description": "描述",
|
||||||
|
"type": "字典类型"
|
||||||
|
},
|
||||||
|
"organizationunit": {
|
||||||
|
"organizationunit": "组织机构",
|
||||||
|
"add": "新增根机构",
|
||||||
|
"member": "成员",
|
||||||
|
"role": "角色",
|
||||||
|
"userName": "用户名",
|
||||||
|
"email": "邮箱",
|
||||||
|
"name": "名称",
|
||||||
|
"selectNode": "请先选择一个节点再进行操作"
|
||||||
|
},
|
||||||
|
"tenant": {
|
||||||
|
"tenant": "租户",
|
||||||
|
"notExist": "租户不存在",
|
||||||
|
"name": "租户名称",
|
||||||
|
"adminEmail": "管理员邮箱",
|
||||||
|
"adminPassword": "管理员密码",
|
||||||
|
"mangeConnectionString": "管理链接字符串",
|
||||||
|
"addorEdit": "新增或编辑",
|
||||||
|
"featureManagement": "功能管理",
|
||||||
|
"connectionStringName": "连接名称",
|
||||||
|
"connectionString": "连接字符串"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
apps/web-antd/src/locales/langs/zh-CN/code.json
Normal file
37
apps/web-antd/src/locales/langs/zh-CN/code.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"companyName": "公司名称",
|
||||||
|
"projectName": "项目名称",
|
||||||
|
"projectEnglishName": "项目英文名称",
|
||||||
|
"namespace": "命名空间",
|
||||||
|
"remark": "备注",
|
||||||
|
"templateName": "模板名称",
|
||||||
|
"model": "模型",
|
||||||
|
"property": "属性",
|
||||||
|
"enum": "枚举",
|
||||||
|
"isRequired": "是否必填",
|
||||||
|
"maxLength": "最大长度",
|
||||||
|
"minLength": "最小长度",
|
||||||
|
"decimalPrecision18": "精度(18,6)中的18",
|
||||||
|
"decimalPrecision6": "精度(18,6)中的6",
|
||||||
|
"pleaseSelectEntity": "请选择实体",
|
||||||
|
"pleaseSelectEnum": "请选择枚举",
|
||||||
|
"detail": "详情",
|
||||||
|
"copy": "复制",
|
||||||
|
"addFolder": "新增文件夹",
|
||||||
|
"addFile": "新增文件",
|
||||||
|
"desc": "描述",
|
||||||
|
"name": "名称",
|
||||||
|
"templateType": "模板类型",
|
||||||
|
"supportTenant": "支持多租户",
|
||||||
|
"project": "项目",
|
||||||
|
"template": "模板",
|
||||||
|
"preview": "预览",
|
||||||
|
"download": "下载",
|
||||||
|
"description": "请选择要生成的目标和项目",
|
||||||
|
"autoGenerate": "自动生成代码",
|
||||||
|
"code": "编码",
|
||||||
|
"type": "类型",
|
||||||
|
"value": "值",
|
||||||
|
"relational": "关系",
|
||||||
|
"dataType": "数据类型"
|
||||||
|
}
|
||||||
39
apps/web-antd/src/locales/langs/zh-CN/common.json
Normal file
39
apps/web-antd/src/locales/langs/zh-CN/common.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"add": "新增",
|
||||||
|
"edit": "编辑",
|
||||||
|
"delete": "删除",
|
||||||
|
"search": "搜索",
|
||||||
|
"save": "保存",
|
||||||
|
"export": "导出",
|
||||||
|
"isEnable": "是否启用",
|
||||||
|
"enabled": "启用",
|
||||||
|
"disabled": "禁用",
|
||||||
|
"seq": "序号",
|
||||||
|
"action": "操作",
|
||||||
|
"createTime": "创建时间",
|
||||||
|
"pleaseInput": "请输入",
|
||||||
|
"addSuccess": "新增成功!",
|
||||||
|
"editSuccess": "编辑成功!",
|
||||||
|
"deleteSuccess": "删除成功!",
|
||||||
|
"yes": "是",
|
||||||
|
"no": "否",
|
||||||
|
"confirmDelete": "确认删除",
|
||||||
|
"askConfirmDelete": "确认删除吗?",
|
||||||
|
"warning": "警告",
|
||||||
|
"info": "正常",
|
||||||
|
"error": "错误",
|
||||||
|
"success": "成功",
|
||||||
|
"keyword": "关键字",
|
||||||
|
"mesage403": "您没有权限访问此页面或功能!",
|
||||||
|
"mesage401": "未登录或登录已超时,请重新登录!",
|
||||||
|
"mesage500": "服务器内部错误!",
|
||||||
|
"mesage404": "您访问的页面不存在!",
|
||||||
|
"mesage400": "请求错误!",
|
||||||
|
"mesage405": "请求方法错误!",
|
||||||
|
"timeOut": "请求超时!",
|
||||||
|
"expandAll": "展开全部",
|
||||||
|
"collapseAll": "折叠全部",
|
||||||
|
"description": "描述",
|
||||||
|
"comfirm": "确认",
|
||||||
|
"valid": "验证"
|
||||||
|
}
|
||||||
15
apps/web-antd/src/locales/langs/zh-CN/page.json
Normal file
15
apps/web-antd/src/locales/langs/zh-CN/page.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": "登录",
|
||||||
|
"register": "注册",
|
||||||
|
"codeLogin": "验证码登录",
|
||||||
|
"qrcodeLogin": "二维码登录",
|
||||||
|
"forgetPassword": "忘记密码",
|
||||||
|
"thirdPartyLogin": "第三方登录"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "概览",
|
||||||
|
"analytics": "分析页",
|
||||||
|
"workspace": "工作台"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
apps/web-antd/src/main.ts
Normal file
35
apps/web-antd/src/main.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { initPreferences } from '@vben/preferences';
|
||||||
|
import { unmountGlobalLoading } from '@vben/utils';
|
||||||
|
|
||||||
|
import { overridesPreferences } from './preferences';
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-imports
|
||||||
|
import client from '#/api-client-config/index';
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-imports
|
||||||
|
import clientblob from '#/api-client-config/index-blob';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用初始化完成之后再进行页面加载渲染
|
||||||
|
*/
|
||||||
|
async function initApplication() {
|
||||||
|
// name用于指定项目唯一标识
|
||||||
|
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||||
|
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||||
|
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||||
|
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
|
||||||
|
|
||||||
|
// app偏好设置初始化
|
||||||
|
await initPreferences({
|
||||||
|
namespace,
|
||||||
|
overrides: overridesPreferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动应用并挂载
|
||||||
|
// vue应用主要逻辑及视图
|
||||||
|
const { bootstrap } = await import('./bootstrap');
|
||||||
|
await bootstrap(namespace);
|
||||||
|
|
||||||
|
// 移除并销毁loading
|
||||||
|
unmountGlobalLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
initApplication();
|
||||||
23
apps/web-antd/src/preferences.ts
Normal file
23
apps/web-antd/src/preferences.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { defineOverridesPreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 项目配置文件
|
||||||
|
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||||
|
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||||
|
*/
|
||||||
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
|
// overrides
|
||||||
|
app: {
|
||||||
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
|
// 是否开启检查更新
|
||||||
|
enableCheckUpdates: false,
|
||||||
|
// 检查更新的时间间隔,单位为分钟
|
||||||
|
checkUpdatesInterval: 1,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
companyName: '集社',
|
||||||
|
},
|
||||||
|
});
|
||||||
42
apps/web-antd/src/router/access.ts
Normal file
42
apps/web-antd/src/router/access.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type {
|
||||||
|
ComponentRecordType,
|
||||||
|
GenerateMenuAndRoutesOptions,
|
||||||
|
} from '@vben/types';
|
||||||
|
|
||||||
|
import { generateAccessible } from '@vben/access';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getAllMenusApi } from '#/api';
|
||||||
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
|
||||||
|
const layoutMap: ComponentRecordType = {
|
||||||
|
BasicLayout,
|
||||||
|
IFrameView,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await generateAccessible(preferences.app.accessMode, {
|
||||||
|
...options,
|
||||||
|
fetchMenuListAsync: async () => {
|
||||||
|
message.loading({
|
||||||
|
content: `${$t('common.loadingMenu')}...`,
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
return await getAllMenusApi();
|
||||||
|
},
|
||||||
|
// 可以指定没有权限跳转403页面
|
||||||
|
forbiddenComponent,
|
||||||
|
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||||
|
layoutMap,
|
||||||
|
pageMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateAccess };
|
||||||
138
apps/web-antd/src/router/guard.ts
Normal file
138
apps/web-antd/src/router/guard.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
|
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
||||||
|
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import { generateAccess } from './access';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用守卫配置
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function setupCommonGuard(router: Router) {
|
||||||
|
// 记录已经加载的页面
|
||||||
|
const loadedPaths = new Set<string>();
|
||||||
|
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
to.meta.loaded = loadedPaths.has(to.path);
|
||||||
|
|
||||||
|
// 页面加载进度条
|
||||||
|
if (!to.meta.loaded && preferences.transition.progress) {
|
||||||
|
startProgress();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||||
|
|
||||||
|
loadedPaths.add(to.path);
|
||||||
|
|
||||||
|
// 关闭页面加载进度条
|
||||||
|
if (preferences.transition.progress) {
|
||||||
|
stopProgress();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限访问守卫配置
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function setupAccessGuard(router: Router) {
|
||||||
|
router.beforeEach(async (to, from) => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
// 基本路由,这些路由不需要进入权限拦截
|
||||||
|
if (coreRouteNames.includes(to.name as string)) {
|
||||||
|
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||||
|
return decodeURIComponent(
|
||||||
|
(to.query?.redirect as string) ||
|
||||||
|
userStore.userInfo?.homePath ||
|
||||||
|
DEFAULT_HOME_PATH,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessToken 检查
|
||||||
|
if (!accessStore.accessToken) {
|
||||||
|
// 明确声明忽略权限访问权限,则可以访问
|
||||||
|
if (to.meta.ignoreAccess) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有访问权限,跳转登录页面
|
||||||
|
if (to.fullPath !== LOGIN_PATH) {
|
||||||
|
return {
|
||||||
|
path: LOGIN_PATH,
|
||||||
|
// 如不需要,直接删除 query
|
||||||
|
query:
|
||||||
|
to.fullPath === DEFAULT_HOME_PATH
|
||||||
|
? {}
|
||||||
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
|
replace: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否已经生成过动态路由
|
||||||
|
if (accessStore.isAccessChecked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 生成路由表
|
||||||
|
// 当前登录用户拥有的角色标识列表
|
||||||
|
// const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||||
|
// const userRoles = userInfo.roles ?? [];
|
||||||
|
const refreshCount = userStore?.applicationInfo?.refreshCount ?? -1;
|
||||||
|
if (import.meta.env.VITE_REFRESH_ROLE && refreshCount > 0) {
|
||||||
|
await authStore.getApplicationConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRoles = accessStore.accessCodes ?? [];
|
||||||
|
|
||||||
|
// 生成菜单和路由
|
||||||
|
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||||
|
roles: userRoles,
|
||||||
|
router,
|
||||||
|
// 则会在菜单中显示,但是访问会被重定向到403
|
||||||
|
routes: accessRoutes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存菜单信息和路由信息
|
||||||
|
accessStore.setAccessMenus(accessibleMenus);
|
||||||
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
|
accessStore.setIsAccessChecked(true);
|
||||||
|
const redirectPath = (from.query.redirect ??
|
||||||
|
(to.path === DEFAULT_HOME_PATH
|
||||||
|
? DEFAULT_HOME_PATH
|
||||||
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...router.resolve(decodeURIComponent(redirectPath)),
|
||||||
|
replace: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目守卫配置
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
function createRouterGuard(router: Router) {
|
||||||
|
/** 通用 */
|
||||||
|
setupCommonGuard(router);
|
||||||
|
/** 权限访问 */
|
||||||
|
setupAccessGuard(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createRouterGuard };
|
||||||
37
apps/web-antd/src/router/index.ts
Normal file
37
apps/web-antd/src/router/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
createWebHistory,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import { resetStaticRoutes } from '@vben/utils';
|
||||||
|
|
||||||
|
import { createRouterGuard } from './guard';
|
||||||
|
import { routes } from './routes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 创建vue-router实例
|
||||||
|
*/
|
||||||
|
const router = createRouter({
|
||||||
|
history:
|
||||||
|
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||||
|
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||||
|
: createWebHistory(import.meta.env.VITE_BASE),
|
||||||
|
// 应该添加到路由的初始路由列表。
|
||||||
|
routes,
|
||||||
|
scrollBehavior: (to, _from, savedPosition) => {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
}
|
||||||
|
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
|
||||||
|
},
|
||||||
|
// 是否应该禁止尾部斜杠。
|
||||||
|
// strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||||
|
|
||||||
|
// 创建路由守卫
|
||||||
|
createRouterGuard(router);
|
||||||
|
|
||||||
|
export { resetRoutes, router };
|
||||||
96
apps/web-antd/src/router/routes/core.ts
Normal file
96
apps/web-antd/src/router/routes/core.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||||
|
|
||||||
|
import { AuthPageLayout, BasicLayout } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import Login from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
|
/** 全局404页面 */
|
||||||
|
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||||
|
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||||
|
meta: {
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInTab: true,
|
||||||
|
title: '404',
|
||||||
|
},
|
||||||
|
name: 'FallbackNotFound',
|
||||||
|
path: '/:path(.*)*',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 基本路由,这些路由是必须存在的 */
|
||||||
|
const coreRoutes: RouteRecordRaw[] = [
|
||||||
|
/**
|
||||||
|
* 根路由
|
||||||
|
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||||
|
* 此路由必须存在,且不应修改
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
component: BasicLayout,
|
||||||
|
meta: {
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
title: 'Root',
|
||||||
|
},
|
||||||
|
name: 'Root',
|
||||||
|
path: '/',
|
||||||
|
redirect: DEFAULT_HOME_PATH,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: AuthPageLayout,
|
||||||
|
meta: {
|
||||||
|
hideInTab: true,
|
||||||
|
title: 'Authentication',
|
||||||
|
},
|
||||||
|
name: 'Authentication',
|
||||||
|
path: '/auth',
|
||||||
|
redirect: LOGIN_PATH,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Login',
|
||||||
|
path: 'login',
|
||||||
|
component: Login,
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.login'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CodeLogin',
|
||||||
|
path: 'code-login',
|
||||||
|
component: () => import('#/views/_core/authentication/code-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.codeLogin'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'QrCodeLogin',
|
||||||
|
path: 'qrcode-login',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/qrcode-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.qrcodeLogin'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ForgetPassword',
|
||||||
|
path: 'forget-password',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/forget-password.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.forgetPassword'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Register',
|
||||||
|
path: 'register',
|
||||||
|
component: () => import('#/views/_core/authentication/register.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.register'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { coreRoutes, fallbackNotFoundRoute };
|
||||||
37
apps/web-antd/src/router/routes/index.ts
Normal file
37
apps/web-antd/src/router/routes/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||||
|
|
||||||
|
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||||
|
|
||||||
|
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 有需要可以自行打开注释,并创建文件夹
|
||||||
|
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||||
|
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||||
|
|
||||||
|
/** 动态路由 */
|
||||||
|
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||||
|
|
||||||
|
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||||
|
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||||
|
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||||
|
const staticRoutes: RouteRecordRaw[] = [];
|
||||||
|
const externalRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
|
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||||
|
* 无需走权限验证(会一直显示在菜单中) */
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
...coreRoutes,
|
||||||
|
...externalRoutes,
|
||||||
|
fallbackNotFoundRoute,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||||
|
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||||
|
|
||||||
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
|
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||||
|
export { accessRoutes, coreRouteNames, routes };
|
||||||
38
apps/web-antd/src/router/routes/modules/dashboard.ts
Normal file
38
apps/web-antd/src/router/routes/modules/dashboard.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:layout-dashboard',
|
||||||
|
order: -1,
|
||||||
|
title: $t('page.dashboard.title'),
|
||||||
|
},
|
||||||
|
name: 'Dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Analytics',
|
||||||
|
path: '/analytics',
|
||||||
|
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||||
|
meta: {
|
||||||
|
affixTab: true,
|
||||||
|
icon: 'lucide:area-chart',
|
||||||
|
title: $t('page.dashboard.analytics'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Workspace',
|
||||||
|
path: '/workspace',
|
||||||
|
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: $t('page.dashboard.workspace'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
144
apps/web-antd/src/router/routes/modules/system.ts
Normal file
144
apps/web-antd/src/router/routes/modules/system.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { BasicLayout } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
component: BasicLayout,
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:layout-dashboard',
|
||||||
|
order: 1,
|
||||||
|
title: $t('abp.menu.system'),
|
||||||
|
authority: ['AbpIdentity'],
|
||||||
|
},
|
||||||
|
name: 'system',
|
||||||
|
path: '/system',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'abpUser',
|
||||||
|
path: 'user',
|
||||||
|
component: () => import('#/views/system/abpuser/index.vue'),
|
||||||
|
meta: {
|
||||||
|
// affixTab: true,
|
||||||
|
icon: 'ph:user',
|
||||||
|
title: $t('abp.menu.user'),
|
||||||
|
authority: ['AbpIdentity.Users'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'abpRole',
|
||||||
|
path: 'role',
|
||||||
|
component: () => import('#/views/system/abprole/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'oui:app-users-roles',
|
||||||
|
title: $t('abp.menu.role'),
|
||||||
|
authority: ['AbpIdentity.Roles'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OrganizationUnit',
|
||||||
|
path: 'organizationUnit',
|
||||||
|
component: () => import('#/views/system/abporganizationunit/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.organizationUnit'),
|
||||||
|
authority: ['AbpIdentity.OrganizationUnitManagement'],
|
||||||
|
icon: 'ant-design:team-outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'abpSetting',
|
||||||
|
path: 'setting',
|
||||||
|
component: () => import('#/views/system/abpsetting/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'uil:setting',
|
||||||
|
title: $t('abp.menu.setting'),
|
||||||
|
authority: ['AbpIdentity.Setting'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'abpfeature',
|
||||||
|
path: 'Feature',
|
||||||
|
component: () => import('#/views/system/abpfeature/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'ant-design:tool-outlined',
|
||||||
|
title: $t('abp.menu.feature'),
|
||||||
|
authority: ['AbpIdentity.FeatureManagement'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AbpAuditLog',
|
||||||
|
path: 'auditlog',
|
||||||
|
component: () => import('#/views/system/abplog/audit.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.auditLog'),
|
||||||
|
authority: ['AbpIdentity.AuditLog'],
|
||||||
|
icon: 'ant-design:snippets-twotone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AbpLoginLog',
|
||||||
|
path: 'loginlog',
|
||||||
|
component: () => import('#/views/system/abplog/login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.loginLog'),
|
||||||
|
authority: ['AbpIdentity.IdentitySecurityLogs'],
|
||||||
|
icon: 'ant-design:file-protect-outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AboLanguage',
|
||||||
|
path: 'language',
|
||||||
|
component: () => import('#/views/system/abplanguage/language.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.language'),
|
||||||
|
authority: ['AbpIdentity.Languages'],
|
||||||
|
icon: 'ant-design:read-outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AboLanguageText',
|
||||||
|
path: 'languagetext',
|
||||||
|
component: () => import('#/views/system/abplanguage/languagetext.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.languageText'),
|
||||||
|
authority: ['AbpIdentity.LanguageTexts'],
|
||||||
|
icon: 'ant-design:font-size-outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DataDictionary',
|
||||||
|
path: 'data-dictionary',
|
||||||
|
component: () => import('#/views/system/abpdatadictionary/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.dataDictionary'),
|
||||||
|
authority: ['AbpIdentity.DataDictionaryManagement'],
|
||||||
|
icon: 'ant-design:table-outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AbpNotification',
|
||||||
|
path: 'notification',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/system/abpnotification/notification.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.notification'),
|
||||||
|
authority: ['AbpIdentity.NotificationSubscriptionManagement'],
|
||||||
|
icon: 'ant-design:comment-outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AbpMessage',
|
||||||
|
path: 'message',
|
||||||
|
component: () => import('#/views/system/abpnotification/message.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('abp.menu.message'),
|
||||||
|
authority: ['AbpIdentity.NotificationManagement'],
|
||||||
|
icon: 'ant-design:customer-service-twotone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
32
apps/web-antd/src/router/routes/modules/tenant.ts
Normal file
32
apps/web-antd/src/router/routes/modules/tenant.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { BasicLayout } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
component: BasicLayout,
|
||||||
|
meta: {
|
||||||
|
icon: 'ant-design:switcher-filled',
|
||||||
|
order: 2,
|
||||||
|
title: $t('abp.menu.tenant'),
|
||||||
|
authority: ['AbpTenantManagement'],
|
||||||
|
},
|
||||||
|
name: 'tenant',
|
||||||
|
path: '/tenant',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'abpTenant',
|
||||||
|
path: 'page',
|
||||||
|
component: () => import('#/views/system/abptenant/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'ph:user',
|
||||||
|
title: $t('abp.menu.tenantList'),
|
||||||
|
authority: ['AbpTenantManagement.Tenants'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
152
apps/web-antd/src/store/auth.ts
Normal file
152
apps/web-antd/src/store/auth.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import type { Recordable, UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message as Message, notification } from 'ant-design-vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ApplicationAuthConfigurationDto,
|
||||||
|
type ApplicationConfigurationDto,
|
||||||
|
getApiAbpApplicationConfiguration,
|
||||||
|
postApiAppAccountLogin,
|
||||||
|
postTenantsFind,
|
||||||
|
} from '#/api-client';
|
||||||
|
import { useSignalR } from '#/hooks/useSignalR';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loginLoading = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步处理登录操作
|
||||||
|
* Asynchronously handle the login process
|
||||||
|
* @param params 登录表单数据
|
||||||
|
*/
|
||||||
|
async function authLogin(
|
||||||
|
params: Recordable<any>,
|
||||||
|
onSuccess?: () => Promise<void> | void,
|
||||||
|
) {
|
||||||
|
// 异步处理用户登录操作并获取 accessToken
|
||||||
|
let userInfo: null | UserInfo = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loginLoading.value = true;
|
||||||
|
// 判断是否租户登录
|
||||||
|
if (params.tenant) {
|
||||||
|
const tenantResult = await postTenantsFind({
|
||||||
|
body: {
|
||||||
|
name: params.tenant,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tenantResult.data?.success) {
|
||||||
|
userStore.setTenantInfo(tenantResult.data as any);
|
||||||
|
} else {
|
||||||
|
Message.error(`${params.tenant}$t('abp.tenant.notExist')`);
|
||||||
|
userStore.setTenantInfo(null);
|
||||||
|
delete params.tenant;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data = {} } = await postApiAppAccountLogin({
|
||||||
|
body: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 如果成功获取到 accessToken
|
||||||
|
if (data.token) {
|
||||||
|
accessStore.setAccessToken(data.token);
|
||||||
|
userInfo = data as any;
|
||||||
|
userStore.setUserInfo(userInfo as any);
|
||||||
|
await getApplicationConfiguration();
|
||||||
|
if (accessStore.loginExpired) {
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
} else {
|
||||||
|
onSuccess
|
||||||
|
? await onSuccess?.()
|
||||||
|
: await router.push(DEFAULT_HOME_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInfo?.userName) {
|
||||||
|
notification.success({
|
||||||
|
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.userName}`,
|
||||||
|
duration: 3,
|
||||||
|
message: $t('authentication.loginSuccess'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
userStore.setTenantInfo(null);
|
||||||
|
userStore.setUserInfo(null);
|
||||||
|
} finally {
|
||||||
|
loginLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(redirect: boolean = true) {
|
||||||
|
try {
|
||||||
|
// await logoutApi();
|
||||||
|
const { closeConnect } = useSignalR();
|
||||||
|
closeConnect();
|
||||||
|
} catch {
|
||||||
|
// 不做任何处理
|
||||||
|
}
|
||||||
|
resetAllStores();
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
|
||||||
|
// 回登录页带上当前路由地址
|
||||||
|
await router.replace({
|
||||||
|
path: LOGIN_PATH,
|
||||||
|
query: redirect
|
||||||
|
? {
|
||||||
|
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
const userInfo: null | UserInfo = null;
|
||||||
|
// userInfo = await getUserInfoApi();
|
||||||
|
// userStore.setUserInfo(userInfo);
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $reset() {
|
||||||
|
loginLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getApplicationConfiguration() {
|
||||||
|
const { data: authData } = await getApiAbpApplicationConfiguration({
|
||||||
|
query: { IncludeLocalizationResources: false },
|
||||||
|
});
|
||||||
|
const { auth } = authData as ApplicationConfigurationDto;
|
||||||
|
const accessCodes = Object.keys(
|
||||||
|
(auth as ApplicationAuthConfigurationDto)
|
||||||
|
.grantedPolicies as unknown as Record<string, any>,
|
||||||
|
);
|
||||||
|
accessStore.setAccessCodes(accessCodes);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
$reset,
|
||||||
|
authLogin,
|
||||||
|
fetchUserInfo,
|
||||||
|
loginLoading,
|
||||||
|
logout,
|
||||||
|
getApplicationConfiguration,
|
||||||
|
};
|
||||||
|
});
|
||||||
1
apps/web-antd/src/store/index.ts
Normal file
1
apps/web-antd/src/store/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './auth';
|
||||||
3
apps/web-antd/src/views/_core/README.md
Normal file
3
apps/web-antd/src/views/_core/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# \_core
|
||||||
|
|
||||||
|
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||||
9
apps/web-antd/src/views/_core/about/index.vue
Normal file
9
apps/web-antd/src/views/_core/about/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { About } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'About' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<About />
|
||||||
|
</template>
|
||||||
69
apps/web-antd/src/views/_core/authentication/code-login.vue
Normal file
69
apps/web-antd/src/views/_core/authentication/code-login.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.mobile'),
|
||||||
|
},
|
||||||
|
fieldName: 'phoneNumber',
|
||||||
|
label: $t('authentication.mobile'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.mobileTip') })
|
||||||
|
.refine((v) => /^\d{11}$/.test(v), {
|
||||||
|
message: $t('authentication.mobileErrortip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenPinInput',
|
||||||
|
componentProps: {
|
||||||
|
codeLength: CODE_LENGTH,
|
||||||
|
createText: (countdown: number) => {
|
||||||
|
const text =
|
||||||
|
countdown > 0
|
||||||
|
? $t('authentication.sendText', [countdown])
|
||||||
|
: $t('authentication.sendCode');
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
placeholder: $t('authentication.code'),
|
||||||
|
},
|
||||||
|
fieldName: 'code',
|
||||||
|
label: $t('authentication.code'),
|
||||||
|
rules: z.string().length(CODE_LENGTH, {
|
||||||
|
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 异步处理登录操作
|
||||||
|
* Asynchronously handle the login process
|
||||||
|
* @param values 登录表单数据
|
||||||
|
*/
|
||||||
|
async function handleLogin(values: Recordable<any>) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(values);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationCodeLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleLogin"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'example@example.com',
|
||||||
|
},
|
||||||
|
fieldName: 'email',
|
||||||
|
label: $t('authentication.email'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.emailTip') })
|
||||||
|
.email($t('authentication.emailValidErrorTip')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(value: Recordable<any>) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('reset email:', value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationForgetPassword
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
76
apps/web-antd/src/views/_core/authentication/login.vue
Normal file
76
apps/web-antd/src/views/_core/authentication/login.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { computed, onBeforeMount, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { getApiAbpApplicationConfiguration } from '#/api-client/index';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const showThirdPartyLogin = ref(false);
|
||||||
|
const thirdPartLoginList = ref([]);
|
||||||
|
const showTenant = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('abp.login.selectTenant'),
|
||||||
|
},
|
||||||
|
fieldName: 'tenant',
|
||||||
|
label: $t('abp.tenant.tenant'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.usernameTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
label: $t('authentication.username'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.usernameTip') })
|
||||||
|
.default('admin'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
|
.default('1q2w3E*'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
const result = await getApiAbpApplicationConfiguration();
|
||||||
|
showThirdPartyLogin.value = result.data?.oidcConfiguration
|
||||||
|
?.enabled as boolean;
|
||||||
|
thirdPartLoginList.value = result.data?.oidcConfiguration
|
||||||
|
?.oidcConfiguration as [];
|
||||||
|
// 是否启用多租户
|
||||||
|
showTenant.value = result.data?.multiTenancy?.isEnabled ?? false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="authStore.loginLoading"
|
||||||
|
:show-tenant-login="showTenant"
|
||||||
|
:show-third-party-login="showThirdPartyLogin"
|
||||||
|
:third-part-login-list="thirdPartLoginList as any"
|
||||||
|
@submit="authStore.authLogin"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||||
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
|
||||||
|
defineOptions({ name: 'QrCodeLogin' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||||
|
</template>
|
||||||
96
apps/web-antd/src/views/_core/authentication/register.vue
Normal file
96
apps/web-antd/src/views/_core/authentication/register.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.usernameTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'username',
|
||||||
|
label: $t('authentication.username'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
renderComponentContent() {
|
||||||
|
return {
|
||||||
|
strengthText: () => $t('authentication.passwordStrength'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { password } = values;
|
||||||
|
return z
|
||||||
|
.string({ required_error: $t('authentication.passwordTip') })
|
||||||
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
|
.refine((value) => value === password, {
|
||||||
|
message: $t('authentication.confirmPasswordTip'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['password'],
|
||||||
|
},
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenCheckbox',
|
||||||
|
fieldName: 'agreePolicy',
|
||||||
|
renderComponentContent: () => ({
|
||||||
|
default: () =>
|
||||||
|
h('span', [
|
||||||
|
$t('authentication.agree'),
|
||||||
|
h(
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: 'vben-link ml-1 ',
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
rules: z.boolean().refine((value) => !!value, {
|
||||||
|
message: $t('authentication.agreeTip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(value: Recordable<any>) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('register submit:', value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationRegister
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
7
apps/web-antd/src/views/_core/fallback/coming-soon.vue
Normal file
7
apps/web-antd/src/views/_core/fallback/coming-soon.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="coming-soon" />
|
||||||
|
</template>
|
||||||
9
apps/web-antd/src/views/_core/fallback/forbidden.vue
Normal file
9
apps/web-antd/src/views/_core/fallback/forbidden.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback403Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="403" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback500Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="500" />
|
||||||
|
</template>
|
||||||
9
apps/web-antd/src/views/_core/fallback/not-found.vue
Normal file
9
apps/web-antd/src/views/_core/fallback/not-found.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback404Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="404" />
|
||||||
|
</template>
|
||||||
9
apps/web-antd/src/views/_core/fallback/offline.vue
Normal file
9
apps/web-antd/src/views/_core/fallback/offline.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="offline" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
grid: {
|
||||||
|
bottom: 0,
|
||||||
|
containLabel: true,
|
||||||
|
left: '1%',
|
||||||
|
right: '1%',
|
||||||
|
top: '2 %',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
areaStyle: {},
|
||||||
|
data: [
|
||||||
|
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||||
|
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||||
|
111,
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5ab1ef',
|
||||||
|
},
|
||||||
|
smooth: true,
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
areaStyle: {},
|
||||||
|
data: [
|
||||||
|
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||||
|
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
},
|
||||||
|
smooth: true,
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
// xAxis: {
|
||||||
|
// axisTick: {
|
||||||
|
// show: false,
|
||||||
|
// },
|
||||||
|
// boundaryGap: false,
|
||||||
|
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||||
|
// type: 'category',
|
||||||
|
// },
|
||||||
|
xAxis: {
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
boundaryGap: false,
|
||||||
|
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'solid',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
type: 'category',
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
max: 80_000,
|
||||||
|
splitArea: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
splitNumber: 4,
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
legend: {
|
||||||
|
bottom: 0,
|
||||||
|
data: ['访问', '趋势'],
|
||||||
|
},
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{
|
||||||
|
name: '网页',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '移动端',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ipad',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '客户端',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '第三方',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '其它',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
radius: '60%',
|
||||||
|
splitNumber: 8,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 1,
|
||||||
|
shadowBlur: 0,
|
||||||
|
shadowColor: 'rgba(0,0,0,.2)',
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowOffsetY: 10,
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
itemStyle: {
|
||||||
|
color: '#b6a2de',
|
||||||
|
},
|
||||||
|
name: '访问',
|
||||||
|
value: [90, 50, 86, 40, 50, 20],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5ab1ef',
|
||||||
|
},
|
||||||
|
name: '趋势',
|
||||||
|
value: [70, 75, 70, 76, 20, 85],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
// borderColor: '#fff',
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
symbolSize: 0,
|
||||||
|
type: 'radar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user