修改结构

This commit is contained in:
ChenYi 2025-05-27 19:31:37 +08:00
parent fed3ce1a01
commit b9bfa5f398
1069 changed files with 109962 additions and 0 deletions

4
.browserslistrc Normal file
View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

1
.commitlintrc.js Normal file
View File

@ -0,0 +1 @@
export { default } from '@vben/commitlint-config';

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.git
.gitignore
*.md
dist
.turbo
dist.zip

18
.editorconfig Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
[core]
ignorecase = false

56
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,6 @@
ports:
- port: 5555
onOpen: open-preview
tasks:
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play

69
.husky/_/commit-msg Normal file
View File

@ -0,0 +1,69 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook.exe -h >/dev/null 2>&1
then
lefthook.exe "$@"
elif lefthook.bat -h >/dev/null 2>&1
then
lefthook.bat "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif go tool lefthook -h >/dev/null 2>&1
then
go tool lefthook "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package lefthook >/dev/null 2>&1
then
swift package --build-path .build/lefthook --disable-sandbox lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
elif uv run lefthook -h >/dev/null 2>&1
then
uv run lefthook "$@"
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "commit-msg" "$@"

69
.husky/_/post-merge Normal file
View File

@ -0,0 +1,69 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook.exe -h >/dev/null 2>&1
then
lefthook.exe "$@"
elif lefthook.bat -h >/dev/null 2>&1
then
lefthook.bat "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif go tool lefthook -h >/dev/null 2>&1
then
go tool lefthook "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package lefthook >/dev/null 2>&1
then
swift package --build-path .build/lefthook --disable-sandbox lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
elif uv run lefthook -h >/dev/null 2>&1
then
uv run lefthook "$@"
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "post-merge" "$@"

69
.husky/_/pre-commit Normal file
View File

@ -0,0 +1,69 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook.exe -h >/dev/null 2>&1
then
lefthook.exe "$@"
elif lefthook.bat -h >/dev/null 2>&1
then
lefthook.bat "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif go tool lefthook -h >/dev/null 2>&1
then
go tool lefthook "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package lefthook >/dev/null 2>&1
then
swift package --build-path .build/lefthook --disable-sandbox lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
elif uv run lefthook -h >/dev/null 2>&1
then
uv run lefthook "$@"
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "pre-commit" "$@"

View File

@ -0,0 +1,69 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook.exe -h >/dev/null 2>&1
then
lefthook.exe "$@"
elif lefthook.bat -h >/dev/null 2>&1
then
lefthook.bat "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook.exe" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook.exe" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif go tool lefthook -h >/dev/null 2>&1
then
go tool lefthook "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package lefthook >/dev/null 2>&1
then
swift package --build-path .build/lefthook --disable-sandbox lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
elif uv run lefthook -h >/dev/null 2>&1
then
uv run lefthook "$@"
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "prepare-commit-msg" "$@"

1
.node-version Normal file
View File

@ -0,0 +1 @@
22.1.0

13
.npmrc Normal file
View File

@ -0,0 +1,13 @@
registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=lefthook
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
View 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
View File

@ -0,0 +1 @@
export { default } from '@vben/prettier-config';

4
.stylelintignore Normal file
View File

@ -0,0 +1,4 @@
dist
public
__tests__
coverage

30
.vscode/extensions.json vendored Normal file
View 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
View 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
View 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"
}
]
}

241
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,241 @@
{
"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": true,
"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,
"**/.vitepress": 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
},
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
// 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,lefthook.yml",
"tailwind.config.mjs": "postcss.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false,
"cSpell.words": [
"archiver",
"axios",
"dotenv",
"isequal",
"jspm",
"napi",
"nolebase",
"rollup",
"vitest"
]
}

44
Dockerfile Normal file
View File

@ -0,0 +1,44 @@
FROM node:20-slim 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;"]

75
_nginx/nginx.conf Normal file
View 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;
}
}
}

8
apps/web-antd/.env Normal file
View File

@ -0,0 +1,8 @@
# 应用标题
VITE_APP_TITLE=采集端综合管理
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=abp-vnext-pro-vben5-antd
# 对store进行加密的密钥在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key

View File

@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true

View File

