feat: initialize platform-vue
This commit is contained in:
commit
644916b42d
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
||||||
1
.commitlintrc.js
Normal file
1
.commitlintrc.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/commitlint-config';
|
||||||
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
dist
|
||||||
|
.turbo
|
||||||
|
dist.zip
|
||||||
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=lf
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=4
|
||||||
|
max_line_length = 100
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.{yml,yaml,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
|
||||||
|
|
||||||
|
# Automatically normalize line endings (to LF) for all text-based files.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Declare files that will always have CRLF line endings on checkout.
|
||||||
|
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||||
|
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||||
|
|
||||||
|
# Denote all files that are truly binary and should not be modified.
|
||||||
|
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
|
||||||
2
.gitconfig
Normal file
2
.gitconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[core]
|
||||||
|
ignorecase = false
|
||||||
59
.gitignore
vendored
Normal file
59
.gitignore
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
|
.cursor
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.agent
|
||||||
|
.agents
|
||||||
|
.claude
|
||||||
|
.codex
|
||||||
|
skills-lock.json
|
||||||
6
.gitpod.yml
Normal file
6
.gitpod.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
ports:
|
||||||
|
- port: 5555
|
||||||
|
onOpen: open-preview
|
||||||
|
tasks:
|
||||||
|
- init: npm i -g corepack && pnpm install
|
||||||
|
command: pnpm run dev:play
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
22.22.0
|
||||||
13
.npmrc
Normal file
13
.npmrc
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
registry=https://registry.npmmirror.com
|
||||||
|
public-hoist-pattern[]=lefthook
|
||||||
|
public-hoist-pattern[]=eslint
|
||||||
|
public-hoist-pattern[]=oxfmt
|
||||||
|
public-hoist-pattern[]=oxlint
|
||||||
|
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
|
||||||
8
.stylelintignore
Normal file
8
.stylelintignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
dist
|
||||||
|
public
|
||||||
|
__tests__
|
||||||
|
coverage
|
||||||
|
.codex
|
||||||
|
.claude
|
||||||
|
.agent
|
||||||
|
.agents
|
||||||
34
.vscode/extensions.json
vendored
Normal file
34
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Vue 3 的语言支持
|
||||||
|
"Vue.volar",
|
||||||
|
// 将 eslint 集成到 VS Code 中。
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
// 将 oxlint 集成到 VS Code 中。
|
||||||
|
"oxc.oxc-vscode",
|
||||||
|
// Visual Studio Code 的官方 Stylelint 扩展
|
||||||
|
"stylelint.vscode-stylelint",
|
||||||
|
// 使用 oxfmt 的代码格式化程序
|
||||||
|
"oxc.oxc-vscode",
|
||||||
|
// 支持 dotenv 文件语法
|
||||||
|
"mikestead.dotenv",
|
||||||
|
// YAML 语言支持,供 ESLint 校验 pnpm-workspace.yaml 等文件
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
// 源代码的拼写检查器
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
// Tailwind CSS 的官方 VS Code 插件
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
// iconify 图标插件
|
||||||
|
"antfu.iconify",
|
||||||
|
// i18n 插件
|
||||||
|
"Lokalise.i18n-ally",
|
||||||
|
// CSS 变量提示
|
||||||
|
"vunguyentuan.vscode-css-variables",
|
||||||
|
// 在 package.json 中显示 PNPM catalog 的版本
|
||||||
|
"antfu.pnpm-catalog-lens"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
// 和 volar 冲突
|
||||||
|
"octref.vetur"
|
||||||
|
]
|
||||||
|
}
|
||||||
37
.vscode/global.code-snippets
vendored
Normal file
37
.vscode/global.code-snippets
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"import": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "im",
|
||||||
|
"body": ["import { $2 } from '$1';"],
|
||||||
|
"description": "Import a module",
|
||||||
|
},
|
||||||
|
"export-all": {
|
||||||
|
"scope": "javascript,typescript",
|
||||||
|
"prefix": "ex",
|
||||||
|
"body": ["export * from '$1';"],
|
||||||
|
"description": "Export a module",
|
||||||
|
},
|
||||||
|
"vue-script-setup": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "<sc",
|
||||||
|
"body": [
|
||||||
|
"<script setup lang=\"ts\">",
|
||||||
|
"const props = defineProps<{",
|
||||||
|
" modelValue?: boolean,",
|
||||||
|
"}>()",
|
||||||
|
"$1",
|
||||||
|
"</script>",
|
||||||
|
"",
|
||||||
|
"<template>",
|
||||||
|
" <div>",
|
||||||
|
" <slot/>",
|
||||||
|
" </div>",
|
||||||
|
"</template>",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"vue-computed": {
|
||||||
|
"scope": "javascript,typescript,vue",
|
||||||
|
"prefix": "com",
|
||||||
|
"body": ["computed(() => { $1 })"],
|
||||||
|
},
|
||||||
|
}
|
||||||
42
.vscode/launch.json
vendored
Normal file
42
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin playground dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5555",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/playground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin antd dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5666",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/apps/web-antd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin ele dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5777",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/apps/web-ele"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"name": "vben admin naive dev",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5888",
|
||||||
|
"env": { "NODE_ENV": "development" },
|
||||||
|
"sourceMaps": true,
|
||||||
|
"webRoot": "${workspaceFolder}/apps/web-naive"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
243
.vscode/settings.json
vendored
Normal file
243
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
{
|
||||||
|
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/theme.css",
|
||||||
|
"tailwindCSS.lint.suggestCanonicalClasses": "ignore",
|
||||||
|
// 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.quickSuggestions": {
|
||||||
|
"strings": "on"
|
||||||
|
},
|
||||||
|
|
||||||
|
// lint && format
|
||||||
|
"oxc.enable": true,
|
||||||
|
"oxc.typeAware": true,
|
||||||
|
"oxc.configPath": "oxlint.config.ts",
|
||||||
|
"oxc.fmt.configPath": "oxfmt.config.ts",
|
||||||
|
"eslint.useFlatConfig": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.oxc": "explicit",
|
||||||
|
"source.fixAll.stylelint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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": {
|
||||||
|
"*.css": "tailwindcss",
|
||||||
|
"*.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"],
|
||||||
|
|
||||||
|
"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",
|
||||||
|
"yaml"
|
||||||
|
],
|
||||||
|
|
||||||
|
"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",
|
||||||
|
"oxlint.config.ts": ".eslintignore,.stylelintignore,.commitlintrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml,oxfmt.config.*,eslint.config.*"
|
||||||
|
},
|
||||||
|
"commentTranslate.hover.enabled": false,
|
||||||
|
"commentTranslate.multiLineMerge": true,
|
||||||
|
"vue.server.hybridMode": true,
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
20
README.zh-CN.md
Normal file
20
README.zh-CN.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
## 安装使用
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g corepack
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
8
apps/.env
Normal file
8
apps/.env
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=Vben Admin Antdv Next
|
||||||
|
|
||||||
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
|
VITE_APP_NAMESPACE=vben-web-antdv-next
|
||||||
|
|
||||||
|
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||||
|
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||||
7
apps/.env.analyze
Normal file
7
apps/.env.analyze
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# public path
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# Basic interface address SPA
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_VISUALIZER=true
|
||||||
16
apps/.env.development
Normal file
16
apps/.env.development
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 端口号
|
||||||
|
VITE_PORT=5999
|
||||||
|
|
||||||
|
VITE_BASE=/
|
||||||
|
|
||||||
|
# 接口地址
|
||||||
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
|
||||||
|
VITE_NITRO_MOCK=false
|
||||||
|
|
||||||
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
||||||
19
apps/.env.production
Normal file
19
apps/.env.production
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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=hash
|
||||||
|
|
||||||
|
# 是否注入全局loading
|
||||||
|
VITE_INJECT_APP_LOADING=true
|
||||||
|
|
||||||
|
# 打包后是否生成dist.zip
|
||||||
|
VITE_ARCHIVER=true
|
||||||
35
apps/index.html
Normal file
35
apps/index.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta name="description" content="A Modern Back-end Management System" />
|
||||||
|
<meta name="keywords" content="Vben Admin Vue3 Vite" />
|
||||||
|
<meta name="author" content="Vben" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||||
|
/>
|
||||||
|
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||||
|
<title><%= VITE_APP_TITLE %></title>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<script>
|
||||||
|
// 生产环境下注入百度统计
|
||||||
|
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||||
|
var _hmt = _hmt || [];
|
||||||
|
(function () {
|
||||||
|
var hm = document.createElement('script');
|
||||||
|
hm.src =
|
||||||
|
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
|
||||||
|
var s = document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
50
apps/package.json
Normal file
50
apps/package.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben/web-antdv-next",
|
||||||
|
"version": "5.7.0",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "vben",
|
||||||
|
"email": "ann.vben@gmail.com",
|
||||||
|
"url": "https://github.com/anncwb"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"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": {
|
||||||
|
"@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:",
|
||||||
|
"antdv-next": "catalog:",
|
||||||
|
"dayjs": "catalog:",
|
||||||
|
"pinia": "catalog:",
|
||||||
|
"vue": "catalog:",
|
||||||
|
"vue-router": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/public/favicon.ico
Normal file
BIN
apps/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
603
apps/src/adapter/component/index.ts
Normal file
603
apps/src/adapter/component/index.ts
Normal file
@ -0,0 +1,603 @@
|
|||||||
|
/**
|
||||||
|
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||||
|
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable vue/one-component-per-file */
|
||||||
|
|
||||||
|
import type { UploadChangeParam, UploadFile, UploadProps } from 'antdv-next';
|
||||||
|
|
||||||
|
import type { Component, Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
defineAsyncComponent,
|
||||||
|
defineComponent,
|
||||||
|
h,
|
||||||
|
ref,
|
||||||
|
render,
|
||||||
|
unref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiComponent,
|
||||||
|
globalShareState,
|
||||||
|
IconPicker,
|
||||||
|
VCropper,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message, Modal, notification } from 'antdv-next';
|
||||||
|
|
||||||
|
const AutoComplete = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/auto-complete/index'),
|
||||||
|
);
|
||||||
|
const Button = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/button/index'),
|
||||||
|
);
|
||||||
|
const Checkbox = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/checkbox/index'),
|
||||||
|
);
|
||||||
|
const CheckboxGroup = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/checkbox/Group'),
|
||||||
|
);
|
||||||
|
const DatePicker = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/date-picker/index'),
|
||||||
|
);
|
||||||
|
const Divider = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/divider/index'),
|
||||||
|
);
|
||||||
|
const Input = defineAsyncComponent(() => import('antdv-next/dist/input/index'));
|
||||||
|
const InputNumber = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/input-number/index'),
|
||||||
|
);
|
||||||
|
const InputPassword = defineAsyncComponent(() =>
|
||||||
|
import('antdv-next/dist/input/index').then((res) => res.InputPassword),
|
||||||
|
);
|
||||||
|
const Mentions = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/mentions/index'),
|
||||||
|
);
|
||||||
|
const Radio = defineAsyncComponent(() => import('antdv-next/dist/radio/index'));
|
||||||
|
const RadioGroup = defineAsyncComponent(() =>
|
||||||
|
import('antdv-next/dist/radio/index').then((res) => res.RadioGroup),
|
||||||
|
);
|
||||||
|
const RangePicker = defineAsyncComponent(() =>
|
||||||
|
import('antdv-next/dist/date-picker/index').then(
|
||||||
|
(res) => res.DateRangePicker,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const Rate = defineAsyncComponent(() => import('antdv-next/dist/rate/index'));
|
||||||
|
const Select = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/select/index'),
|
||||||
|
);
|
||||||
|
const Space = defineAsyncComponent(() => import('antdv-next/dist/space/index'));
|
||||||
|
const Switch = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/switch/index'),
|
||||||
|
);
|
||||||
|
const Textarea = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/input/TextArea'),
|
||||||
|
);
|
||||||
|
const TimePicker = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/time-picker/index'),
|
||||||
|
);
|
||||||
|
const TreeSelect = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/tree-select/index'),
|
||||||
|
);
|
||||||
|
const Cascader = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/cascader/index'),
|
||||||
|
);
|
||||||
|
const Upload = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/upload/index'),
|
||||||
|
);
|
||||||
|
const Image = defineAsyncComponent(() => import('antdv-next/dist/image/index'));
|
||||||
|
const PreviewGroup = defineAsyncComponent(() =>
|
||||||
|
import('antdv-next/dist/image/index').then((res) => res.ImagePreviewGroup),
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
expose(
|
||||||
|
new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
component,
|
||||||
|
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const withPreviewUpload = () => {
|
||||||
|
// 检查是否为图片文件的辅助函数
|
||||||
|
const isImageFile = (file: UploadFile): boolean => {
|
||||||
|
const imageExtensions = new Set([
|
||||||
|
'bmp',
|
||||||
|
'gif',
|
||||||
|
'jpeg',
|
||||||
|
'jpg',
|
||||||
|
'png',
|
||||||
|
'svg',
|
||||||
|
'webp',
|
||||||
|
]);
|
||||||
|
if (file.url) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||||
|
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
} catch {
|
||||||
|
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!file.type) {
|
||||||
|
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
return file.type.startsWith('image/');
|
||||||
|
};
|
||||||
|
// 创建默认的上传按钮插槽
|
||||||
|
const createDefaultSlotsWithUpload = (
|
||||||
|
listType: string,
|
||||||
|
placeholder: string,
|
||||||
|
) => {
|
||||||
|
switch (listType) {
|
||||||
|
case 'picture-card': {
|
||||||
|
return {
|
||||||
|
default: () => placeholder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
icon: h(IconifyIcon, {
|
||||||
|
icon: 'ant-design:upload-outlined',
|
||||||
|
class: 'mb-1 size-4',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
() => placeholder,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 构建预览图片组
|
||||||
|
const previewImage = async (
|
||||||
|
file: UploadFile,
|
||||||
|
visible: Ref<boolean>,
|
||||||
|
fileList: Ref<UploadProps['fileList']>,
|
||||||
|
) => {
|
||||||
|
// 如果当前文件不是图片,直接打开
|
||||||
|
if (!isImageFile(file)) {
|
||||||
|
if (file.url) {
|
||||||
|
window.open(file.url, '_blank');
|
||||||
|
} else if (file.preview) {
|
||||||
|
window.open(file.preview, '_blank');
|
||||||
|
} else {
|
||||||
|
message.error($t('ui.formRules.previewWarning'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于图片文件,继续使用预览组
|
||||||
|
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||||
|
Image,
|
||||||
|
PreviewGroup,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getBase64 = (file: File) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.addEventListener('load', () => resolve(reader.result));
|
||||||
|
reader.addEventListener('error', (error) => reject(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 从fileList中过滤出所有图片文件
|
||||||
|
const imageFiles = (unref(fileList) || []).filter((element) =>
|
||||||
|
isImageFile(element),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 为所有没有预览地址的图片生成预览
|
||||||
|
for (const imgFile of imageFiles) {
|
||||||
|
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||||
|
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const container: HTMLElement | null = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
|
||||||
|
// 用于追踪组件是否已卸载
|
||||||
|
let isUnmounted = false;
|
||||||
|
|
||||||
|
const PreviewWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
return h(
|
||||||
|
PreviewGroupComponent,
|
||||||
|
{
|
||||||
|
class: 'hidden',
|
||||||
|
preview: {
|
||||||
|
open: visible.value,
|
||||||
|
// 设置初始显示的图片索引
|
||||||
|
current: imageFiles.findIndex((f) => f.uid === file.uid),
|
||||||
|
onOpenChange: (value: boolean) => {
|
||||||
|
visible.value = value;
|
||||||
|
if (!value) {
|
||||||
|
// 延迟清理,确保动画完成
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
// 渲染所有图片文件
|
||||||
|
imageFiles.map((imgFile) =>
|
||||||
|
h(ImageComponent, {
|
||||||
|
key: imgFile.uid,
|
||||||
|
src: imgFile.url || imgFile.preview,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(PreviewWrapper), container);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片裁剪操作
|
||||||
|
const cropImage = (file: File, aspectRatio: string | undefined) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const container: HTMLElement | null = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
|
||||||
|
// 用于追踪组件是否已卸载
|
||||||
|
let isUnmounted = false;
|
||||||
|
let objectUrl: null | string = null;
|
||||||
|
|
||||||
|
const open = ref<boolean>(true);
|
||||||
|
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
open.value = false;
|
||||||
|
// 延迟清理,确保动画完成
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CropperWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
if (!objectUrl) {
|
||||||
|
objectUrl = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
Modal,
|
||||||
|
{
|
||||||
|
open: open.value,
|
||||||
|
title: h('div', {}, [
|
||||||
|
$t('ui.crop.title'),
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
class: `${aspectRatio ? '' : 'hidden'} ml-2 text-sm text-gray-400 font-normal`,
|
||||||
|
},
|
||||||
|
$t('ui.crop.titleTip', [aspectRatio]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
centered: true,
|
||||||
|
width: 548,
|
||||||
|
keyboard: false,
|
||||||
|
maskClosable: false,
|
||||||
|
closable: false,
|
||||||
|
cancelText: $t('common.cancel'),
|
||||||
|
okText: $t('ui.crop.confirm'),
|
||||||
|
destroyOnHidden: true,
|
||||||
|
onOk: async () => {
|
||||||
|
const cropper = cropperRef.value;
|
||||||
|
if (!cropper) {
|
||||||
|
reject(new Error('Cropper not found'));
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dataUrl = await cropper.getCropImage();
|
||||||
|
resolve(dataUrl);
|
||||||
|
} catch {
|
||||||
|
reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
} finally {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
resolve('');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
h(VCropper, {
|
||||||
|
ref: (ref: any) => (cropperRef.value = ref),
|
||||||
|
img: objectUrl as string,
|
||||||
|
aspectRatio,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(CropperWrapper), container);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return defineComponent({
|
||||||
|
name: 'AUpload',
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup: (
|
||||||
|
props: any,
|
||||||
|
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
||||||
|
) => {
|
||||||
|
const previewVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
|
||||||
|
|
||||||
|
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
|
||||||
|
|
||||||
|
const fileList = ref<UploadProps['fileList']>(
|
||||||
|
attrs?.fileList || attrs?.['file-list'] || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxSize = computed(() => attrs?.maxSize ?? attrs?.['max-size']);
|
||||||
|
const aspectRatio = computed(
|
||||||
|
() => attrs?.aspectRatio ?? attrs?.['aspect-ratio'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBeforeUpload = async (
|
||||||
|
file: UploadFile,
|
||||||
|
originFileList: Array<File>,
|
||||||
|
) => {
|
||||||
|
if (maxSize.value && (file.size || 0) / 1024 / 1024 > maxSize.value) {
|
||||||
|
message.error($t('ui.formRules.sizeLimit', [maxSize.value]));
|
||||||
|
file.status = 'removed';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 多选或者非图片不唤起裁剪框
|
||||||
|
if (
|
||||||
|
attrs.crop &&
|
||||||
|
!attrs.multiple &&
|
||||||
|
originFileList[0] &&
|
||||||
|
isImageFile(file)
|
||||||
|
) {
|
||||||
|
file.status = 'removed';
|
||||||
|
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
|
||||||
|
const blob = await cropImage(originFileList[0], aspectRatio.value);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!blob) {
|
||||||
|
return reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
}
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs.beforeUpload?.(file) ?? true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: UploadChangeParam) => {
|
||||||
|
try {
|
||||||
|
// 行内写法 handleChange: (event) => {}
|
||||||
|
attrs.handleChange?.(event);
|
||||||
|
// template写法 @handle-change="(event) => {}"
|
||||||
|
attrs.onHandleChange?.(event);
|
||||||
|
} catch (error) {
|
||||||
|
// Avoid breaking internal v-model sync on user handler errors
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
fileList.value = event.fileList.filter(
|
||||||
|
(file) => file.status !== 'removed',
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
event.fileList?.length ? fileList.value : undefined,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = async (file: UploadFile) => {
|
||||||
|
previewVisible.value = true;
|
||||||
|
await previewImage(file, previewVisible, fileList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUploadButton = (): any => {
|
||||||
|
const isDisabled = attrs.disabled;
|
||||||
|
|
||||||
|
// 如果禁用,不渲染上传按钮
|
||||||
|
if (isDisabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则渲染默认上传按钮
|
||||||
|
return isEmpty(slots)
|
||||||
|
? createDefaultSlotsWithUpload(listType, placeholder)
|
||||||
|
: slots;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可以监听到表单API设置的值
|
||||||
|
watch(
|
||||||
|
() => attrs.modelValue,
|
||||||
|
(res) => {
|
||||||
|
fileList.value = res;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
Upload,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
fileList: fileList.value,
|
||||||
|
beforeUpload: handleBeforeUpload,
|
||||||
|
onChange: handleChange,
|
||||||
|
onPreview: handlePreview,
|
||||||
|
},
|
||||||
|
renderUploadButton(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||||
|
export type ComponentType =
|
||||||
|
| 'ApiCascader'
|
||||||
|
| 'ApiSelect'
|
||||||
|
| 'ApiTreeSelect'
|
||||||
|
| 'AutoComplete'
|
||||||
|
| 'Cascader'
|
||||||
|
| '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),
|
||||||
|
|
||||||
|
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
|
component: Cascader,
|
||||||
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
modelPropName: 'value',
|
||||||
|
visibleEvent: 'onVisibleChange',
|
||||||
|
}),
|
||||||
|
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
|
component: Select,
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
modelPropName: 'value',
|
||||||
|
visibleEvent: 'onVisibleChange',
|
||||||
|
}),
|
||||||
|
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
|
component: TreeSelect,
|
||||||
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
|
loadingSlot: 'suffixIcon',
|
||||||
|
modelPropName: 'value',
|
||||||
|
optionsPropName: 'treeData',
|
||||||
|
visibleEvent: 'onVisibleChange',
|
||||||
|
}),
|
||||||
|
AutoComplete,
|
||||||
|
Cascader,
|
||||||
|
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: withPreviewUpload(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将组件注册到全局共享状态中
|
||||||
|
globalShareState.setComponents(components);
|
||||||
|
|
||||||
|
// 定义全局共享状态中的消息提示
|
||||||
|
globalShareState.defineMessage({
|
||||||
|
// 复制成功消息提示
|
||||||
|
copyPreferencesSuccess: (title, content) => {
|
||||||
|
notification.success({
|
||||||
|
description: content,
|
||||||
|
title,
|
||||||
|
placement: 'bottomRight',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initComponentAdapter };
|
||||||
49
apps/src/adapter/form.ts
Normal file
49
apps/src/adapter/form.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
async function initSetupVbenForm() {
|
||||||
|
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 { initSetupVbenForm, useVbenForm, z };
|
||||||
|
|
||||||
|
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||||
|
export type { VbenFormProps };
|
||||||
70
apps/src/adapter/vxe-table.ts
Normal file
70
apps/src/adapter/vxe-table.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||||
|
|
||||||
|
import { Button, Image } from 'antdv-next';
|
||||||
|
|
||||||
|
import { useVbenForm } from './form';
|
||||||
|
|
||||||
|
setupVbenVxeTable({
|
||||||
|
configVxeTable: (vxeUI) => {
|
||||||
|
vxeUI.setConfig({
|
||||||
|
grid: {
|
||||||
|
align: 'center',
|
||||||
|
border: false,
|
||||||
|
columnConfig: {
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
minHeight: 180,
|
||||||
|
formConfig: {
|
||||||
|
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
autoLoad: true,
|
||||||
|
response: {
|
||||||
|
result: 'items',
|
||||||
|
total: 'total',
|
||||||
|
list: 'items',
|
||||||
|
},
|
||||||
|
showActiveMsg: true,
|
||||||
|
showResponseMsg: false,
|
||||||
|
},
|
||||||
|
round: true,
|
||||||
|
showOverflow: true,
|
||||||
|
size: 'small',
|
||||||
|
} as VxeTableGridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
|
vxeUI.renderer.add('CellImage', {
|
||||||
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
|
const { column, row } = params;
|
||||||
|
return h(Image, { src: row[column.field], ...props });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格配置项可以用 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';
|
||||||
51
apps/src/api/core/auth.ts
Normal file
51
apps/src/api/core/auth.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { baseRequestClient, requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace AuthApi {
|
||||||
|
/** 登录接口参数 */
|
||||||
|
export interface LoginParams {
|
||||||
|
password?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录接口返回值 */
|
||||||
|
export interface LoginResult {
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenResult {
|
||||||
|
data: string;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
export async function loginApi(data: AuthApi.LoginParams) {
|
||||||
|
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新accessToken
|
||||||
|
*/
|
||||||
|
export async function refreshTokenApi() {
|
||||||
|
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
export async function logoutApi() {
|
||||||
|
return baseRequestClient.post('/auth/logout', {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户权限码
|
||||||
|
*/
|
||||||
|
export async function getAccessCodesApi() {
|
||||||
|
return requestClient.get<string[]>('/auth/codes');
|
||||||
|
}
|
||||||
3
apps/src/api/core/index.ts
Normal file
3
apps/src/api/core/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './menu';
|
||||||
|
export * from './user';
|
||||||
10
apps/src/api/core/menu.ts
Normal file
10
apps/src/api/core/menu.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { RouteRecordStringComponent } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户所有菜单
|
||||||
|
*/
|
||||||
|
export async function getAllMenusApi() {
|
||||||
|
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||||
|
}
|
||||||
10
apps/src/api/core/user.ts
Normal file
10
apps/src/api/core/user.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
export async function getUserInfoApi() {
|
||||||
|
return requestClient.get<UserInfo>('/user/info');
|
||||||
|
}
|
||||||
1
apps/src/api/index.ts
Normal file
1
apps/src/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './core';
|
||||||
113
apps/src/api/request.ts
Normal file
113
apps/src/api/request.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* 该文件可自行根据业务逻辑进行调整
|
||||||
|
*/
|
||||||
|
import type { RequestClientOptions } from '@vben/request';
|
||||||
|
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import {
|
||||||
|
authenticateResponseInterceptor,
|
||||||
|
defaultResponseInterceptor,
|
||||||
|
errorMessageResponseInterceptor,
|
||||||
|
RequestClient,
|
||||||
|
} from '@vben/request';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from 'antdv-next';
|
||||||
|
|
||||||
|
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/src/app.vue
Normal file
39
apps/src/app.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useAntdDesignTokens } from '@vben/hooks';
|
||||||
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { App, ConfigProvider, theme } from 'antdv-next';
|
||||||
|
|
||||||
|
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>
|
||||||
76
apps/src/bootstrap.ts
Normal file
76
apps/src/bootstrap.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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/antdv-next';
|
||||||
|
|
||||||
|
import { useTitle } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { $t, setupI18n } from '#/locales';
|
||||||
|
|
||||||
|
import { initComponentAdapter } from './adapter/component';
|
||||||
|
import { initSetupVbenForm } from './adapter/form';
|
||||||
|
import App from './app.vue';
|
||||||
|
import { router } from './router';
|
||||||
|
|
||||||
|
async function bootstrap(namespace: string) {
|
||||||
|
// 初始化组件适配器
|
||||||
|
await initComponentAdapter();
|
||||||
|
|
||||||
|
// 初始化表单组件
|
||||||
|
await initSetupVbenForm();
|
||||||
|
|
||||||
|
// // 设置弹窗的默认配置
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 配置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.mount('#app');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { bootstrap };
|
||||||
25
apps/src/layouts/auth.vue
Normal file
25
apps/src/layouts/auth.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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);
|
||||||
|
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthPageLayout
|
||||||
|
:app-name="appName"
|
||||||
|
:logo="logo"
|
||||||
|
:logo-dark="logoDark"
|
||||||
|
:page-description="$t('authentication.pageDesc')"
|
||||||
|
:page-title="$t('authentication.pageTitle')"
|
||||||
|
>
|
||||||
|
<!-- 自定义工具栏 -->
|
||||||
|
<!-- <template #toolbar></template> -->
|
||||||
|
</AuthPageLayout>
|
||||||
|
</template>
|
||||||
206
apps/src/layouts/basic.vue
Normal file
206
apps/src/layouts/basic.vue
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||||
|
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
|
import { useWatermark } from '@vben/hooks';
|
||||||
|
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
|
||||||
|
import {
|
||||||
|
BasicLayout,
|
||||||
|
LockScreen,
|
||||||
|
Notification,
|
||||||
|
UserDropdown,
|
||||||
|
} from '@vben/layouts';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
|
const notifications = ref<NotificationItem[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||||
|
date: '3小时前',
|
||||||
|
isRead: true,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '收到了 14 份新周报',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
|
date: '刚刚',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '朱偏右 回复了你',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
avatar: 'https://avatar.vercel.sh/1',
|
||||||
|
date: '2024-01-01',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '曲丽丽 评论了你',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '代办提醒',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '跳转Workspace示例',
|
||||||
|
link: '/workspace',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
date: '1天前',
|
||||||
|
isRead: false,
|
||||||
|
message: '描述信息描述信息描述信息',
|
||||||
|
title: '跳转外部链接示例',
|
||||||
|
link: 'https://doc.vben.pro',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||||
|
const showDot = computed(() =>
|
||||||
|
notifications.value.some((item) => !item.isRead),
|
||||||
|
);
|
||||||
|
|
||||||
|
const menus = computed(() => [
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
router.push({ name: 'Profile' });
|
||||||
|
},
|
||||||
|
icon: 'lucide:user',
|
||||||
|
text: $t('page.auth.profile'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openWindow(VBEN_DOC_URL, {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: BookOpenText,
|
||||||
|
text: $t('ui.widgets.document'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openWindow(VBEN_GITHUB_URL, {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: SvgGithubIcon,
|
||||||
|
text: 'GitHub',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handler: () => {
|
||||||
|
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: CircleHelp,
|
||||||
|
text: $t('ui.widgets.qa'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const avatar = computed(() => {
|
||||||
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await authStore.logout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNoticeClear() {
|
||||||
|
notifications.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRead(id: number | string) {
|
||||||
|
const item = notifications.value.find((item) => item.id === id);
|
||||||
|
if (item) {
|
||||||
|
item.isRead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(id: number | string) {
|
||||||
|
notifications.value = notifications.value.filter((item) => item.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMakeAll() {
|
||||||
|
notifications.value.forEach((item) => (item.isRead = true));
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => ({
|
||||||
|
enable: preferences.app.watermark,
|
||||||
|
content: preferences.app.watermarkContent,
|
||||||
|
}),
|
||||||
|
async ({ enable, content }) => {
|
||||||
|
if (enable) {
|
||||||
|
await updateWatermark({
|
||||||
|
content:
|
||||||
|
content ||
|
||||||
|
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
destroyWatermark();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||||||
|
<template #user-dropdown>
|
||||||
|
<UserDropdown
|
||||||
|
:avatar
|
||||||
|
:menus
|
||||||
|
:text="userStore.userInfo?.realName"
|
||||||
|
description="ann.vben@gmail.com"
|
||||||
|
tag-text="Pro"
|
||||||
|
@logout="handleLogout"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #notification>
|
||||||
|
<Notification
|
||||||
|
:dot="showDot"
|
||||||
|
:notifications="notifications"
|
||||||
|
@clear="handleNoticeClear"
|
||||||
|
@read="(item) => item.id && markRead(item.id)"
|
||||||
|
@remove="(item) => item.id && remove(item.id)"
|
||||||
|
@make-all="handleMakeAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<AuthenticationLoginExpiredModal
|
||||||
|
v-model:open="accessStore.loginExpired"
|
||||||
|
:avatar
|
||||||
|
>
|
||||||
|
<LoginForm />
|
||||||
|
</AuthenticationLoginExpiredModal>
|
||||||
|
</template>
|
||||||
|
<template #lock-screen>
|
||||||
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
</template>
|
||||||
|
</BasicLayout>
|
||||||
|
</template>
|
||||||
6
apps/src/layouts/index.ts
Normal file
6
apps/src/layouts/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const BasicLayout = () => import('./basic.vue');
|
||||||
|
const AuthPageLayout = () => import('./auth.vue');
|
||||||
|
|
||||||
|
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||||
|
|
||||||
|
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||||
3
apps/src/locales/README.md
Normal file
3
apps/src/locales/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# locale
|
||||||
|
|
||||||
|
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||||
102
apps/src/locales/index.ts
Normal file
102
apps/src/locales/index.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import type { Locale } from 'antdv-next/dist/locale/index';
|
||||||
|
|
||||||
|
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 'antdv-next/dist/locale/en_US';
|
||||||
|
import antdDefaultLocale from 'antdv-next/dist/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 };
|
||||||
14
apps/src/locales/langs/en-US/demos.json
Normal file
14
apps/src/locales/langs/en-US/demos.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"title": "Demos",
|
||||||
|
"antd": "Antdv Next",
|
||||||
|
"vben": {
|
||||||
|
"title": "Project",
|
||||||
|
"about": "About",
|
||||||
|
"document": "Document",
|
||||||
|
"antdv": "Ant Design Vue Version",
|
||||||
|
"antdv-next": "Antdv Next Version",
|
||||||
|
"naive-ui": "Naive UI Version",
|
||||||
|
"element-plus": "Element Plus Version",
|
||||||
|
"tdesign": "TDesign Vue Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/src/locales/langs/en-US/page.json
Normal file
15
apps/src/locales/langs/en-US/page.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": "Login",
|
||||||
|
"register": "Register",
|
||||||
|
"codeLogin": "Code Login",
|
||||||
|
"qrcodeLogin": "Qr Code Login",
|
||||||
|
"forgetPassword": "Forget Password",
|
||||||
|
"profile": "Profile"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"workspace": "Workspace"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
apps/src/locales/langs/zh-CN/demos.json
Normal file
14
apps/src/locales/langs/zh-CN/demos.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"title": "演示",
|
||||||
|
"antd": "Antdv Next",
|
||||||
|
"vben": {
|
||||||
|
"title": "项目",
|
||||||
|
"about": "关于",
|
||||||
|
"document": "文档",
|
||||||
|
"antdv": "Ant Design Vue 版本",
|
||||||
|
"antdv-next": "Antdv Next 版本",
|
||||||
|
"naive-ui": "Naive UI 版本",
|
||||||
|
"element-plus": "Element Plus 版本",
|
||||||
|
"tdesign": "TDesign Vue 版本"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/src/locales/langs/zh-CN/page.json
Normal file
15
apps/src/locales/langs/zh-CN/page.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"login": "登录",
|
||||||
|
"register": "注册",
|
||||||
|
"codeLogin": "验证码登录",
|
||||||
|
"qrcodeLogin": "二维码登录",
|
||||||
|
"forgetPassword": "忘记密码",
|
||||||
|
"profile": "个人中心"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "概览",
|
||||||
|
"analytics": "分析页",
|
||||||
|
"workspace": "工作台"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/src/main.ts
Normal file
31
apps/src/main.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { initPreferences } from '@vben/preferences';
|
||||||
|
import { unmountGlobalLoading } from '@vben/utils';
|
||||||
|
|
||||||
|
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();
|
||||||
13
apps/src/preferences.ts
Normal file
13
apps/src/preferences.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineOverridesPreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 项目配置文件
|
||||||
|
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||||
|
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||||
|
*/
|
||||||
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
|
// overrides
|
||||||
|
app: {
|
||||||
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
|
},
|
||||||
|
});
|
||||||
42
apps/src/router/access.ts
Normal file
42
apps/src/router/access.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type {
|
||||||
|
ComponentRecordType,
|
||||||
|
GenerateMenuAndRoutesOptions,
|
||||||
|
} from '@vben/types';
|
||||||
|
|
||||||
|
import { generateAccessible } from '@vben/access';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { message } from 'antdv-next';
|
||||||
|
|
||||||
|
import { getAllMenusApi } from '#/api';
|
||||||
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
|
||||||
|
const layoutMap: ComponentRecordType = {
|
||||||
|
BasicLayout,
|
||||||
|
IFrameView,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await generateAccessible(preferences.app.accessMode, {
|
||||||
|
...options,
|
||||||
|
fetchMenuListAsync: async () => {
|
||||||
|
message.loading({
|
||||||
|
content: `${$t('common.loadingMenu')}...`,
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
return await getAllMenusApi();
|
||||||
|
},
|
||||||
|
// 可以指定没有权限跳转403页面
|
||||||
|
forbiddenComponent,
|
||||||
|
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||||
|
layoutMap,
|
||||||
|
pageMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateAccess };
|
||||||
133
apps/src/router/guard.ts
Normal file
133
apps/src/router/guard.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
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((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 { 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
|
||||||
|
? 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 };
|
||||||
37
apps/src/router/index.ts
Normal file
37
apps/src/router/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
createWebHistory,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import { resetStaticRoutes } from '@vben/utils';
|
||||||
|
|
||||||
|
import { createRouterGuard } from './guard';
|
||||||
|
import { routes } from './routes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 创建vue-router实例
|
||||||
|
*/
|
||||||
|
const router = createRouter({
|
||||||
|
history:
|
||||||
|
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||||
|
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||||
|
: createWebHistory(import.meta.env.VITE_BASE),
|
||||||
|
// 应该添加到路由的初始路由列表。
|
||||||
|
routes,
|
||||||
|
scrollBehavior: (to, _from, savedPosition) => {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
}
|
||||||
|
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
|
||||||
|
},
|
||||||
|
// 是否应该禁止尾部斜杠。
|
||||||
|
// strict: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||||
|
|
||||||
|
// 创建路由守卫
|
||||||
|
createRouterGuard(router);
|
||||||
|
|
||||||
|
export { resetRoutes, router };
|
||||||
97
apps/src/router/routes/core.ts
Normal file
97
apps/src/router/routes/core.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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: 'QrCodeLogin',
|
||||||
|
path: 'qrcode-login',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/qrcode-login.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.qrcodeLogin'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ForgetPassword',
|
||||||
|
path: 'forget-password',
|
||||||
|
component: () =>
|
||||||
|
import('#/views/_core/authentication/forget-password.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.forgetPassword'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Register',
|
||||||
|
path: 'register',
|
||||||
|
component: () => import('#/views/_core/authentication/register.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.auth.register'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { coreRoutes, fallbackNotFoundRoute };
|
||||||
37
apps/src/router/routes/index.ts
Normal file
37
apps/src/router/routes/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||||
|
|
||||||
|
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||||
|
|
||||||
|
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 有需要可以自行打开注释,并创建文件夹
|
||||||
|
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||||
|
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||||
|
|
||||||
|
/** 动态路由 */
|
||||||
|
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||||
|
|
||||||
|
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||||
|
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||||
|
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||||
|
const staticRoutes: RouteRecordRaw[] = [];
|
||||||
|
const externalRoutes: RouteRecordRaw[] = [];
|
||||||
|
|
||||||
|
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||||
|
* 无需走权限验证(会一直显示在菜单中) */
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
...coreRoutes,
|
||||||
|
...externalRoutes,
|
||||||
|
fallbackNotFoundRoute,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||||
|
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||||
|
|
||||||
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
|
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||||
|
export { accessRoutes, coreRouteNames, routes };
|
||||||
38
apps/src/router/routes/modules/dashboard.ts
Normal file
38
apps/src/router/routes/modules/dashboard.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:layout-dashboard',
|
||||||
|
order: -1,
|
||||||
|
title: $t('page.dashboard.title'),
|
||||||
|
},
|
||||||
|
name: 'Dashboard',
|
||||||
|
path: '/dashboard',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Analytics',
|
||||||
|
path: '/analytics',
|
||||||
|
component: () => import('#/views/dashboard/analytics/index.vue'),
|
||||||
|
meta: {
|
||||||
|
affixTab: true,
|
||||||
|
icon: 'lucide:area-chart',
|
||||||
|
title: $t('page.dashboard.analytics'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Workspace',
|
||||||
|
path: '/workspace',
|
||||||
|
component: () => import('#/views/dashboard/workspace/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: $t('page.dashboard.workspace'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
28
apps/src/router/routes/modules/demos.ts
Normal file
28
apps/src/router/routes/modules/demos.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
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'),
|
||||||
|
},
|
||||||
|
name: 'AntDesignDemos',
|
||||||
|
path: '/demos/ant-design-next',
|
||||||
|
component: () => import('#/views/demos/antd/index.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
116
apps/src/router/routes/modules/vben.ts
Normal file
116
apps/src/router/routes/modules/vben.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
VBEN_ANT_PREVIEW_URL,
|
||||||
|
VBEN_DOC_URL,
|
||||||
|
VBEN_ELE_PREVIEW_URL,
|
||||||
|
VBEN_GITHUB_URL,
|
||||||
|
VBEN_LOGO_URL,
|
||||||
|
VBEN_NAIVE_PREVIEW_URL,
|
||||||
|
VBEN_TD_PREVIEW_URL,
|
||||||
|
} from '@vben/constants';
|
||||||
|
import { SvgAntdvLogoIcon, SvgTDesignIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { IFrameView } from '#/layouts';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: VBEN_LOGO_URL,
|
||||||
|
order: 9998,
|
||||||
|
title: $t('demos.vben.title'),
|
||||||
|
},
|
||||||
|
name: 'VbenProject',
|
||||||
|
path: '/vben-admin',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'VbenDocument',
|
||||||
|
path: '/vben-admin/document',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:book-open-text',
|
||||||
|
link: VBEN_DOC_URL,
|
||||||
|
title: $t('demos.vben.document'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenGithub',
|
||||||
|
path: '/vben-admin/github',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
icon: 'mdi:github',
|
||||||
|
link: VBEN_GITHUB_URL,
|
||||||
|
title: 'Github',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenAntd',
|
||||||
|
path: '/vben-admin/antd',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: SvgAntdvLogoIcon,
|
||||||
|
link: VBEN_ANT_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.antdv'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenNaive',
|
||||||
|
path: '/vben-admin/naive',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: 'logos:naiveui',
|
||||||
|
link: VBEN_NAIVE_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.naive-ui'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenTDesign',
|
||||||
|
path: '/vben-admin/tdesign',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: SvgTDesignIcon,
|
||||||
|
link: VBEN_TD_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.tdesign'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenElementPlus',
|
||||||
|
path: '/vben-admin/ele',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
icon: 'logos:element',
|
||||||
|
link: VBEN_ELE_PREVIEW_URL,
|
||||||
|
title: $t('demos.vben.element-plus'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VbenAbout',
|
||||||
|
path: '/vben-admin/about',
|
||||||
|
component: () => import('#/views/_core/about/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:copyright',
|
||||||
|
title: $t('demos.vben.about'),
|
||||||
|
order: 9999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Profile',
|
||||||
|
path: '/profile',
|
||||||
|
component: () => import('#/views/_core/profile/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:user',
|
||||||
|
hideInMenu: true,
|
||||||
|
title: $t('page.auth.profile'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
117
apps/src/store/auth.ts
Normal file
117
apps/src/store/auth.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import type { Recordable, UserInfo } from '@vben/types';
|
||||||
|
|
||||||
|
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 { notification } from 'antdv-next';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
|
||||||
|
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;
|
||||||
|
const { accessToken } = await loginApi(params);
|
||||||
|
|
||||||
|
// 如果成功获取到 accessToken
|
||||||
|
if (accessToken) {
|
||||||
|
accessStore.setAccessToken(accessToken);
|
||||||
|
|
||||||
|
// 获取用户信息并存储到 accessStore 中
|
||||||
|
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||||
|
fetchUserInfo(),
|
||||||
|
getAccessCodesApi(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
userInfo = fetchUserInfoResult;
|
||||||
|
|
||||||
|
userStore.setUserInfo(userInfo);
|
||||||
|
accessStore.setAccessCodes(accessCodes);
|
||||||
|
|
||||||
|
if (accessStore.loginExpired) {
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
} else {
|
||||||
|
onSuccess
|
||||||
|
? await onSuccess?.()
|
||||||
|
: await router.push(
|
||||||
|
userInfo.homePath || preferences.app.defaultHomePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInfo?.realName) {
|
||||||
|
notification.success({
|
||||||
|
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||||
|
duration: 3,
|
||||||
|
title: $t('authentication.loginSuccess'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loginLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(redirect: boolean = true) {
|
||||||
|
try {
|
||||||
|
await logoutApi();
|
||||||
|
} catch {
|
||||||
|
// 不做任何处理
|
||||||
|
}
|
||||||
|
resetAllStores();
|
||||||
|
accessStore.setLoginExpired(false);
|
||||||
|
|
||||||
|
// 回登录页带上当前路由地址
|
||||||
|
await router.replace({
|
||||||
|
path: LOGIN_PATH,
|
||||||
|
query: redirect
|
||||||
|
? {
|
||||||
|
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
const userInfo = await getUserInfoApi();
|
||||||
|
userStore.setUserInfo(userInfo);
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $reset() {
|
||||||
|
loginLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
$reset,
|
||||||
|
authLogin,
|
||||||
|
fetchUserInfo,
|
||||||
|
loginLoading,
|
||||||
|
logout,
|
||||||
|
};
|
||||||
|
});
|
||||||
1
apps/src/store/index.ts
Normal file
1
apps/src/store/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './auth';
|
||||||
3
apps/src/views/_core/README.md
Normal file
3
apps/src/views/_core/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# \_core
|
||||||
|
|
||||||
|
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||||
9
apps/src/views/_core/about/index.vue
Normal file
9
apps/src/views/_core/about/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { About } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'About' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<About />
|
||||||
|
</template>
|
||||||
68
apps/src/views/_core/authentication/code-login.vue
Normal file
68
apps/src/views/_core/authentication/code-login.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const CODE_LENGTH = 6;
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.mobile'),
|
||||||
|
},
|
||||||
|
fieldName: 'phoneNumber',
|
||||||
|
label: $t('authentication.mobile'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.mobileTip') })
|
||||||
|
.refine((v) => /^\d{11}$/.test(v), {
|
||||||
|
message: $t('authentication.mobileErrortip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenPinInput',
|
||||||
|
componentProps: {
|
||||||
|
codeLength: CODE_LENGTH,
|
||||||
|
createText: (countdown: number) => {
|
||||||
|
const text =
|
||||||
|
countdown > 0
|
||||||
|
? $t('authentication.sendText', [countdown])
|
||||||
|
: $t('authentication.sendCode');
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
placeholder: $t('authentication.code'),
|
||||||
|
},
|
||||||
|
fieldName: 'code',
|
||||||
|
label: $t('authentication.code'),
|
||||||
|
rules: z.string().length(CODE_LENGTH, {
|
||||||
|
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 异步处理登录操作
|
||||||
|
* Asynchronously handle the login process
|
||||||
|
* @param values 登录表单数据
|
||||||
|
*/
|
||||||
|
async function handleLogin(values: Recordable<any>) {
|
||||||
|
void values;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationCodeLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleLogin"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
42
apps/src/views/_core/authentication/forget-password.vue
Normal file
42
apps/src/views/_core/authentication/forget-password.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'example@example.com',
|
||||||
|
},
|
||||||
|
fieldName: 'email',
|
||||||
|
label: $t('authentication.email'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.emailTip') })
|
||||||
|
.email($t('authentication.emailValidErrorTip')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(value: Recordable<any>) {
|
||||||
|
void value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationForgetPassword
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
98
apps/src/views/_core/authentication/login.vue
Normal file
98
apps/src/views/_core/authentication/login.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { BasicOption } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, markRaw } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Super',
|
||||||
|
value: 'vben',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Admin',
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User',
|
||||||
|
value: 'jack',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
options: MOCK_USER_OPTIONS,
|
||||||
|
placeholder: $t('authentication.selectAccount'),
|
||||||
|
},
|
||||||
|
fieldName: 'selectAccount',
|
||||||
|
label: $t('authentication.selectAccount'),
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: $t('authentication.selectAccount') })
|
||||||
|
.optional()
|
||||||
|
.default('vben'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.usernameTip'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
trigger(values, form) {
|
||||||
|
if (values.selectAccount) {
|
||||||
|
const findUser = MOCK_USER_OPTIONS.find(
|
||||||
|
(item) => item.value === values.selectAccount,
|
||||||
|
);
|
||||||
|
if (findUser) {
|
||||||
|
form.setValues({
|
||||||
|
password: '123456',
|
||||||
|
username: findUser.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerFields: ['selectAccount'],
|
||||||
|
},
|
||||||
|
fieldName: 'username',
|
||||||
|
label: $t('authentication.username'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(SliderCaptcha),
|
||||||
|
fieldName: 'captcha',
|
||||||
|
rules: z.boolean().refine((value) => value, {
|
||||||
|
message: $t('authentication.verifyRequiredTip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="authStore.loginLoading"
|
||||||
|
@submit="authStore.authLogin"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
10
apps/src/views/_core/authentication/qrcode-login.vue
Normal file
10
apps/src/views/_core/authentication/qrcode-login.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||||
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
|
||||||
|
defineOptions({ name: 'QrCodeLogin' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||||
|
</template>
|
||||||
95
apps/src/views/_core/authentication/register.vue
Normal file
95
apps/src/views/_core/authentication/register.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
|
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.usernameTip'),
|
||||||
|
},
|
||||||
|
fieldName: 'username',
|
||||||
|
label: $t('authentication.username'),
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: $t('authentication.password'),
|
||||||
|
},
|
||||||
|
fieldName: 'password',
|
||||||
|
label: $t('authentication.password'),
|
||||||
|
renderComponentContent() {
|
||||||
|
return {
|
||||||
|
strengthText: () => $t('authentication.passwordStrength'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { password } = values;
|
||||||
|
return z
|
||||||
|
.string({ required_error: $t('authentication.passwordTip') })
|
||||||
|
.min(1, { message: $t('authentication.passwordTip') })
|
||||||
|
.refine((value) => value === password, {
|
||||||
|
message: $t('authentication.confirmPasswordTip'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['password'],
|
||||||
|
},
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: $t('authentication.confirmPassword'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenCheckbox',
|
||||||
|
fieldName: 'agreePolicy',
|
||||||
|
renderComponentContent: () => ({
|
||||||
|
default: () =>
|
||||||
|
h('span', [
|
||||||
|
$t('authentication.agree'),
|
||||||
|
h(
|
||||||
|
'a',
|
||||||
|
{
|
||||||
|
class: 'vben-link ml-1 ',
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
rules: z.boolean().refine((value) => !!value, {
|
||||||
|
message: $t('authentication.agreeTip'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(value: Recordable<any>) {
|
||||||
|
void value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AuthenticationRegister
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:loading="loading"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
7
apps/src/views/_core/fallback/coming-soon.vue
Normal file
7
apps/src/views/_core/fallback/coming-soon.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="coming-soon" />
|
||||||
|
</template>
|
||||||
9
apps/src/views/_core/fallback/forbidden.vue
Normal file
9
apps/src/views/_core/fallback/forbidden.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback403Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="403" />
|
||||||
|
</template>
|
||||||
9
apps/src/views/_core/fallback/internal-error.vue
Normal file
9
apps/src/views/_core/fallback/internal-error.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback500Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="500" />
|
||||||
|
</template>
|
||||||
9
apps/src/views/_core/fallback/not-found.vue
Normal file
9
apps/src/views/_core/fallback/not-found.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'Fallback404Demo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="404" />
|
||||||
|
</template>
|
||||||
9
apps/src/views/_core/fallback/offline.vue
Normal file
9
apps/src/views/_core/fallback/offline.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Fallback } from '@vben/common-ui';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fallback status="offline" />
|
||||||
|
</template>
|
||||||
65
apps/src/views/_core/profile/base-setting.vue
Normal file
65
apps/src/views/_core/profile/base-setting.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { BasicOption } from '@vben/types';
|
||||||
|
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileBaseSetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { getUserInfoApi } from '#/api';
|
||||||
|
|
||||||
|
const profileBaseSettingRef = ref();
|
||||||
|
|
||||||
|
const MOCK_ROLES_OPTIONS: BasicOption[] = [
|
||||||
|
{
|
||||||
|
label: '管理员',
|
||||||
|
value: 'super',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '用户',
|
||||||
|
value: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'realName',
|
||||||
|
component: 'Input',
|
||||||
|
label: '姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'username',
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'roles',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
options: MOCK_ROLES_OPTIONS,
|
||||||
|
},
|
||||||
|
label: '角色',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'introduction',
|
||||||
|
component: 'Textarea',
|
||||||
|
label: '个人简介',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await getUserInfoApi();
|
||||||
|
profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
49
apps/src/views/_core/profile/index.vue
Normal file
49
apps/src/views/_core/profile/index.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Profile } from '@vben/common-ui';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import ProfileBase from './base-setting.vue';
|
||||||
|
import ProfileNotificationSetting from './notification-setting.vue';
|
||||||
|
import ProfilePasswordSetting from './password-setting.vue';
|
||||||
|
import ProfileSecuritySetting from './security-setting.vue';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const tabsValue = ref<string>('basic');
|
||||||
|
|
||||||
|
const tabs = ref([
|
||||||
|
{
|
||||||
|
label: '基本设置',
|
||||||
|
value: 'basic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '安全设置',
|
||||||
|
value: 'security',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '修改密码',
|
||||||
|
value: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '新消息提醒',
|
||||||
|
value: 'notice',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Profile
|
||||||
|
v-model:model-value="tabsValue"
|
||||||
|
title="个人中心"
|
||||||
|
:user-info="userStore.userInfo"
|
||||||
|
:tabs="tabs"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<ProfileBase v-if="tabsValue === 'basic'" />
|
||||||
|
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
||||||
|
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
||||||
|
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
||||||
|
</template>
|
||||||
|
</Profile>
|
||||||
|
</template>
|
||||||
31
apps/src/views/_core/profile/notification-setting.vue
Normal file
31
apps/src/views/_core/profile/notification-setting.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '其他用户的消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'systemMessage',
|
||||||
|
label: '系统消息',
|
||||||
|
description: '系统消息将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'todoTask',
|
||||||
|
label: '待办任务',
|
||||||
|
description: '待办任务将以站内信的形式通知',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
63
apps/src/views/_core/profile/password-setting.vue
Normal file
63
apps/src/views/_core/profile/password-setting.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'antdv-next';
|
||||||
|
|
||||||
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'oldPassword',
|
||||||
|
label: '旧密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入旧密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'newPassword',
|
||||||
|
label: '新密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请输入新密码',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
component: 'VbenInputPassword',
|
||||||
|
componentProps: {
|
||||||
|
passwordStrength: true,
|
||||||
|
placeholder: '请再次输入新密码',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules(values) {
|
||||||
|
const { newPassword } = values;
|
||||||
|
return z
|
||||||
|
.string({ required_error: '请再次输入新密码' })
|
||||||
|
.min(1, { message: '请再次输入新密码' })
|
||||||
|
.refine((value) => value === newPassword, {
|
||||||
|
message: '两次输入的密码不一致',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerFields: ['newPassword'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
message.success('密码修改成功');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfilePasswordSetting
|
||||||
|
class="w-1/3"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
43
apps/src/views/_core/profile/security-setting.vue
Normal file
43
apps/src/views/_core/profile/security-setting.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { ProfileSecuritySetting } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'accountPassword',
|
||||||
|
label: '账户密码',
|
||||||
|
description: '当前密码强度:强',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityPhone',
|
||||||
|
label: '密保手机',
|
||||||
|
description: '已绑定手机:138****8293',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityQuestion',
|
||||||
|
label: '密保问题',
|
||||||
|
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
fieldName: 'securityEmail',
|
||||||
|
label: '备用邮箱',
|
||||||
|
description: '已绑定邮箱:ant***sign.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
fieldName: 'securityMfa',
|
||||||
|
label: 'MFA 设备',
|
||||||
|
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||||
|
</template>
|
||||||
98
apps/src/views/dashboard/analytics/analytics-trends.vue
Normal file
98
apps/src/views/dashboard/analytics/analytics-trends.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
grid: {
|
||||||
|
bottom: 0,
|
||||||
|
containLabel: true,
|
||||||
|
left: '1%',
|
||||||
|
right: '1%',
|
||||||
|
top: '2 %',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
areaStyle: {},
|
||||||
|
data: [
|
||||||
|
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||||
|
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||||
|
111,
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5ab1ef',
|
||||||
|
},
|
||||||
|
smooth: true,
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
areaStyle: {},
|
||||||
|
data: [
|
||||||
|
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||||
|
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
},
|
||||||
|
smooth: true,
|
||||||
|
type: 'line',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
// xAxis: {
|
||||||
|
// axisTick: {
|
||||||
|
// show: false,
|
||||||
|
// },
|
||||||
|
// boundaryGap: false,
|
||||||
|
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||||
|
// type: 'category',
|
||||||
|
// },
|
||||||
|
xAxis: {
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
boundaryGap: false,
|
||||||
|
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
type: 'solid',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
type: 'category',
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
max: 80_000,
|
||||||
|
splitArea: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
splitNumber: 4,
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
82
apps/src/views/dashboard/analytics/analytics-visits-data.vue
Normal file
82
apps/src/views/dashboard/analytics/analytics-visits-data.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
legend: {
|
||||||
|
bottom: 0,
|
||||||
|
data: ['访问', '趋势'],
|
||||||
|
},
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{
|
||||||
|
name: '网页',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '移动端',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ipad',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '客户端',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '第三方',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '其它',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
radius: '60%',
|
||||||
|
splitNumber: 8,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
areaStyle: {
|
||||||
|
opacity: 1,
|
||||||
|
shadowBlur: 0,
|
||||||
|
shadowColor: 'rgba(0,0,0,.2)',
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowOffsetY: 10,
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
itemStyle: {
|
||||||
|
color: '#b6a2de',
|
||||||
|
},
|
||||||
|
name: '访问',
|
||||||
|
value: [90, 50, 86, 40, 50, 20],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5ab1ef',
|
||||||
|
},
|
||||||
|
name: '趋势',
|
||||||
|
value: [70, 75, 70, 76, 20, 85],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
// borderColor: '#fff',
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
symbolSize: 0,
|
||||||
|
type: 'radar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
animationDelay() {
|
||||||
|
return Math.random() * 400;
|
||||||
|
},
|
||||||
|
animationEasing: 'exponentialInOut',
|
||||||
|
animationType: 'scale',
|
||||||
|
center: ['50%', '50%'],
|
||||||
|
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||||
|
data: [
|
||||||
|
{ name: '外包', value: 500 },
|
||||||
|
{ name: '定制', value: 310 },
|
||||||
|
{ name: '技术支持', value: 274 },
|
||||||
|
{ name: '远程', value: 400 },
|
||||||
|
].toSorted((a, b) => {
|
||||||
|
return a.value - b.value;
|
||||||
|
}),
|
||||||
|
name: '商业占比',
|
||||||
|
radius: '80%',
|
||||||
|
roseType: 'radius',
|
||||||
|
type: 'pie',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
legend: {
|
||||||
|
bottom: '2%',
|
||||||
|
left: 'center',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
animationDelay() {
|
||||||
|
return Math.random() * 100;
|
||||||
|
},
|
||||||
|
animationEasing: 'exponentialInOut',
|
||||||
|
animationType: 'scale',
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||||
|
data: [
|
||||||
|
{ name: '搜索引擎', value: 1048 },
|
||||||
|
{ name: '直接访问', value: 735 },
|
||||||
|
{ name: '邮件营销', value: 580 },
|
||||||
|
{ name: '联盟广告', value: 484 },
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
fontSize: '12',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
// borderColor: '#fff',
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
position: 'center',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
name: '访问来源',
|
||||||
|
radius: ['40%', '65%'],
|
||||||
|
type: 'pie',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
55
apps/src/views/dashboard/analytics/analytics-visits.vue
Normal file
55
apps/src/views/dashboard/analytics/analytics-visits.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
renderEcharts({
|
||||||
|
grid: {
|
||||||
|
bottom: 0,
|
||||||
|
containLabel: true,
|
||||||
|
left: '1%',
|
||||||
|
right: '1%',
|
||||||
|
top: '2 %',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
barMaxWidth: 80,
|
||||||
|
// color: '#4f69fd',
|
||||||
|
data: [
|
||||||
|
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||||
|
3200, 4800,
|
||||||
|
],
|
||||||
|
type: 'bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
// color: '#4f69fd',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||||
|
type: 'category',
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
max: 8000,
|
||||||
|
splitNumber: 4,
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EchartsUI ref="chartRef" />
|
||||||
|
</template>
|
||||||
90
apps/src/views/dashboard/analytics/index.vue
Normal file
90
apps/src/views/dashboard/analytics/index.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||||
|
import type { TabOption } from '@vben/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnalysisChartCard,
|
||||||
|
AnalysisChartsTabs,
|
||||||
|
AnalysisOverview,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
SvgBellIcon,
|
||||||
|
SvgCakeIcon,
|
||||||
|
SvgCardIcon,
|
||||||
|
SvgDownloadIcon,
|
||||||
|
} from '@vben/icons';
|
||||||
|
|
||||||
|
import AnalyticsTrends from './analytics-trends.vue';
|
||||||
|
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||||
|
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||||
|
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||||
|
import AnalyticsVisits from './analytics-visits.vue';
|
||||||
|
|
||||||
|
const overviewItems: AnalysisOverviewItem[] = [
|
||||||
|
{
|
||||||
|
icon: SvgCardIcon,
|
||||||
|
title: '用户量',
|
||||||
|
totalTitle: '总用户量',
|
||||||
|
totalValue: 120_000,
|
||||||
|
value: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgCakeIcon,
|
||||||
|
title: '访问量',
|
||||||
|
totalTitle: '总访问量',
|
||||||
|
totalValue: 500_000,
|
||||||
|
value: 20_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgDownloadIcon,
|
||||||
|
title: '下载量',
|
||||||
|
totalTitle: '总下载量',
|
||||||
|
totalValue: 120_000,
|
||||||
|
value: 8000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgBellIcon,
|
||||||
|
title: '使用量',
|
||||||
|
totalTitle: '总使用量',
|
||||||
|
totalValue: 50_000,
|
||||||
|
value: 5000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const chartTabs: TabOption[] = [
|
||||||
|
{
|
||||||
|
label: '流量趋势',
|
||||||
|
value: 'trends',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '月访问量',
|
||||||
|
value: 'visits',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<AnalysisOverview :items="overviewItems" />
|
||||||
|
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||||
|
<template #trends>
|
||||||
|
<AnalyticsTrends />
|
||||||
|
</template>
|
||||||
|
<template #visits>
|
||||||
|
<AnalyticsVisits />
|
||||||
|
</template>
|
||||||
|
</AnalysisChartsTabs>
|
||||||
|
|
||||||
|
<div class="mt-5 w-full md:flex">
|
||||||
|
<AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问数量">
|
||||||
|
<AnalyticsVisitsData />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
<AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="访问来源">
|
||||||
|
<AnalyticsVisitsSource />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||||
|
<AnalyticsVisitsSales />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
266
apps/src/views/dashboard/workspace/index.vue
Normal file
266
apps/src/views/dashboard/workspace/index.vue
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type {
|
||||||
|
WorkbenchProjectItem,
|
||||||
|
WorkbenchQuickNavItem,
|
||||||
|
WorkbenchTodoItem,
|
||||||
|
WorkbenchTrendItem,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnalysisChartCard,
|
||||||
|
WorkbenchHeader,
|
||||||
|
WorkbenchProject,
|
||||||
|
WorkbenchQuickNav,
|
||||||
|
WorkbenchTodo,
|
||||||
|
WorkbenchTrends,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||||
|
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||||
|
// 例如:url: /dashboard/workspace
|
||||||
|
const projectItems: WorkbenchProjectItem[] = [
|
||||||
|
{
|
||||||
|
color: '',
|
||||||
|
content: '不要等待机会,而要创造机会。',
|
||||||
|
date: '2021-04-01',
|
||||||
|
group: '开源组',
|
||||||
|
icon: 'carbon:logo-github',
|
||||||
|
title: 'Github',
|
||||||
|
url: 'https://github.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#3fb27f',
|
||||||
|
content: '现在的你决定将来的你。',
|
||||||
|
date: '2021-04-01',
|
||||||
|
group: '算法组',
|
||||||
|
icon: 'ion:logo-vue',
|
||||||
|
title: 'Vue',
|
||||||
|
url: 'https://vuejs.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#e18525',
|
||||||
|
content: '没有什么才能比努力更重要。',
|
||||||
|
date: '2021-04-01',
|
||||||
|
group: '上班摸鱼',
|
||||||
|
icon: 'ion:logo-html5',
|
||||||
|
title: 'Html5',
|
||||||
|
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#bf0c2c',
|
||||||
|
content: '热情和欲望可以突破一切难关。',
|
||||||
|
date: '2021-04-01',
|
||||||
|
group: 'UI',
|
||||||
|
icon: 'ion:logo-angular',
|
||||||
|
title: 'Angular',
|
||||||
|
url: 'https://angular.io',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#00d8ff',
|
||||||
|
content: '健康的身体是实现目标的基石。',
|
||||||
|
date: '2021-04-01',
|
||||||
|
group: '技术牛',
|
||||||
|
icon: 'bx:bxl-react',
|
||||||
|
title: 'React',
|
||||||
|
url: 'https://reactjs.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#EBD94E',
|
||||||
|
content: '路是走出来的,而不是空想出来的。',
|
||||||
|
date: '2021-04-01',
|
||||||
|
group: '架构组',
|
||||||
|
icon: 'ion:logo-javascript',
|
||||||
|
title: 'Js',
|
||||||
|
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||||
|
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||||
|
{
|
||||||
|
color: '#1fdaca',
|
||||||
|
icon: 'ion:home-outline',
|
||||||
|
title: '首页',
|
||||||
|
url: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#bf0c2c',
|
||||||
|
icon: 'ion:grid-outline',
|
||||||
|
title: '仪表盘',
|
||||||
|
url: '/dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#e18525',
|
||||||
|
icon: 'ion:layers-outline',
|
||||||
|
title: '组件',
|
||||||
|
url: '/demos/features/icons',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#3fb27f',
|
||||||
|
icon: 'ion:settings-outline',
|
||||||
|
title: '系统管理',
|
||||||
|
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#4daf1bc9',
|
||||||
|
icon: 'ion:key-outline',
|
||||||
|
title: '权限管理',
|
||||||
|
url: '/demos/access/page-control',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#00d8ff',
|
||||||
|
icon: 'ion:bar-chart-outline',
|
||||||
|
title: '图表',
|
||||||
|
url: '/analytics',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||||
|
{
|
||||||
|
completed: false,
|
||||||
|
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||||
|
date: '2024-07-30 11:00:00',
|
||||||
|
title: '审查前端代码提交',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completed: true,
|
||||||
|
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||||
|
date: '2024-07-30 11:00:00',
|
||||||
|
title: '系统性能优化',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completed: false,
|
||||||
|
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||||
|
date: '2024-07-30 11:00:00',
|
||||||
|
title: '安全检查',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completed: false,
|
||||||
|
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||||
|
date: '2024-07-30 11:00:00',
|
||||||
|
title: '更新项目依赖',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
completed: false,
|
||||||
|
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||||
|
date: '2024-07-30 11:00:00',
|
||||||
|
title: '修复UI显示问题',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const trendItems: WorkbenchTrendItem[] = [
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-1',
|
||||||
|
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||||
|
date: '刚刚',
|
||||||
|
title: '威廉',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-2',
|
||||||
|
content: `关注了 <a>威廉</a> `,
|
||||||
|
date: '1个小时前',
|
||||||
|
title: '艾文',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-3',
|
||||||
|
content: `发布了 <a>个人动态</a> `,
|
||||||
|
date: '1天前',
|
||||||
|
title: '克里斯',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-4',
|
||||||
|
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||||
|
date: '2天前',
|
||||||
|
title: 'Vben',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-1',
|
||||||
|
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||||
|
date: '3天前',
|
||||||
|
title: '皮特',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-2',
|
||||||
|
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||||
|
date: '1周前',
|
||||||
|
title: '杰克',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-3',
|
||||||
|
content: `发布了 <a>个人动态</a> `,
|
||||||
|
date: '1周前',
|
||||||
|
title: '威廉',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-4',
|
||||||
|
content: `推送了代码到 <a>Github</a>`,
|
||||||
|
date: '2021-04-01 20:00',
|
||||||
|
title: '威廉',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'svg:avatar-4',
|
||||||
|
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||||
|
date: '2021-03-01 20:00',
|
||||||
|
title: 'Vben',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||||
|
// This is a sample method, adjust according to the actual project requirements
|
||||||
|
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||||
|
if (nav.url?.startsWith('http')) {
|
||||||
|
openWindow(nav.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nav.url?.startsWith('/')) {
|
||||||
|
router.push(nav.url).catch((error) => {
|
||||||
|
console.error('Navigation failed:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-5">
|
||||||
|
<WorkbenchHeader
|
||||||
|
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||||
|
</template>
|
||||||
|
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||||
|
</WorkbenchHeader>
|
||||||
|
|
||||||
|
<div class="mt-5 flex flex-col lg:flex-row">
|
||||||
|
<div class="mr-4 w-full lg:w-3/5">
|
||||||
|
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||||
|
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full lg:w-2/5">
|
||||||
|
<WorkbenchQuickNav
|
||||||
|
:items="quickNavItems"
|
||||||
|
class="mt-5 lg:mt-0"
|
||||||
|
title="快捷导航"
|
||||||
|
@click="navTo"
|
||||||
|
/>
|
||||||
|
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||||
|
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||||
|
<AnalyticsVisitsSource />
|
||||||
|
</AnalysisChartCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
66
apps/src/views/demos/antd/index.vue
Normal file
66
apps/src/views/demos/antd/index.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card, message, notification, Space } from 'antdv-next';
|
||||||
|
|
||||||
|
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||||
|
|
||||||
|
function info() {
|
||||||
|
message.info('How many roads must a man walk down');
|
||||||
|
}
|
||||||
|
|
||||||
|
function error() {
|
||||||
|
message.error({
|
||||||
|
content: 'Once upon a time you dressed so fine',
|
||||||
|
duration: 2.5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning() {
|
||||||
|
message.warning('How many roads must a man walk down');
|
||||||
|
}
|
||||||
|
function success() {
|
||||||
|
message.success('Cause you walked hand in hand With another man in my place');
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(type: NotificationType) {
|
||||||
|
notification[type]({
|
||||||
|
duration: 2.5,
|
||||||
|
title: '说点啥呢',
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="支持多语言,主题功能集成切换等"
|
||||||
|
title="Ant Design Vue组件使用演示"
|
||||||
|
>
|
||||||
|
<Card class="mb-5" title="按钮">
|
||||||
|
<Space>
|
||||||
|
<Button>Default</Button>
|
||||||
|
<Button type="primary"> Primary </Button>
|
||||||
|
<Button> Info </Button>
|
||||||
|
<Button danger> Error </Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card class="mb-5" title="Message">
|
||||||
|
<Space>
|
||||||
|
<Button @click="info"> 信息 </Button>
|
||||||
|
<Button danger @click="error"> 错误 </Button>
|
||||||
|
<Button @click="warning"> 警告 </Button>
|
||||||
|
<Button @click="success"> 成功 </Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-5" title="Notification">
|
||||||
|
<Space>
|
||||||
|
<Button @click="notify('info')"> 信息 </Button>
|
||||||
|
<Button danger @click="notify('error')"> 错误 </Button>
|
||||||
|
<Button @click="notify('warning')"> 警告 </Button>
|
||||||
|
<Button @click="notify('success')"> 成功 </Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
11
apps/tsconfig.json
Normal file
11
apps/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/web-app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"#/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
10
apps/tsconfig.node.json
Normal file
10
apps/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"noEmit": false
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
20
apps/vite.config.ts
Normal file
20
apps/vite.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from '@vben/vite-config';
|
||||||
|
|
||||||
|
export default defineConfig(async () => {
|
||||||
|
return {
|
||||||
|
application: {},
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
// mock代理目标地址
|
||||||
|
target: 'http://localhost:5320/api',
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
91
cspell.json
Normal file
91
cspell.json
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||||
|
"version": "0.2",
|
||||||
|
"language": "en,en-US",
|
||||||
|
"allowCompoundWords": true,
|
||||||
|
"words": [
|
||||||
|
"acmr",
|
||||||
|
"antd",
|
||||||
|
"antdv",
|
||||||
|
"archiver",
|
||||||
|
"astro",
|
||||||
|
"axios",
|
||||||
|
"brotli",
|
||||||
|
"cascader",
|
||||||
|
"chatcmpl",
|
||||||
|
"clsx",
|
||||||
|
"dedup",
|
||||||
|
"defu",
|
||||||
|
"demi",
|
||||||
|
"dotenv",
|
||||||
|
"echart",
|
||||||
|
"echarts",
|
||||||
|
"ependencies",
|
||||||
|
"esbuild",
|
||||||
|
"esno",
|
||||||
|
"etag",
|
||||||
|
"execa",
|
||||||
|
"iconify",
|
||||||
|
"iconoir",
|
||||||
|
"intlify",
|
||||||
|
"isequal",
|
||||||
|
"jspm",
|
||||||
|
"lockb",
|
||||||
|
"lucide",
|
||||||
|
"minh",
|
||||||
|
"minw",
|
||||||
|
"mkdist",
|
||||||
|
"mockjs",
|
||||||
|
"naiveui",
|
||||||
|
"napi",
|
||||||
|
"nocheck",
|
||||||
|
"nolebase",
|
||||||
|
"noopener",
|
||||||
|
"noreferrer",
|
||||||
|
"nprogress",
|
||||||
|
"nuxt",
|
||||||
|
"organisation",
|
||||||
|
"oxfmt",
|
||||||
|
"oxlint",
|
||||||
|
"pinia",
|
||||||
|
"prefixs",
|
||||||
|
"publint",
|
||||||
|
"qrcode",
|
||||||
|
"reka",
|
||||||
|
"rollup",
|
||||||
|
"shadcn",
|
||||||
|
"sonner",
|
||||||
|
"sortablejs",
|
||||||
|
"styl",
|
||||||
|
"tabler",
|
||||||
|
"taze",
|
||||||
|
"tdesign",
|
||||||
|
"tsdown",
|
||||||
|
"tsgolint",
|
||||||
|
"turborepo",
|
||||||
|
"ui-kit",
|
||||||
|
"uicons",
|
||||||
|
"unplugin",
|
||||||
|
"unref",
|
||||||
|
"vben",
|
||||||
|
"vbenjs",
|
||||||
|
"vite",
|
||||||
|
"vitejs",
|
||||||
|
"vitepress",
|
||||||
|
"vitest",
|
||||||
|
"vnode",
|
||||||
|
"vueuse",
|
||||||
|
"yxxx"
|
||||||
|
],
|
||||||
|
"ignorePaths": [
|
||||||
|
"**/*-dist/**",
|
||||||
|
"**/*.log",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/__tests__/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/icons/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"pnpm-lock.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
45
docs/.vitepress/components/demo-preview.vue
Normal file
45
docs/.vitepress/components/demo-preview.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import PreviewGroup from './preview-group.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
files?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), { files: '() => []' });
|
||||||
|
|
||||||
|
const parsedFiles = computed(() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(decodeURIComponent(props.files ?? ''));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative rounded-xl border border-border shadow-float">
|
||||||
|
<div
|
||||||
|
class="not-prose relative w-full overflow-x-auto rounded-t-lg px-4 py-6"
|
||||||
|
>
|
||||||
|
<div class="flex w-full max-w-[700px] px-2">
|
||||||
|
<ClientOnly>
|
||||||
|
<slot v-if="parsedFiles.length > 0"></slot>
|
||||||
|
<div v-else class="text-sm text-destructive">
|
||||||
|
<span class="rounded-sm bg-destructive p-1 text-foreground">
|
||||||
|
ERROR:
|
||||||
|
</span>
|
||||||
|
The preview directory does not exist. Please check the 'dir'
|
||||||
|
parameter.
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PreviewGroup v-if="parsedFiles.length > 0" :files="parsedFiles">
|
||||||
|
<template v-for="file in parsedFiles" #[file]>
|
||||||
|
<slot :name="file"></slot>
|
||||||
|
</template>
|
||||||
|
</PreviewGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
1
docs/.vitepress/components/index.ts
Normal file
1
docs/.vitepress/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as DemoPreview } from './demo-preview.vue';
|
||||||
110
docs/.vitepress/components/preview-group.vue
Normal file
110
docs/.vitepress/components/preview-group.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SetupContext } from 'vue';
|
||||||
|
|
||||||
|
import { computed, ref, useSlots } from 'vue';
|
||||||
|
|
||||||
|
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { Code } from 'lucide-vue-next';
|
||||||
|
import {
|
||||||
|
TabsContent,
|
||||||
|
TabsIndicator,
|
||||||
|
TabsList,
|
||||||
|
TabsRoot,
|
||||||
|
TabsTrigger,
|
||||||
|
} from 'reka-ui';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
files?: string[];
|
||||||
|
}>(),
|
||||||
|
{ files: () => [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
|
const slots: SetupContext['slots'] = useSlots();
|
||||||
|
|
||||||
|
const tabs = computed(() => {
|
||||||
|
return props.files.map((file) => {
|
||||||
|
return {
|
||||||
|
component: slots[file],
|
||||||
|
label: file,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentTab = ref('index.vue');
|
||||||
|
|
||||||
|
const toggleOpen = () => {
|
||||||
|
open.value = !open.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TabsRoot
|
||||||
|
v-model="currentTab"
|
||||||
|
class="overflow-hidden rounded-b-xl border-t border-border bg-background-deep"
|
||||||
|
@update:model-value="open = true"
|
||||||
|
>
|
||||||
|
<div class="flex border-b-2 border-border bg-background pr-2">
|
||||||
|
<div class="flex w-full items-center justify-between text-[13px]">
|
||||||
|
<TabsList class="relative flex">
|
||||||
|
<template v-if="open">
|
||||||
|
<TabsIndicator
|
||||||
|
class="absolute bottom-0 left-0 h-[2px] w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) rounded-full transition-[width,transform] duration-300"
|
||||||
|
>
|
||||||
|
<div class="size-full bg-(--vp-c-indigo-1)"></div>
|
||||||
|
</TabsIndicator>
|
||||||
|
<TabsTrigger
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="index"
|
||||||
|
:value="tab.label"
|
||||||
|
class="border-box px-4 py-3 text-foreground data-[state=active]:text-(--vp-c-indigo-1)"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</TabsTrigger>
|
||||||
|
</template>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'py-2': !open,
|
||||||
|
}"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<VbenTooltip side="top">
|
||||||
|
<template #trigger>
|
||||||
|
<Code
|
||||||
|
class="size-7 cursor-pointer rounded-full p-1.5 hover:bg-accent"
|
||||||
|
@click="toggleOpen"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ open ? 'Collapse code' : 'Expand code' }}
|
||||||
|
</VbenTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="`${open ? 'h-[unset] max-h-[80vh]' : 'h-0'}`"
|
||||||
|
class="block overflow-y-scroll bg-(--vp-code-block-bg) transition-all duration-300"
|
||||||
|
>
|
||||||
|
<TabsContent
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.label"
|
||||||
|
:value="tab.label"
|
||||||
|
as-child
|
||||||
|
class="rounded-xl"
|
||||||
|
>
|
||||||
|
<div class="relative rounded-xl text-foreground">
|
||||||
|
<component :is="tab.component" class="border-0" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</TabsRoot>
|
||||||
|
</template>
|
||||||
302
docs/.vitepress/config/en.mts
Normal file
302
docs/.vitepress/config/en.mts
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import type { DefaultTheme } from 'vitepress';
|
||||||
|
|
||||||
|
import { defineConfig } from 'vitepress';
|
||||||
|
|
||||||
|
import { version } from '../../../package.json';
|
||||||
|
|
||||||
|
export const en = defineConfig({
|
||||||
|
description: 'Vben Admin & Enterprise level management system framework',
|
||||||
|
lang: 'en-US',
|
||||||
|
themeConfig: {
|
||||||
|
darkModeSwitchLabel: 'Theme',
|
||||||
|
darkModeSwitchTitle: 'Switch to Dark Mode',
|
||||||
|
docFooter: {
|
||||||
|
next: 'Next Page',
|
||||||
|
prev: 'Previous Page',
|
||||||
|
},
|
||||||
|
editLink: {
|
||||||
|
pattern:
|
||||||
|
'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
|
||||||
|
text: 'Edit this page on GitHub',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
|
||||||
|
message: 'Released under the MIT License.',
|
||||||
|
},
|
||||||
|
langMenuLabel: 'Language',
|
||||||
|
lastUpdated: {
|
||||||
|
formatOptions: {
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'medium',
|
||||||
|
},
|
||||||
|
text: 'Last updated on',
|
||||||
|
},
|
||||||
|
lightModeSwitchTitle: 'Switch to Light Mode',
|
||||||
|
nav: nav(),
|
||||||
|
outline: {
|
||||||
|
label: 'Navigate',
|
||||||
|
},
|
||||||
|
returnToTopLabel: 'Back to top',
|
||||||
|
sidebar: {
|
||||||
|
'/en/commercial/': {
|
||||||
|
base: '/en/commercial/',
|
||||||
|
items: sidebarCommercial(),
|
||||||
|
},
|
||||||
|
'/en/components/': {
|
||||||
|
base: '/en/components/',
|
||||||
|
items: sidebarComponents(),
|
||||||
|
},
|
||||||
|
'/en/guide/': { base: '/en/guide/', items: sidebarGuide() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
collapsed: false,
|
||||||
|
text: 'Introduction',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'introduction/vben',
|
||||||
|
text: 'About Vben Admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'introduction/why',
|
||||||
|
text: 'Why Choose Us?',
|
||||||
|
},
|
||||||
|
{ link: 'introduction/quick-start', text: 'Quick Start' },
|
||||||
|
{ link: 'introduction/thin', text: 'Lite Version' },
|
||||||
|
{
|
||||||
|
base: '/',
|
||||||
|
link: 'components/introduction',
|
||||||
|
text: 'Components',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Basics',
|
||||||
|
items: [
|
||||||
|
{ link: 'essentials/concept', text: 'Basic Concepts' },
|
||||||
|
{ link: 'essentials/development', text: 'Local Development' },
|
||||||
|
{ link: 'essentials/route', text: 'Routing and Menu' },
|
||||||
|
{ link: 'essentials/settings', text: 'Configuration' },
|
||||||
|
{ link: 'essentials/icons', text: 'Icons' },
|
||||||
|
{ link: 'essentials/styles', text: 'Styles' },
|
||||||
|
{ link: 'essentials/external-module', text: 'External Modules' },
|
||||||
|
{ link: 'essentials/build', text: 'Build and Deployment' },
|
||||||
|
{ link: 'essentials/server', text: 'Server Interaction and Data Mock' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Advanced',
|
||||||
|
items: [
|
||||||
|
{ link: 'in-depth/login', text: 'Login' },
|
||||||
|
{ link: 'in-depth/theme', text: 'Theme' },
|
||||||
|
{ link: 'in-depth/access', text: 'Access Control' },
|
||||||
|
{ link: 'in-depth/locale', text: 'Internationalization' },
|
||||||
|
{ link: 'in-depth/features', text: 'Common Features' },
|
||||||
|
{ link: 'in-depth/check-updates', text: 'Check Updates' },
|
||||||
|
{ link: 'in-depth/loading', text: 'Global Loading' },
|
||||||
|
{ link: 'in-depth/ui-framework', text: 'UI Framework Switching' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Engineering',
|
||||||
|
items: [
|
||||||
|
{ link: 'project/standard', text: 'Standards' },
|
||||||
|
{ link: 'project/cli', text: 'CLI' },
|
||||||
|
{ link: 'project/dir', text: 'Directory Explanation' },
|
||||||
|
{ link: 'project/test', text: 'Unit Testing' },
|
||||||
|
{ link: 'project/tailwindcss', text: 'Tailwind CSS' },
|
||||||
|
{ link: 'project/changeset', text: 'Changeset' },
|
||||||
|
{ link: 'project/vite', text: 'Vite Config' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Others',
|
||||||
|
items: [
|
||||||
|
{ link: 'other/project-update', text: 'Project Update' },
|
||||||
|
{ link: 'other/remove-code', text: 'Remove Code' },
|
||||||
|
{ link: 'other/faq', text: 'FAQ' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
link: 'community',
|
||||||
|
text: 'Community',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'technical-support',
|
||||||
|
text: 'Technical-support',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'customized',
|
||||||
|
text: 'Customized',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Components',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'introduction',
|
||||||
|
text: 'Introduction',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collapsed: false,
|
||||||
|
text: 'Layout UI',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'layout-ui/page',
|
||||||
|
text: 'Page',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collapsed: false,
|
||||||
|
text: 'Common UI',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-api-component',
|
||||||
|
text: 'ApiComponent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-alert',
|
||||||
|
text: 'Alert',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-modal',
|
||||||
|
text: 'Modal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-drawer',
|
||||||
|
text: 'Drawer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-form',
|
||||||
|
text: 'Form',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-vxe-table',
|
||||||
|
text: 'Vxe Table',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-count-to-animator',
|
||||||
|
text: 'CountToAnimator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-ellipsis-text',
|
||||||
|
text: 'EllipsisText',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function nav(): DefaultTheme.NavItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
activeMatch: '^/en/(guide|components)/',
|
||||||
|
text: 'Doc',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
activeMatch: '^/en/guide/',
|
||||||
|
link: '/en/guide/introduction/vben',
|
||||||
|
text: 'Guide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activeMatch: '^/en/components/',
|
||||||
|
link: '/en/components/introduction',
|
||||||
|
text: 'Components',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Historical Versions',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://doc.vvbin.cn',
|
||||||
|
text: '2.x Version Documentation',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Demo',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Vben Admin',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://www.vben.pro',
|
||||||
|
text: 'Demo Version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://ant.vben.pro',
|
||||||
|
text: 'Ant Design Vue Version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://naive.vben.pro',
|
||||||
|
text: 'Naive Version',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://ele.vben.pro',
|
||||||
|
text: 'Element Plus Version',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Others',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://vben.vvbin.cn',
|
||||||
|
text: 'Vben Admin 2.x',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: version,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://github.com/vbenjs/vue-vben-admin/releases',
|
||||||
|
text: 'Changelog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://github.com/orgs/vbenjs/projects/5',
|
||||||
|
text: 'Roadmap',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
|
||||||
|
text: 'Contribution',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/commercial/technical-support',
|
||||||
|
text: '🦄 Tech Support',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/sponsor/personal',
|
||||||
|
text: '✨ Sponsor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/commercial/community',
|
||||||
|
text: '👨👦👦 Community',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// link: '/friend-links/',
|
||||||
|
// text: '🤝 Friend Links',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
}
|
||||||
25
docs/.vitepress/config/index.mts
Normal file
25
docs/.vitepress/config/index.mts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { withPwa } from '@vite-pwa/vitepress';
|
||||||
|
import { defineConfigWithTheme } from 'vitepress';
|
||||||
|
|
||||||
|
import { en } from './en.mts';
|
||||||
|
import { shared } from './shared.mts';
|
||||||
|
import { zh } from './zh.mts';
|
||||||
|
|
||||||
|
export default withPwa(
|
||||||
|
defineConfigWithTheme({
|
||||||
|
...shared,
|
||||||
|
locales: {
|
||||||
|
en: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en',
|
||||||
|
link: '/en/',
|
||||||
|
...en,
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
label: '简体中文',
|
||||||
|
lang: 'zh-CN',
|
||||||
|
...zh,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
142
docs/.vitepress/config/plugins/demo-preview.ts
Normal file
142
docs/.vitepress/config/plugins/demo-preview.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
|
||||||
|
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { readdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
export const rawPathRegexp =
|
||||||
|
/^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
|
||||||
|
|
||||||
|
function rawPathToToken(rawPath: string) {
|
||||||
|
const [
|
||||||
|
filepath = '',
|
||||||
|
extension = '',
|
||||||
|
region = '',
|
||||||
|
lines = '',
|
||||||
|
lang = '',
|
||||||
|
rawTitle = '',
|
||||||
|
] = (rawPathRegexp.exec(rawPath) || []).slice(1);
|
||||||
|
|
||||||
|
const title = rawTitle || filepath.split('/').pop() || '';
|
||||||
|
|
||||||
|
return { extension, filepath, lang, lines, region, title };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const demoPreviewPlugin = (md: MarkdownRenderer) => {
|
||||||
|
md.core.ruler.after('inline', 'demo-preview', (state) => {
|
||||||
|
const insertComponentImport = (importString: string) => {
|
||||||
|
const index = state.tokens.findIndex(
|
||||||
|
(i) => i.type === 'html_block' && i.content.match(/<script setup>/g),
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
const importComponent = new state.Token('html_block', '', 0);
|
||||||
|
importComponent.content = `<script setup>\n${importString}\n</script>\n`;
|
||||||
|
state.tokens.splice(0, 0, importComponent);
|
||||||
|
} else {
|
||||||
|
if (state.tokens[index]) {
|
||||||
|
const content = state.tokens[index].content;
|
||||||
|
state.tokens[index].content = content.replace(
|
||||||
|
'</script>',
|
||||||
|
`${importString}\n</script>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Define the regular expression to match the desired pattern
|
||||||
|
const regex = /<DemoPreview[^>]*\sdir="([^"]*)"/g;
|
||||||
|
// Iterate through the Markdown content and replace the pattern
|
||||||
|
state.src = state.src.replaceAll(regex, (_match, dir) => {
|
||||||
|
const componentDir = join(process.cwd(), 'src', dir).replaceAll(
|
||||||
|
'\\',
|
||||||
|
'/',
|
||||||
|
);
|
||||||
|
|
||||||
|
let childFiles: string[] = [];
|
||||||
|
let dirExists = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
childFiles =
|
||||||
|
readdirSync(componentDir, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: false,
|
||||||
|
}) || [];
|
||||||
|
} catch {
|
||||||
|
dirExists = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dirExists) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueWord = generateContentHash(componentDir);
|
||||||
|
|
||||||
|
const ComponentName = `DemoComponent_${uniqueWord}`;
|
||||||
|
insertComponentImport(
|
||||||
|
`import ${ComponentName} from '${componentDir}/index.vue'`,
|
||||||
|
);
|
||||||
|
const { path: _path } = state.env as MarkdownEnv;
|
||||||
|
|
||||||
|
const index = state.tokens.findIndex((i) => i.content.match(regex));
|
||||||
|
|
||||||
|
if (!state.tokens[index]) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const firstString = 'index.vue';
|
||||||
|
childFiles = childFiles.toSorted((a, b) => {
|
||||||
|
if (a === firstString) return -1;
|
||||||
|
if (b === firstString) return 1;
|
||||||
|
return a.localeCompare(b, 'en', { sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
state.tokens[index].content =
|
||||||
|
`<DemoPreview files="${encodeURIComponent(JSON.stringify(childFiles))}" ><${ComponentName}/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const _dummyToken = new state.Token('', '', 0);
|
||||||
|
const tokenArray: Array<typeof _dummyToken> = [];
|
||||||
|
childFiles.forEach((filename) => {
|
||||||
|
// const slotName = filename.replace(extname(filename), '');
|
||||||
|
|
||||||
|
const templateStart = new state.Token('html_inline', '', 0);
|
||||||
|
templateStart.content = `<template #${filename}>`;
|
||||||
|
tokenArray.push(templateStart);
|
||||||
|
|
||||||
|
const resolvedPath = join(componentDir, filename);
|
||||||
|
|
||||||
|
const { extension, filepath, lang, lines, title } =
|
||||||
|
rawPathToToken(resolvedPath);
|
||||||
|
// Add code tokens for each line
|
||||||
|
const token = new state.Token('fence', 'code', 0);
|
||||||
|
token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
|
||||||
|
title ? `[${title}]` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
token.content = `<<< ${filepath}`;
|
||||||
|
(token as any).src = [resolvedPath];
|
||||||
|
tokenArray.push(token);
|
||||||
|
|
||||||
|
const templateEnd = new state.Token('html_inline', '', 0);
|
||||||
|
templateEnd.content = '</template>';
|
||||||
|
tokenArray.push(templateEnd);
|
||||||
|
});
|
||||||
|
const endTag = new state.Token('html_inline', '', 0);
|
||||||
|
endTag.content = '</DemoPreview>';
|
||||||
|
tokenArray.push(endTag);
|
||||||
|
|
||||||
|
state.tokens.splice(index + 1, 0, ...tokenArray);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
|
||||||
|
// );
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateContentHash(input: string, length: number = 10): string {
|
||||||
|
// 使用 SHA-256 生成哈希值
|
||||||
|
const hash = crypto.createHash('sha256').update(input).digest('hex');
|
||||||
|
|
||||||
|
// 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
|
||||||
|
return Number.parseInt(hash, 16).toString(36).slice(0, length);
|
||||||
|
}
|
||||||
170
docs/.vitepress/config/shared.mts
Normal file
170
docs/.vitepress/config/shared.mts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import type { PwaOptions } from '@vite-pwa/vitepress';
|
||||||
|
import type { HeadConfig } from 'vitepress';
|
||||||
|
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
viteArchiverPlugin,
|
||||||
|
viteVxeTableImportsPlugin,
|
||||||
|
} from '@vben/vite-config';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GitChangelog,
|
||||||
|
GitChangelogMarkdownSection,
|
||||||
|
} from '@nolebase/vitepress-plugin-git-changelog/vite';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { defineConfig, postcssIsolateStyles } from 'vitepress';
|
||||||
|
import {
|
||||||
|
groupIconMdPlugin,
|
||||||
|
groupIconVitePlugin,
|
||||||
|
} from 'vitepress-plugin-group-icons';
|
||||||
|
|
||||||
|
import { demoPreviewPlugin } from './plugins/demo-preview';
|
||||||
|
import { search as zhSearch } from './zh.mts';
|
||||||
|
|
||||||
|
export const shared = defineConfig({
|
||||||
|
appearance: 'dark',
|
||||||
|
head: head(),
|
||||||
|
markdown: {
|
||||||
|
preConfig(md) {
|
||||||
|
md.use(demoPreviewPlugin);
|
||||||
|
md.use(groupIconMdPlugin);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pwa: pwa(),
|
||||||
|
srcDir: 'src',
|
||||||
|
themeConfig: {
|
||||||
|
i18nRouting: true,
|
||||||
|
logo: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
|
search: {
|
||||||
|
options: {
|
||||||
|
locales: {
|
||||||
|
...zhSearch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: 'local',
|
||||||
|
},
|
||||||
|
siteTitle: 'Vben Admin',
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/vbenjs/vue-vben-admin' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
title: 'Vben Admin',
|
||||||
|
vite: {
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: Infinity,
|
||||||
|
minify: 'terser',
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] })],
|
||||||
|
},
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: 'modern',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
stringify: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
GitChangelog({
|
||||||
|
mapAuthors: [
|
||||||
|
{
|
||||||
|
mapByNameAliases: ['Vben'],
|
||||||
|
name: 'vben',
|
||||||
|
username: 'anncwb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vince',
|
||||||
|
username: 'vince292007',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Li Kui',
|
||||||
|
username: 'likui628',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
|
||||||
|
}),
|
||||||
|
GitChangelogMarkdownSection(),
|
||||||
|
viteArchiverPlugin({ outputDir: '.vitepress' }),
|
||||||
|
groupIconVitePlugin(),
|
||||||
|
await viteVxeTableImportsPlugin(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
allow: ['../..'],
|
||||||
|
},
|
||||||
|
host: true,
|
||||||
|
port: 6173,
|
||||||
|
},
|
||||||
|
|
||||||
|
ssr: {
|
||||||
|
external: ['@vue/repl'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function head(): HeadConfig[] {
|
||||||
|
return [
|
||||||
|
['meta', { content: 'Vbenjs Team', name: 'author' }],
|
||||||
|
[
|
||||||
|
'meta',
|
||||||
|
{
|
||||||
|
content: 'vben, vitejs, vite, shacdn-ui, vue',
|
||||||
|
name: 'keywords',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['link', { href: '/favicon.ico', rel: 'icon', type: 'image/svg+xml' }],
|
||||||
|
[
|
||||||
|
'meta',
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no',
|
||||||
|
name: 'viewport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['meta', { content: 'vben admin docs', name: 'keywords' }],
|
||||||
|
['link', { href: '/favicon.ico', rel: 'icon' }],
|
||||||
|
// [
|
||||||
|
// 'script',
|
||||||
|
// {
|
||||||
|
// src: 'https://cdn.tailwindcss.com',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pwa(): PwaOptions {
|
||||||
|
return {
|
||||||
|
includeManifestIcons: false,
|
||||||
|
manifest: {
|
||||||
|
description:
|
||||||
|
'Vben Admin is a modern admin dashboard template based on Vue 3. ',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
sizes: '192x192',
|
||||||
|
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sizes: '512x512',
|
||||||
|
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: '/',
|
||||||
|
name: 'Vben Admin Doc',
|
||||||
|
short_name: 'vben_admin_doc',
|
||||||
|
theme_color: '#ffffff',
|
||||||
|
},
|
||||||
|
outDir: resolve(process.cwd(), '.vitepress/dist'),
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{css,js,html,svg,png,ico,txt,woff2}'],
|
||||||
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
358
docs/.vitepress/config/zh.mts
Normal file
358
docs/.vitepress/config/zh.mts
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
import type { DefaultTheme } from 'vitepress';
|
||||||
|
|
||||||
|
import { defineConfig } from 'vitepress';
|
||||||
|
|
||||||
|
import { version } from '../../../package.json';
|
||||||
|
|
||||||
|
export const zh = defineConfig({
|
||||||
|
description: 'Vben Admin & 企业级管理系统框架',
|
||||||
|
lang: 'zh-Hans',
|
||||||
|
themeConfig: {
|
||||||
|
darkModeSwitchLabel: '主题',
|
||||||
|
darkModeSwitchTitle: '切换到深色模式',
|
||||||
|
docFooter: {
|
||||||
|
next: '下一页',
|
||||||
|
prev: '上一页',
|
||||||
|
},
|
||||||
|
editLink: {
|
||||||
|
pattern:
|
||||||
|
'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
|
||||||
|
text: '在 GitHub 上编辑此页面',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
|
||||||
|
message: '基于 MIT 许可发布.',
|
||||||
|
},
|
||||||
|
langMenuLabel: '多语言',
|
||||||
|
lastUpdated: {
|
||||||
|
formatOptions: {
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'medium',
|
||||||
|
},
|
||||||
|
text: '最后更新于',
|
||||||
|
},
|
||||||
|
lightModeSwitchTitle: '切换到浅色模式',
|
||||||
|
nav: nav(),
|
||||||
|
|
||||||
|
outline: {
|
||||||
|
label: '页面导航',
|
||||||
|
},
|
||||||
|
returnToTopLabel: '回到顶部',
|
||||||
|
|
||||||
|
sidebar: {
|
||||||
|
'/commercial/': { base: '/commercial/', items: sidebarCommercial() },
|
||||||
|
'/components/': { base: '/components/', items: sidebarComponents() },
|
||||||
|
'/guide/': { base: '/guide/', items: sidebarGuide() },
|
||||||
|
},
|
||||||
|
sidebarMenuLabel: '菜单',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
collapsed: false,
|
||||||
|
text: '简介',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'introduction/vben',
|
||||||
|
text: '关于 Vben Admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'introduction/why',
|
||||||
|
text: '为什么选择我们?',
|
||||||
|
},
|
||||||
|
{ link: 'introduction/quick-start', text: '快速开始' },
|
||||||
|
{ link: 'introduction/thin', text: '精简版本' },
|
||||||
|
{
|
||||||
|
base: '/',
|
||||||
|
link: 'components/introduction',
|
||||||
|
text: '组件文档',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '基础',
|
||||||
|
items: [
|
||||||
|
{ link: 'essentials/concept', text: '基础概念' },
|
||||||
|
{ link: 'essentials/development', text: '本地开发' },
|
||||||
|
{ link: 'essentials/route', text: '路由和菜单' },
|
||||||
|
{ link: 'essentials/settings', text: '配置' },
|
||||||
|
{ link: 'essentials/icons', text: '图标' },
|
||||||
|
{ link: 'essentials/styles', text: '样式' },
|
||||||
|
{ link: 'essentials/external-module', text: '外部模块' },
|
||||||
|
{ link: 'essentials/build', text: '构建与部署' },
|
||||||
|
{ link: 'essentials/server', text: '服务端交互与数据Mock' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '深入',
|
||||||
|
items: [
|
||||||
|
{ link: 'in-depth/login', text: '登录' },
|
||||||
|
// { link: 'in-depth/layout', text: '布局' },
|
||||||
|
{ link: 'in-depth/theme', text: '主题' },
|
||||||
|
{ link: 'in-depth/access', text: '权限' },
|
||||||
|
{ link: 'in-depth/locale', text: '国际化' },
|
||||||
|
{ link: 'in-depth/features', text: '常用功能' },
|
||||||
|
{ link: 'in-depth/check-updates', text: '检查更新' },
|
||||||
|
{ link: 'in-depth/loading', text: '全局loading' },
|
||||||
|
{ link: 'in-depth/ui-framework', text: '组件库切换' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '工程',
|
||||||
|
items: [
|
||||||
|
{ link: 'project/standard', text: '规范' },
|
||||||
|
{ link: 'project/cli', text: 'CLI' },
|
||||||
|
{ link: 'project/dir', text: '目录说明' },
|
||||||
|
{ link: 'project/test', text: '单元测试' },
|
||||||
|
{ link: 'project/tailwindcss', text: 'Tailwind CSS' },
|
||||||
|
{ link: 'project/changeset', text: 'Changeset' },
|
||||||
|
{ link: 'project/vite', text: 'Vite Config' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '其他',
|
||||||
|
items: [
|
||||||
|
{ link: 'other/project-update', text: '项目更新' },
|
||||||
|
{ link: 'other/remove-code', text: '移除代码' },
|
||||||
|
{ link: 'other/faq', text: '常见问题' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
link: 'community',
|
||||||
|
text: '交流群',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'technical-support',
|
||||||
|
text: '技术支持',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'customized',
|
||||||
|
text: '定制开发',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: '组件',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'introduction',
|
||||||
|
text: '介绍',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collapsed: false,
|
||||||
|
text: '布局组件',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'layout-ui/page',
|
||||||
|
text: 'Page 页面',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collapsed: false,
|
||||||
|
text: '通用组件',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-api-component',
|
||||||
|
text: 'ApiComponent Api组件包装器',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-alert',
|
||||||
|
text: 'Alert 轻量提示框',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-modal',
|
||||||
|
text: 'Modal 模态框',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-drawer',
|
||||||
|
text: 'Drawer 抽屉',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-form',
|
||||||
|
text: 'Form 表单',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-vxe-table',
|
||||||
|
text: 'Vxe Table 表格',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-count-to-animator',
|
||||||
|
text: 'CountToAnimator 数字动画',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-ellipsis-text',
|
||||||
|
text: 'EllipsisText 省略文本',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function nav(): DefaultTheme.NavItem[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
activeMatch: '^/(guide|components)/',
|
||||||
|
text: '文档',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
activeMatch: '^/guide/',
|
||||||
|
link: '/guide/introduction/vben',
|
||||||
|
text: '指南',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activeMatch: '^/components/',
|
||||||
|
link: '/components/introduction',
|
||||||
|
text: '组件',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '历史版本',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://doc.vvbin.cn',
|
||||||
|
text: '2.x版本文档',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '演示',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
text: 'Vben Admin',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://www.vben.pro',
|
||||||
|
text: '演示版本',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://ant.vben.pro',
|
||||||
|
text: 'Ant Design Vue 版本',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://naive.vben.pro',
|
||||||
|
text: 'Naive 版本',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://ele.vben.pro',
|
||||||
|
text: 'Element Plus版本',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '其他',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://vben.vvbin.cn',
|
||||||
|
text: 'Vben Admin 2.x',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: version,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
link: 'https://github.com/vbenjs/vue-vben-admin/releases',
|
||||||
|
text: '更新日志',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://github.com/orgs/vbenjs/projects/5',
|
||||||
|
text: '路线图',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
|
||||||
|
text: '贡献',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/commercial/technical-support',
|
||||||
|
text: '🦄 技术支持',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/sponsor/personal',
|
||||||
|
text: '✨ 赞助',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/commercial/community',
|
||||||
|
text: '👨👦👦 交流群',
|
||||||
|
// items: [
|
||||||
|
// {
|
||||||
|
// link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
|
||||||
|
// text: 'QQ频道',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// link: 'https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=mjZmlhgVzzUxvdxllB6C1vHpX8O8QRL0&authKey=DBdFbBwERmfaKY95JvRWqLCJIRGJAmKyZbrpzZ41EKDMZ5SR6MfbjOBaaNRN73fr&noverify=0&group_code=4286109',
|
||||||
|
// text: 'QQ群',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// link: 'https://discord.gg/VU62jTecad',
|
||||||
|
// text: 'Discord',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// link: '/friend-links/',
|
||||||
|
// text: '🤝 友情链接',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = {
|
||||||
|
root: {
|
||||||
|
placeholder: '搜索文档',
|
||||||
|
translations: {
|
||||||
|
button: {
|
||||||
|
buttonAriaLabel: '搜索文档',
|
||||||
|
buttonText: '搜索文档',
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
errorScreen: {
|
||||||
|
helpText: '你可能需要检查你的网络连接',
|
||||||
|
titleText: '无法获取结果',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
closeText: '关闭',
|
||||||
|
navigateText: '切换',
|
||||||
|
searchByText: '搜索提供者',
|
||||||
|
selectText: '选择',
|
||||||
|
},
|
||||||
|
noResultsScreen: {
|
||||||
|
noResultsText: '无法找到相关结果',
|
||||||
|
reportMissingResultsLinkText: '点击反馈',
|
||||||
|
reportMissingResultsText: '你认为该查询应该有结果?',
|
||||||
|
suggestedQueryText: '你可以尝试查询',
|
||||||
|
},
|
||||||
|
searchBox: {
|
||||||
|
cancelButtonAriaLabel: '取消',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
resetButtonAriaLabel: '清除查询条件',
|
||||||
|
resetButtonTitle: '清除查询条件',
|
||||||
|
},
|
||||||
|
startScreen: {
|
||||||
|
favoriteSearchesTitle: '收藏',
|
||||||
|
noRecentSearchesText: '没有搜索历史',
|
||||||
|
recentSearchesTitle: '搜索历史',
|
||||||
|
removeFavoriteSearchButtonTitle: '从收藏中移除',
|
||||||
|
removeRecentSearchButtonTitle: '从搜索历史中移除',
|
||||||
|
saveRecentSearchButtonTitle: '保存至搜索历史',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
96
docs/.vitepress/theme/components/site-layout.vue
Normal file
96
docs/.vitepress/theme/components/site-layout.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
// import { useAntdDesignTokens } from '@vben/hooks';
|
||||||
|
// import { initPreferences } from '@vben/preferences';
|
||||||
|
import { ConfigProvider, theme } from 'ant-design-vue';
|
||||||
|
import mediumZoom from 'medium-zoom';
|
||||||
|
import { useRoute } from 'vitepress';
|
||||||
|
import DefaultTheme from 'vitepress/theme';
|
||||||
|
|
||||||
|
const { Layout } = DefaultTheme;
|
||||||
|
const route = useRoute();
|
||||||
|
// const { tokens } = useAntdDesignTokens();
|
||||||
|
|
||||||
|
const initZoom = () => {
|
||||||
|
// mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' });
|
||||||
|
mediumZoom('.VPContent img', { background: 'var(--vp-c-bg)' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDark = ref(true);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => nextTick(() => initZoom()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// initPreferences({
|
||||||
|
// namespace: 'docs',
|
||||||
|
// });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用该函数
|
||||||
|
const observer = watchDarkModeChange((dark) => {
|
||||||
|
isDark.value = dark;
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer?.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
function watchDarkModeChange(callback: (isDark: boolean) => void) {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const isDark = htmlElement.classList.contains('dark');
|
||||||
|
callback(isDark);
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(htmlElement, {
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
attributes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialIsDark = htmlElement.classList.contains('dark');
|
||||||
|
callback(initialIsDark);
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenTheme = computed(() => {
|
||||||
|
const algorithm = isDark.value
|
||||||
|
? [theme.darkAlgorithm]
|
||||||
|
: [theme.defaultAlgorithm];
|
||||||
|
|
||||||
|
return {
|
||||||
|
algorithm,
|
||||||
|
// token: tokens,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfigProvider :theme="tokenTheme">
|
||||||
|
<Layout />
|
||||||
|
</ConfigProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.medium-zoom-overlay,
|
||||||
|
.medium-zoom-image--opened {
|
||||||
|
z-index: 2147483647;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
docs/.vitepress/theme/components/vben-contributors.vue
Normal file
29
docs/.vitepress/theme/components/vben-contributors.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="vben-contributors vp-doc">
|
||||||
|
<p>Contributors</p>
|
||||||
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
|
<img
|
||||||
|
alt="Contributors"
|
||||||
|
src="https://opencollective.com/vbenjs/contributors.svg?button=false"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vben-contributors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 60px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
docs/.vitepress/theme/index.ts
Normal file
29
docs/.vitepress/theme/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import type { EnhanceAppContext, Theme } from 'vitepress';
|
||||||
|
|
||||||
|
import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
|
||||||
|
import DefaultTheme from 'vitepress/theme';
|
||||||
|
|
||||||
|
import { DemoPreview } from '../components';
|
||||||
|
import SiteLayout from './components/site-layout.vue';
|
||||||
|
import VbenContributors from './components/vben-contributors.vue';
|
||||||
|
import { initHmPlugin } from './plugins/hm';
|
||||||
|
|
||||||
|
import './styles';
|
||||||
|
|
||||||
|
import 'virtual:group-icons.css';
|
||||||
|
import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async enhanceApp(ctx: EnhanceAppContext) {
|
||||||
|
const { app } = ctx;
|
||||||
|
app.component('VbenContributors', VbenContributors);
|
||||||
|
app.component('DemoPreview', DemoPreview);
|
||||||
|
app.use(NolebaseGitChangelogPlugin);
|
||||||
|
|
||||||
|
// 百度统计
|
||||||
|
initHmPlugin();
|
||||||
|
},
|
||||||
|
extends: DefaultTheme,
|
||||||
|
Layout: SiteLayout,
|
||||||
|
} satisfies Theme;
|
||||||
28
docs/.vitepress/theme/plugins/hm.ts
Normal file
28
docs/.vitepress/theme/plugins/hm.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { inBrowser } from 'vitepress';
|
||||||
|
|
||||||
|
const SITE_ID = '2e443a834727c065877c01d89921545e';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
_hmt: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerAnalytics() {
|
||||||
|
window._hmt = window._hmt || [];
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.innerHTML = `var _hmt = _hmt || [];
|
||||||
|
(function() {
|
||||||
|
var hm = document.createElement("script");
|
||||||
|
hm.src = "https://hm.baidu.com/hm.js?${SITE_ID}";
|
||||||
|
var s = document.getElementsByTagName("script")[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})()`;
|
||||||
|
document.querySelector('head')?.append(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initHmPlugin() {
|
||||||
|
if (inBrowser && import.meta.env.PROD) {
|
||||||
|
registerAnalytics();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
docs/.vitepress/theme/styles/base.css
Normal file
22
docs/.vitepress/theme/styles/base.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
html.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .VPContent {
|
||||||
|
/* background-color: #14161a; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-valid-error p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏选中项样式 */
|
||||||
|
.VPNavBarMenuLink,
|
||||||
|
.VPNavBarMenuGroup {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNavBarMenuLink.active,
|
||||||
|
.VPNavBarMenuGroup.active {
|
||||||
|
border-bottom-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
4
docs/.vitepress/theme/styles/index.ts
Normal file
4
docs/.vitepress/theme/styles/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import '@vben/styles';
|
||||||
|
|
||||||
|
import './variables.css';
|
||||||
|
import './base.css';
|
||||||
132
docs/.vitepress/theme/styles/variables.css
Normal file
132
docs/.vitepress/theme/styles/variables.css
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Customize default theme styling by overriding CSS variables:
|
||||||
|
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
*
|
||||||
|
* Each colors have exact same color scale system with 3 levels of solid
|
||||||
|
* colors with different brightness, and 1 soft color.
|
||||||
|
*
|
||||||
|
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||||
|
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||||
|
*
|
||||||
|
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||||
|
*
|
||||||
|
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||||
|
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||||
|
* top of it.
|
||||||
|
*
|
||||||
|
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||||
|
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||||
|
* on top of it.
|
||||||
|
*
|
||||||
|
* The soft color must be semi transparent alpha channel. This is crucial
|
||||||
|
* because it allows adding multiple "soft" colors on top of each other
|
||||||
|
* to create a accent, such as when having inline code block inside
|
||||||
|
* custom containers.
|
||||||
|
*
|
||||||
|
* - `default`: The color used purely for subtle indication without any
|
||||||
|
* special meanings attched to it such as bg color for menu hover state.
|
||||||
|
*
|
||||||
|
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||||
|
* brand theme, etc.
|
||||||
|
*
|
||||||
|
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||||
|
* brand color for this by default.
|
||||||
|
*
|
||||||
|
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||||
|
* container, badges, etc.
|
||||||
|
*
|
||||||
|
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||||
|
* in custom container, badges, etc.
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* --vp-c-indigo-1: #4f69fd; */
|
||||||
|
--vp-c-default-1: var(--vp-c-gray-1);
|
||||||
|
--vp-c-default-2: var(--vp-c-gray-2);
|
||||||
|
--vp-c-default-3: var(--vp-c-gray-3);
|
||||||
|
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||||
|
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||||
|
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||||
|
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||||
|
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||||
|
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||||
|
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||||
|
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||||
|
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||||
|
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||||
|
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||||
|
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||||
|
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||||
|
--vp-c-danger-1: var(--vp-c-red-1);
|
||||||
|
--vp-c-danger-2: var(--vp-c-red-2);
|
||||||
|
--vp-c-danger-3: var(--vp-c-red-3);
|
||||||
|
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Button
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
--vp-button-brand-border: transparent;
|
||||||
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||||
|
--vp-button-brand-hover-border: transparent;
|
||||||
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||||
|
--vp-button-brand-active-border: transparent;
|
||||||
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Home
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
var(--vp-c-indigo-1) 30%,
|
||||||
|
#18cefe
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
#18cefe 50%,
|
||||||
|
#c279ed 50%
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-filter: blur(44px);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Custom Block
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
--vp-custom-block-tip-border: transparent;
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||||
|
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* modal zIndex
|
||||||
|
*/
|
||||||
|
--popup-z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(68px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Algolia
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.DocSearch {
|
||||||
|
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||||
|
}
|
||||||
37
docs/package.json
Normal file
37
docs/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben/docs",
|
||||||
|
"version": "5.7.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "vitepress build",
|
||||||
|
"dev": "vitepress dev",
|
||||||
|
"docs:preview": "vitepress preview"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"#/*": {
|
||||||
|
"node": "./src/_env/node/*",
|
||||||
|
"default": "./src/_env/*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
|
"@vben/common-ui": "workspace:*",
|
||||||
|
"@vben/locales": "workspace:*",
|
||||||
|
"@vben/plugins": "workspace:*",
|
||||||
|
"@vben/styles": "workspace:*",
|
||||||
|
"ant-design-vue": "catalog:",
|
||||||
|
"lucide-vue-next": "catalog:",
|
||||||
|
"medium-zoom": "catalog:",
|
||||||
|
"reka-ui": "catalog:",
|
||||||
|
"vitepress-plugin-group-icons": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nolebase/vitepress-plugin-git-changelog": "catalog:",
|
||||||
|
"@tailwindcss/vite": "catalog:",
|
||||||
|
"@vben/tailwind-config": "workspace:*",
|
||||||
|
"@vben/vite-config": "workspace:*",
|
||||||
|
"@vite-pwa/vitepress": "catalog:",
|
||||||
|
"vitepress": "catalog:",
|
||||||
|
"vue": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user