@ -0,0 +1,37 @@
# 端口号
VITE_PORT=4200
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=/api
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=true
# vue-router 的模式
VITE_ROUTER_HISTORY=history
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 是否打开刷新浏览器检查用户角色信息
VITE_REFRESH_ROLE = true
# # # 后端接口地址
# VITE_APP_API_ADDRESS=http://182.43.18.151:44317
# # websocket地址
# VITE_WEBSOCKET_URL=http://182.43.18.151:44317/signalr/notification
# 后端接口地址
VITE_APP_API_ADDRESS=http://localhost:44315
# websocket地址
VITE_WEBSOCKET_URL=http://localhost:44315/signalr/notification

View 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://182.43.18.151:44317
# websocket地址
VITE_WEBSOCKET_URL=http://182.43.18.151:44317/signalr/notification

35
apps/web-antd/index.html Normal file
View 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>

View File

@ -0,0 +1,62 @@
{
"name": "@vben/web-antd",
"version": "5.5.6",
"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": {
"@iconify/json": "^2.2.282",
"@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"
}
}

View File

@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,218 @@
/**
* 使 adapter/form 使便使
* vben-formvben-modalvben-drawer 使,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import {
defineAsyncComponent,
defineComponent,
getCurrentInstance,
h,
ref,
} from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
componentProps: Recordable<any> = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
const publicApi: Recordable<any> = {};
expose(publicApi);
const instance = getCurrentInstance();
instance?.proxy?.$nextTick(() => {
for (const key in innerRef.value) {
if (typeof innerRef.value[key] === 'function') {
publicApi[key] = innerRef.value[key];
}
}
});
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
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: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
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 };

View 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 };

View File

@ -0,0 +1,72 @@
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,
},
sortConfig: {
remote: true,
},
proxyConfig: {
sort: true, // 启用排序请求代理
autoLoad: true,
response: {
result: 'items',
total: 'totalCount',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
stripe: 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';

View 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/AbpPro/swagger.json',
output: 'src/api-client',
});

View File

@ -0,0 +1,143 @@
import { useAccessStore, useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue';
import { postApiAppAccountRefreshToken } from '#/api-client';
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,
});
// 是否正在刷新token
let isRefreshing = false;
// 刷新token队列
let refreshTokenQueue: ((token: string) => void)[] = [];
client.instance.interceptors.request.use((request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore();
const accessStore = useAccessStore();
const token = accessStore.getAccessToken();
// 设置请求头
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 (request.headers) {
request.headers.Authorization = `Bearer ${token}`;
}
return request;
});
client.instance.interceptors.response.use(
(response) => {
return Promise.resolve(response);
},
async (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');
const { config } = error;
const originalRequest = config;
if (isRefreshing) {
return new Promise((resolve) => {
refreshTokenQueue.push((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(client.request(originalRequest));
});
});
} else {
isRefreshing = true;
try {
const newToken = await refreshTokenAsync();
// 处理队列中的请求
refreshTokenQueue.forEach((callback) => callback(newToken));
// 清空队列
refreshTokenQueue = [];
return client.request(originalRequest);
} catch (refreshError) {
// 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面)
message = $t('common.mesage401');
refreshTokenQueue = [];
const authStore = useAuthStore();
authStore.logout();
console.error(refreshError);
} finally {
isRefreshing = false;
}
}
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);
throw error;
},
);
async function refreshTokenAsync(): Promise<string> {
try {
const userStore = useUserStore();
const accessStore = useAccessStore();
const refreshToken = accessStore.getRefreshToken();
if (!refreshToken) return '';
const res = await postApiAppAccountRefreshToken({
body: {
userId: userStore.userInfo?.id,
refreshToken,
},
});
if (res?.data?.success) {
accessStore.setAccessToken(res.data.token as string);
accessStore.setRefreshToken(res.data.refreshToken as string);
return res.data.token as string;
} else {
throw new Error('get refreshToken error');
}
} catch {
throw new Error($t('common.mesage401'));
}
}
export default client;

View 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';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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');
}

View File

@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

View File

@ -0,0 +1,39 @@
/**
*
*/
export async function getAllMenusApi() {
const dashboardMenus = [
{
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/dashboard',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
title: 'page.dashboard.analytics',
authority: ['FileManagement.File1'],
},
},
{
name: 'Workspace',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
title: 'page.dashboard.workspace',
},
},
],
},
];
debugger;
return dashboardMenus;
// return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View 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');
}

View File

@ -0,0 +1 @@
export * from './core';

View 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
View 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>

View File

@ -0,0 +1,81 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
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 '#/hooks/useLoadIcon';
import 'vue3-json-viewer/dist/index.css';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置 json-viewer
app.use(JsonViewer);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
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 };

View File

@ -0,0 +1,4 @@
export { createLoading } from './src/createLoading';
export { default as Loading } from './src/Loading.vue';
export { useLoading } from './src/useLoading';

View 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>

View 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;
},
};
}

View 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';
}

View File

@ -0,0 +1,62 @@
import type { Ref } from 'vue';
import type { LoadingProps } from './typing';
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];
}

View 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>

View File

@ -0,0 +1 @@
export { default as Icon } from './icon.vue';

View File

@ -0,0 +1,2 @@
export { default as TableAction } from './table-action.vue';
export type * from './types.d.ts';

View File

@ -0,0 +1,227 @@
<script setup lang="ts">
import type { ButtonType } from 'ant-design-vue/es/button';
import type { PropType } from 'vue';
import type { ActionItem, PopConfirm } from './types';
import { computed, 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>

View 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;
}

View File

@ -0,0 +1,23 @@
import { addCollection } from '@vben/icons';
import AntDesignIcons from '@iconify/json/json/ant-design.json';
import CarbonIcons from '@iconify/json/json/carbon.json';
import EpIcons from '@iconify/json/json/ep.json';
import IcIcons from '@iconify/json/json/ic.json';
import LogosIcons from '@iconify/json/json/logos.json';
import LucideIcons from '@iconify/json/json/lucide.json';
import MdiIcons from '@iconify/json/json/mdi.json';
import OuiIcons from '@iconify/json/json/oui.json';
import PhosphorIcons from '@iconify/json/json/ph.json';
import UnIcons from '@iconify/json/json/uil.json';
addCollection(AntDesignIcons);
addCollection(LucideIcons);
addCollection(CarbonIcons);
addCollection(IcIcons as any);
addCollection(LogosIcons as any);
addCollection(PhosphorIcons as any);
addCollection(UnIcons);
addCollection(OuiIcons);
addCollection(MdiIcons);
addCollection(EpIcons);

View File

@ -0,0 +1,127 @@
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 {
const userStore = useUserStore();
if (userStore.checkUserLoginExpire()) {
console.debug('未检测到用户信息,登录之后才会链接SignalR.');
return;
}
connectionsignalR();
await connection.start();
} catch (error) {
console.error(error);
setTimeout(() => startConnect(), 5000);
}
}
/**
* SignalR连接
*/
function closeConnect(): void {
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 };
}

View 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,
},
showOverflow: 'title',
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>

View 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>

View File

@ -0,0 +1,281 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { useEventbus, 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,
postUsersNeedChangePassword,
} 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 ChangePassword from './change-password.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 changePasswordTitle = ref<string>($t('abp.user.changePassword'));
const [ChangePasswordModal, changePasswordModalApi] = useVbenModal({
draggable: false,
// centered: true,
showCancelButton: false,
showConfirmButton: false,
closeOnPressEscape: false,
closeOnClickModal: false,
closable: false,
fullscreenButton: false,
title: changePasswordTitle.value,
onConfirm: () => {},
onBeforeClose: () => true,
});
// const router = useRouter();
// const { refresh } = useRefresh();
onMounted(async () => {
// if (userStore.checkUserLoginExpire()) {
// //
// await router.replace({
// path: LOGIN_PATH,
// query: {
// redirect: encodeURIComponent(router.currentRoute.value.fullPath),
// },
// });
// return;
// }
const result = await postUsersNeedChangePassword();
if (result.data?.needChangePassword) {
changePasswordTitle.value = result.data.message as string;
changePasswordModalApi.open();
}
// 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();
eventbus.clear();
});
const { destroyWatermark, updateWatermark } = useWatermark();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const [MyProfileModal, myProfileModalApi] = useVbenModal({
draggable: true,
onConfirm: () => {},
onBeforeClose: () => true,
});
const [NotifyItemModal, notifyItemModalApi] = useVbenModal({
draggable: true,
onConfirm: () => {},
onBeforeClose: () => true,
});
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>
<ChangePasswordModal>
<ChangePassword
:message="changePasswordTitle"
@close="changePasswordModalApi.close"
/>
</ChangePasswordModal>
</template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { defineProps } from 'vue';
import { z } from '@vben/common-ui';
import { message as Message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { postUsersChangePassword } from '#/api-client';
import { $t } from '#/locales';
interface Props {
message: string;
}
defineOptions({
name: 'ChangePassword',
});
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{
close: any;
}>();
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('abp.user.changePassword') + $t('common.success'));
await resetPasswordApi.resetForm();
emit('close');
},
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',
});
</script>
<template>
<div>
<div
class="flex w-full justify-center"
style="margin-bottom: 20px; color: red"
>
{{ message }}
</div>
<ResetPasswordForm />
</div>
</template>

View 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 };

View File

@ -0,0 +1,403 @@
<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 {
postUsersCanUseTwoFactor,
postUsersChangePassword,
postUsersDisabledTwoFactor,
postUsersEnabledTwoFactor,
postUsersGetQrCode,
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',
});
const [EnabledTwoFactorForm, enabledTwoFactorApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'vertical',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.code'),
},
fieldName: 'code',
label: $t('abp.user.code'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.code'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
wrapperClass: 'grid-cols-1',
});
const [DisabledTwoFactorForm, disabledTwoFactorApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.code'),
},
fieldName: 'code',
label: $t('abp.user.code'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.code'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
wrapperClass: 'grid-cols-1',
});
onMounted(async () => {
try {
loading.value = true;
const resp = await postUsersMyProfile();
await profileFormApi.setValues({ ...resp.data });
} finally {
loading.value = false;
}
});
const currentTab = ref(0);
const qrCode = ref('data:image/png;base64,');
const secret = ref(''); //
const enableTwoFactor = ref(false); //
async function activeChange(e: any) {
if (e !== 2) return;
loading.value = true;
//
const canUseTwoFactor = await postUsersCanUseTwoFactor();
enableTwoFactor.value = canUseTwoFactor.data as boolean;
if (!enableTwoFactor.value) {
//
const qrCodeRes = await postUsersGetQrCode();
qrCode.value = `data:image/png;base64,${qrCodeRes.data?.qrCode}` as string;
secret.value = qrCodeRes.data?.secret as string;
}
loading.value = false;
}
/**
* 开启双因素验证
*/
async function enabledTwoFactor() {
try {
loading.value = true;
const validResult = await enabledTwoFactorApi.validate();
if (!validResult.valid) {
Message.warning($t('common.pleaseInput') + $t('abp.user.code'));
return;
}
const formValues = await enabledTwoFactorApi.getValues();
await postUsersEnabledTwoFactor({
body: {
...formValues,
secret: secret.value,
},
});
enableTwoFactor.value = true;
Message.info($t('abp.user.twoFactorEnabled'));
} finally {
await enabledTwoFactorApi.resetForm();
loading.value = false;
}
}
/**
* 关闭双因素验证
*/
async function disabledTwoFactor() {
try {
loading.value = true;
const validResult = await disabledTwoFactorApi.validate();
if (!validResult.valid) {
Message.warning($t('common.pleaseInput') + $t('abp.user.code'));
return;
}
const formValues = await disabledTwoFactorApi.getValues();
await postUsersDisabledTwoFactor({
body: {
...formValues,
},
});
Message.info($t('abp.user.twoFactorDisabled'));
} finally {
await disabledTwoFactorApi.resetForm();
enableTwoFactor.value = false;
loading.value = false;
await activeChange(2);
}
}
</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>
<TabPane :key="2" :tab="$t('abp.user.twoFactor')">
<div v-show="enableTwoFactor">
<div style="margin-bottom: 5%">
{{ $t('abp.user.twoFactorEnabledDesc') }}
</div>
<DisabledTwoFactorForm />
<Button
style="margin-left: 72%"
type="primary"
@click="disabledTwoFactor"
>
{{ $t('common.disabled') }}
</Button>
</div>
<div v-show="!enableTwoFactor" class="mx-auto max-w-lg">
<Steps :current="currentTab" class="steps">
<Step title="Authenticator" />
<Step :title="$t('abp.user.verifyAuthenticator')" />
</Steps>
<div style="margin-top: 30px; margin-left: 20px">
<Row>
<Col :span="12">
<Image :src="qrCode" :width="200" />
</Col>
<Col :span="12">
<div>
{{ $t('abp.user.twoFactorDesc') }}
<EnabledTwoFactorForm />
<Button
style="margin-left: 55%"
type="primary"
@click="enabledTwoFactor"
>
{{ $t('common.enabled') }}
</Button>
</div>
</Col>
</Row>
</div>
</div>
</TabPane>
</Tabs>
</div>
</Spin>
</div>
</template>

View File

@ -0,0 +1,3 @@
# locale
每个app使用的国际化可能不同这里用于扩展国际化的功能例如扩展 dayjs、antd组件库的多语言切换以及app本身的国际化文件。

View 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 };

View File

@ -0,0 +1,163 @@
{
"login": {
"selectTenant": "Please select Tenant and ignore the non-tenant mode",
"inputCode": "Please enter the two-factor authentication code. If two-factor authentication has not been enabled for your account, please ignore this message.",
"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",
"file": "FileManagement",
"menu": "MenuManagement"
},
"user": {
"user": "User",
"userName": "UserName",
"name": "Name",
"surname": "Surname",
"email": "Email",
"phone": "Phone",
"password": "Password",
"currentPassword": "CurrentPassword",
"newPassword": "NewPassword",
"confirmNewPassword": "ConfirmNewPassword",
"changePassword": "ChangePassword",
"resetPassword": "ResetPassword",
"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"
},
"file": {
"file": "File",
"name": "FileName",
"size": "Size",
"contentType": "ContentType"
},
"dynamicMenu": {
"name": "RouteName",
"title": "Title",
"displayTitle": "DisplayTitle",
"path": "Path",
"component": "Component",
"policy": "Policy",
"menuType": "MenuType",
"enabled": "Enabled",
"hideInMenu": "HideInMenu",
"keepAlive": "keepAlive",
"order": "Order",
"openType": "OpenType",
"parentId": "Parent",
"icon": "Icon",
"url": "Url",
"menu": "Menu",
"folder": "Folder",
"componentType": "Component",
"internalLink": "InternalLink",
"externalLink": "ExternalLink"
}
}

View 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"
}

View File

@ -0,0 +1,41 @@
{
"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",
"upload": "Upload",
"download": "Download"
}

View File

@ -0,0 +1,12 @@
{
"title": "Demos",
"antd": "Ant Design Vue",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}

View 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"
}
}

View File

@ -0,0 +1,8 @@
{
"templateManagement": "TemplateManagement",
"templateList": "TemplateList",
"name": "Name",
"code": "Code",
"content": "Content",
"cultureName": "Language"
}

View File

@ -0,0 +1,163 @@
{
"login": {
"selectTenant": "请选择租户,非租户模式请忽略",
"inputCode": "请输入双因素验证码,如果账户没有开启双因素验证请忽略",
"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": "预览",
"file": "文件管理",
"menu": "菜单管理"
},
"user": {
"user": "用户",
"userName": "用户名",
"name": "名称",
"surname": "姓氏",
"email": "邮箱",
"phone": "手机号",
"password": "密码",
"currentPassword": "当前密码",
"newPassword": "新密码",
"confirmNewPassword": "确认新密码",
"changePassword": "修改密码",
"resetPassword": "重置密码",
"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": "连接字符串"
},
"file": {
"file": "文件",
"name": "文件名称",
"size": "文件大小",
"contentType": "文件类型"
},
"dynamicMenu": {
"name": "路由名称",
"title": "标题",
"displayTitle": "标题(多语言)",
"path": "路由地址",
"component": "组件地址",
"policy": "授权策略",
"menuType": "菜单类型",
"enabled": "是否启用",
"hideInMenu": "是否隐藏",
"keepAlive": "是否缓存",
"order": "排序",
"openType": "打开类型",
"parentId": "所属上级",
"icon": "图标",
"url": "内外链地址",
"menu": "菜单",
"folder": "目录",
"componentType": "组件",
"internalLink": "内链",
"externalLink": "外链"
}
}

View 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": "数据类型"
}

View File

@ -0,0 +1,41 @@
{
"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": "验证",
"upload": "上传",
"download": "下载"
}

View File

@ -0,0 +1,12 @@
{
"title": "演示",
"antd": "Ant Design Vue",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}

View File

@ -0,0 +1,15 @@
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码",
"thirdPartyLogin": "第三方登录"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
}
}

View File

@ -0,0 +1,8 @@
{
"templateManagement": "模板管理",
"templateList": "模板列表",
"name": "名称",
"code": "编码",
"content": "内容",
"cultureName": "语言"
}

34
apps/web-antd/src/main.ts Normal file
View File

@ -0,0 +1,34 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
// eslint-disable-next-line unused-imports/no-unused-imports
import client from '#/api-client-config/index';
import { overridesPreferences } from './preferences';
/**
*
*/
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();

View File

@ -0,0 +1,28 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description
* 使
* !!!
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
// 是否开启检查更新
enableCheckUpdates: false,
// 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1,
// accessMode: 'backend', // 默认值frontend|backend 默认值frontend可不填写
defaultAvatar: '/public/avatar-v1.webp', // 默认头像
},
theme: {
mode: 'light',
},
copyright: {
companyName: 'Abp Vben5 Antd',
},
logo: {
source: '/logo-v1.webp', // 网站图标
},
});

View File

@ -0,0 +1,43 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
// import { message } from 'ant-design-vue';
import { postMenusUserMenu } from '#/api-client/index';
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,
// });
const resp = await postMenusUserMenu();
return resp.data as any;
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@ -0,0 +1,138 @@
import type { Router } from 'vue-router';
import { 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 ||
preferences.app.defaultHomePath,
);
}
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 === preferences.app.defaultHomePath
? {}
: { 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 === preferences.app.defaultHomePath
? userStore.userInfo?.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
*
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View 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 };

View File

@ -0,0 +1,105 @@
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.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: preferences.app.defaultHomePath,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
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: 'OidcLogin',
path: 'oidc-login',
component: () => import('#/views/_core/authentication/oidc-login.vue'),
meta: {
title: $t('page.auth.thirdPartyLogin'),
},
},
{
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 };

View 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 };

View File

@ -0,0 +1,82 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ant-design:format-painter-filled',
order: 3,
title: $t('abp.menu.code'),
authority: ['AbpCodeManagement'],
},
name: 'code',
path: '/code',
children: [
{
name: 'project',
path: 'project',
component: () => import('#/views/code/project/index.vue'),
meta: {
icon: 'ant-design:profile-outlined',
title: $t('abp.menu.code-project'),
authority: ['AbpCodeManagement.Project'],
},
},
{
name: 'template',
path: 'template',
component: () => import('#/views/code/template/index.vue'),
meta: {
icon: 'ant-design:file-markdown-filled',
title: $t('abp.menu.code-template'),
authority: ['AbpCodeManagement.Template'],
},
},
{
name: 'generate',
path: 'generate',
component: () => import('#/views/code/generate/index.vue'),
meta: {
icon: 'ant-design:copyright-circle-filled',
title: $t('abp.menu.code-genarate'),
authority: ['AbpCodeManagement.Generator'],
},
},
{
name: 'EntityModel',
path: 'entityModel',
component: () => import('#/views/code/project/entityModel/index.vue'),
meta: {
icon: 'ant-design:file-markdown-filled',
title: $t('abp.menu.code-entity'),
hideInMenu: true,
},
},
{
name: 'TemplateDetail',
path: 'templateDetail',
component: () => import('#/views/code/template/TemplateDetail.vue'),
meta: {
icon: 'ant-design:file-markdown-filled',
title: $t('abp.menu.code-template-detail'),
hideInMenu: true,
},
},
{
name: 'preview',
path: 'Preview',
component: () => import('#/views/code/generate/preview.vue'),
meta: {
icon: 'ant-design:file-markdown-filled',
title: $t('abp.menu.code-Preview'),
hideInMenu: true,
},
},
],
},
];
export default routes;

View 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;

View File

@ -0,0 +1,41 @@
import type { RouteRecordRaw } from 'vue-router';
import { SvgAntdvLogoIcon } from '@vben/icons';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
{
meta: {
title: $t('demos.antd'),
icon: SvgAntdvLogoIcon,
},
name: 'AntDesignDemos',
path: '/demos/ant-design',
component: () => import('#/views/demos/antd/index.vue'),
},
// {
// meta: {
// title: '列表页示例',
// },
// name: 'ListPageDemos',
// path: '/demos/list-page',
// component: () => import('#/views/demos/listPage/index.vue'),
// },
],
},
];
export default routes;

View 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:folder-open-outlined',
order: 3,
title: $t('abp.menu.file'),
authority: ['FileManagement'],
},
name: 'file',
path: '/file',
children: [
{
name: 'abpFile',
path: 'page',
component: () => import('#/views/system/abpfiles/index.vue'),
meta: {
icon: 'ant-design:file-text-twotone',
title: $t('abp.menu.file'),
authority: ['FileManagement.File'],
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,154 @@
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: 'AbpLanguage',
path: 'language',
component: () => import('#/views/system/abplanguage/language.vue'),
meta: {
title: $t('abp.menu.language'),
authority: ['AbpIdentity.Languages'],
icon: 'ant-design:read-outlined',
},
},
{
name: 'AbpLanguageText',
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: 'dataDictionary',
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',
},
},
{
path: 'menu',
name: 'AbpMenu',
component: () => import('#/views/system/abpmenu/index.vue'),
meta: {
icon: 'ant-design:bars-outlined',
title: $t('abp.menu.menu'),
authority: ['AbpIdentity.DynamicMenuManagement'],
},
},
],
},
];
export default routes;

View 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;

View 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:tool-outlined',
// order: 998,
title: $t('textTemplate.templateManagement'),
authority: ['AbpTemplateManagement'],
},
name: 'TextTemplate',
path: '/TextTemplate',
children: [
{
path: 'page',
name: 'TextTemplatePage',
component: () => import('#/views/textTemplate/index.vue'),
meta: {
icon: 'ant-design:file-markdown-filled',
title: $t('textTemplate.templateList'),
authority: ['AbpTemplateManagement.Template'],
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,94 @@
import type { RouteRecordRaw } from 'vue-router';
import { SvgAntdvLogoIcon } from '@vben/icons';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
badgeType: 'dot',
icon: 'ph:file-doc-light',
order: 9999,
title: $t('demos.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
// {
// name: 'VbenAbout',
// path: '/vben-admin/about',
// component: () => import('#/views/_core/about/index.vue'),
// meta: {
// icon: 'lucide:copyright',
// title: $t('demos.vben.about'),
// },
// },
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: 'https://doc.cncore.club/',
title: 'ABPPro文档',
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: 'http://doc.china.cncore.club:81/',
title: 'ABPPro国内文档',
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: 'https://abp.io/docs/latest/',
title: 'ABP官方文档',
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: 'https://github.com/WangJunZzz/abp-vnext-pro',
title: 'Github',
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'ant-design:google-circle-filled',
link: 'https://gitee.com/WangJunZzz/abp-vnext-pro',
title: 'Gitee',
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: SvgAntdvLogoIcon,
// link: 'https://www.antdv.com/components/overview-cn/',
iframeSrc: 'https://doc.cncore.club/',
title: 'Ant Design Vue',
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,158 @@
import type { Recordable, UserInfo } from '@vben/types';
import type {
ApplicationAuthConfigurationDto,
ApplicationConfigurationDto,
} from '#/api-client';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { message as Message, notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import {
getApiAbpApplicationConfiguration,
postApiAppAccountLogin2Fa,
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 postApiAppAccountLogin2Fa({
body: {
...params,
},
});
// 如果成功获取到 accessToken
if (data.token) {
accessStore.setAccessToken(data.token);
accessStore.setRefreshToken(data.refreshToken);
userInfo = data as any;
userStore.setUserInfo(userInfo as any);
await getApplicationConfiguration();
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(
userInfo?.homePath || preferences.app.defaultHomePath,
);
}
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,
};
});

View File

@ -0,0 +1 @@
export * from './auth';

View File

@ -0,0 +1,3 @@
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。

Some files were not shown because too many files have changed in this diff Show More