初始化

This commit is contained in:
cli 2025-03-11 10:29:43 +08:00
commit 039eeb6670
990 changed files with 90217 additions and 0 deletions

4
.browserslistrc Normal file
View File

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

1
.commitlintrc.js Normal file
View File

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

7
.dockerignore Normal file
View File

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

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100
trim_trailing_whitespace = true
quote_type = single
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary

2
.gitconfig Normal file
View File

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

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
node_modules
.DS_Store
dist
dist-ssr
dist.zip
dist.tar
dist.war
.nitro
.output
*-dist.zip
*-dist.tar
*-dist.war
coverage
*.local
**/.vitepress/cache
.cache
.turbo
.temp
dev-dist
.stylelintcache
yarn.lock
package-lock.json
.VSCodeCounter
**/backend-mock/data
# local env files
.env.local
.env.*.local
.eslintcache
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
vite.config.mts.*
vite.config.mjs.*
vite.config.js.*
vite.config.ts.*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
/docs
/playground
/apps/backend-mock
/playground
/docs

6
.gitpod.yml Normal file
View File

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

20
.lintstagedrc.mjs Normal file
View File

@ -0,0 +1,20 @@
export default {
'*.md': ['prettier --cache --ignore-unknown --write'],
'*.vue': [
'prettier --write',
'eslint --cache --fix',
'stylelint --fix --allow-empty-input',
],
'*.{js,jsx,ts,tsx}': [
'prettier --cache --ignore-unknown --write',
'eslint --cache --fix',
],
'*.{scss,less,styl,html,vue,css}': [
'prettier --cache --ignore-unknown --write',
'stylelint --fix --allow-empty-input',
],
'package.json': ['prettier --cache --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
'prettier --cache --write--parser json',
],
};

1
.node-version Normal file
View File

@ -0,0 +1 @@
20.14.0

13
.npmrc Normal file
View File

@ -0,0 +1,13 @@
registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss
public-hoist-pattern[]=stylelint
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=@commitlint/*
public-hoist-pattern[]=czg
strict-peer-dependencies=false
auto-install-peers=true
dedupe-peer-dependents=true

18
.prettierignore Normal file
View File

@ -0,0 +1,18 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml

1
.prettierrc.mjs Normal file
View File

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

4
.stylelintignore Normal file
View File

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

30
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"recommendations": [
// Vue 3
"Vue.volar",
// ESLint JavaScript VS Code
"dbaeumer.vscode-eslint",
// Visual Studio Code Stylelint
"stylelint.vscode-stylelint",
// 使 Prettier
"esbenp.prettier-vscode",
// dotenv
"mikestead.dotenv",
//
"streetsidesoftware.code-spell-checker",
// Tailwind CSS VS Code
"bradlc.vscode-tailwindcss",
// iconify
"antfu.iconify",
// i18n
"Lokalise.i18n-ally",
// CSS
"vunguyentuan.vscode-css-variables",
// package.json PNPM catalog
"antfu.pnpm-catalog-lens"
],
"unwantedRecommendations": [
// volar
"octref.vetur"
]
}

37
.vscode/global.code-snippets vendored Normal file
View File

@ -0,0 +1,37 @@
{
"import": {
"scope": "javascript,typescript",
"prefix": "im",
"body": ["import { $2 } from '$1';"],
"description": "Import a module",
},
"export-all": {
"scope": "javascript,typescript",
"prefix": "ex",
"body": ["export * from '$1';"],
"description": "Export a module",
},
"vue-script-setup": {
"scope": "vue",
"prefix": "<sc",
"body": [
"<script setup lang=\"ts\">",
"const props = defineProps<{",
" modelValue?: boolean,",
"}>()",
"$1",
"</script>",
"",
"<template>",
" <div>",
" <slot/>",
" </div>",
"</template>",
],
},
"vue-computed": {
"scope": "javascript,typescript,vue",
"prefix": "com",
"body": ["computed(() => { $1 })"],
},
}

42
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"name": "vben admin playground dev",
"request": "launch",
"url": "http://localhost:5555",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/playground"
},
{
"type": "chrome",
"name": "vben admin antd dev",
"request": "launch",
"url": "http://localhost:5666",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-antd"
},
{
"type": "chrome",
"name": "vben admin ele dev",
"request": "launch",
"url": "http://localhost:5777",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-ele"
},
{
"type": "chrome",
"name": "vben admin naive dev",
"request": "launch",
"url": "http://localhost:5888",
"env": { "NODE_ENV": "development" },
"sourceMaps": true,
"webRoot": "${workspaceFolder}/apps/web-naive"
}
]
}

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

@ -0,0 +1,227 @@
{
"tailwindCSS.experimental.configFile": "internal/tailwind-config/src/index.ts",
// workbench
"workbench.list.smoothScrolling": true,
"workbench.startupEditor": "newUntitledFile",
"workbench.tree.indent": 10,
"workbench.editor.highlightModifiedTabs": true,
"workbench.editor.closeOnFileDelete": true,
"workbench.editor.limit.enabled": true,
"workbench.editor.limit.perEditorGroup": true,
"workbench.editor.limit.value": 5,
// editor
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.cursorBlinking": "expand",
"editor.largeFileOptimizations": false,
"editor.accessibilitySupport": "off",
"editor.cursorSmoothCaretAnimation": "on",
"editor.guides.bracketPairs": "active",
"editor.inlineSuggest.enabled": true,
"editor.suggestSelection": "recentlyUsedByPrefix",
"editor.acceptSuggestionOnEnter": "smart",
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.stickyScroll.enabled": true,
"editor.hover.sticky": true,
"editor.suggest.insertMode": "replace",
"editor.bracketPairColorization.enabled": true,
"editor.autoClosingBrackets": "beforeWhitespace",
"editor.autoClosingDelete": "always",
"editor.autoClosingOvertype": "always",
"editor.autoClosingQuotes": "beforeWhitespace",
"editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
},
// extensions
"extensions.ignoreRecommendations": true,
// terminal
"terminal.integrated.cursorBlinking": true,
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.tabs.enabled": true,
"terminal.integrated.scrollback": 10000,
"terminal.integrated.stickyScroll.enabled": true,
// files
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.simpleDialog.enable": true,
"files.associations": {
"*.ejs": "html",
"*.art": "html",
"**/tsconfig.json": "jsonc",
"*.json": "jsonc",
"package.json": "json"
},
"files.exclude": {
"**/.eslintcache": true,
"**/bower_components": true,
"**/.turbo": true,
"**/.idea": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.stylelintcache": true,
"**/.DS_Store": true,
"**/vite.config.mts.*": true,
"**/tea.yaml": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.vscode/**": true,
"**/node_modules/**": true,
"**/tmp/**": true,
"**/bower_components/**": true,
"**/dist/**": true,
"**/yarn.lock": true
},
// search
"search.searchEditor.singleClickBehaviour": "peekDefinition",
"search.followSymlinks": false,
// 使/
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
"**/*.log*": true,
"**/bower_components": true,
"**/dist": true,
"**/elehukouben": true,
"**/.git": true,
"**/.github": true,
"**/.gitignore": true,
"**/.svn": true,
"**/.DS_Store": true,
"**/.vitepress/cache": true,
"**/.idea": true,
"**/.vscode": false,
"**/.yarn": true,
"**/tmp": true,
"*.xml": true,
"out": true,
"dist": true,
"node_modules": true,
"CHANGELOG.md": true,
"**/pnpm-lock.yaml": true,
"**/yarn.lock": true
},
"debug.onTaskErrors": "debugAnyway",
"diffEditor.ignoreTrimWhitespace": false,
"npm.packageManager": "pnpm",
"css.validate": false,
"less.validate": false,
"scss.validate": false,
// extension
"emmet.showSuggestionsAsSnippets": true,
"emmet.triggerExpansionOnTab": false,
"errorLens.enabledDiagnosticLevels": ["warning", "error"],
"errorLens.excludeBySource": ["cSpell", "Grammarly", "eslint"],
"stylelint.enable": true,
"stylelint.packageManager": "pnpm",
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"stylelint.customSyntax": "postcss-html",
"stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
"typescript.inlayHints.enumMemberValues.enabled": true,
"typescript.preferences.preferTypeOnlyAutoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "on",
"eslint.validate": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"json5"
],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"github.copilot.enable": {
"*": true,
"markdown": true,
"plaintext": false,
"yaml": false
},
"cssVariables.lookupFiles": ["packages/core/base/design/src/**/*.css"],
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
"playground/src/locales/langs",
"apps/*/src/locales/langs"
],
"i18n-ally.pathMatcher": "{locale}/{namespace}.{ext}",
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
//
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
"tailwind.config.mjs": "postcss.*"
},
"commentTranslate.hover.enabled": false,
"commentTranslate.multiLineMerge": true,
"vue.server.hybridMode": true,
"typescript.tsdk": "node_modules/typescript/lib",
"oxc.enable": false
}

44
Dockerfile Normal file
View File

@ -0,0 +1,44 @@
FROM node:22-alpine AS builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_OPTIONS=--max-old-space-size=8192
ENV TZ=Asia/Shanghai
RUN corepack enable
WORKDIR /vben5
# copy package.json and pnpm-lock.yaml to workspace
COPY . /vben5
RUN pnpm install --frozen-lockfile
# 如果你的项目中使用了 antd需要执行下面的命令
RUN pnpm build:antd
# 如果你的项目中使用了 element-ui需要执行下面的命令
#RUN pnpm build:ele
# 如果你的项目中使用了 naive需要执行下面的命令
#RUN pnpm build:naive
RUN echo "Builder Success 🎉"
FROM nginx:stable-alpine AS production
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf
# 如果你的项目中使用了 antd需要执行下面的命令
COPY --from=builder /vben5/apps/web-antd/dist /usr/share/nginx/html
# 如果你的项目中使用了 element-ui需要执行下面的命令
#COPY --from=builder /vben5/apps/web-ele/dist /usr/share/nginx/html
# 如果你的项目中使用了 naive需要执行下面的命令
#COPY --from=builder /vben5/apps/web-naive/dist /usr/share/nginx/html
COPY --from=builder /vben5/_nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
# start nginx
CMD ["nginx", "-g", "daemon off;"]

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2024, WangJunZzz
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

109
README.md Normal file
View File

@ -0,0 +1,109 @@
<p align="center">
<a href="https://github.com/WangJunZzz/abp-vnext-pro">
<img src="https://blog-resouce.oss-cn-shenzhen.aliyuncs.com/images/abp/06.jpg">
</a>
</p>
<h1 align="center">Abp Vnext Pro Vben5 ...</h1>
## 🔗 链接
- [AbpPro Vben2预览](http://182.43.18.151:44318/)
- [AbpPro Vben5预览](http://182.43.18.151:44320/)
- [代码生成器预览](http://182.43.18.151:44311/)
- [文档地址](http://doc.cncore.club/)
- [国内文档地址](http://doc.china.cncore.club:81/)
- [视频教程](https://www.bilibili.com/video/BV1pt4y1E7aZ)
- [代码生成器仓库地址](https://github.com/WangJunZzz/abp-vnext-pro-suite)
## 📦 快速开始
- 安装Cli
```bash
dotnet tool install Lion.AbpPro.Cli -g
```
- 更新Cli
```bash
dotnet tool update Lion.AbpPro.Cli -g
```
### 三个项目模板
- 源码版本
```bash
lion.abp new -t pro -c 公司名称 -p 项目名称 -v 版本(默认LastRelease) -o 默认当前控制台执行目录
```
- nuget版本
```bash
lion.abp new -t pro.all -c 公司名称 -p 项目名称 -v 版本(默认LastRelease) -o 默认当前控制台执行目录
```
- 模块
```bash
lion.abp new -t pro.module -c 公司名称 -p 项目名称 -v 版本(默认LastRelease) -o 默认当前控制台执行目录
```
## ✨ 系统功能
- [x] 用户管理
- [x] 角色管理
- [x] 审计日志
- [x] 后台任务
- [x] 集成事件
- [x] SinglaR 消息通知(站内信)
- [x] 多语言
- [x] 数据字典
- [x] 容器化部署
- [x] 单元测试
- [x] ES 日志
- [x] Setting 管理
- [x] 多租户
- [x] 文件管理
## 🤝 如何贡献
非常欢迎你的加入!提一个 Issue 或者提交一个 Pull Request。
**Pull Request:**
1. Fork 代码!
2. 创建自己的分支: `git checkout -b feat/xxxx`
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支: `git push origin feat/xxxx`
5. 提交`pull request`
## Git 贡献提交规范
- 参考
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
## ✒️交流
- QQ 1群<s>686933575(已满)</s>
- QQ 2群862717726
## 💖赞助
- Star就是对该项目的最大肯定!
- 如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
![](https://blog-resouce.oss-cn-shenzhen.aliyuncs.com/images/donate.png)

75
_nginx/nginx.conf Normal file
View File

@ -0,0 +1,75 @@
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
types {
application/javascript js mjs;
text/css css;
text/html html;
}
sendfile on;
# tcp_nopush on;
#keepalive_timeout 0;
# keepalive_timeout 65;
# gzip on;
# gzip_buffers 32 16k;
# gzip_comp_level 6;
# gzip_min_length 1k;
# gzip_static on;
# gzip_types text/plain
# text/css
# application/javascript
# application/json
# application/x-javascript
# text/xml
# application/xml
# application/xml+rss
# text/javascript; #设置压缩的文件类型
# gzip_vary on;
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html;
# Enable CORS
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

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

@ -0,0 +1,5 @@
# 应用标题
VITE_APP_TITLE=采集端综合管理
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=abp-vnext-pro-vben5-antd

View File

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

View File

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

View File

@ -0,0 +1,28 @@
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
# 是否开启压缩,可以设置为 none, brotli, gzip
VITE_COMPRESS=none
# 是否开启 PWA
VITE_PWA=false
# vue-router 的模式
VITE_ROUTER_HISTORY=history
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
# 是否打开刷新浏览器检查用户角色信息
VITE_REFRESH_ROLE = true
# 后端接口地址
VITE_APP_API_ADDRESS=http://118.190.144.92:9110/
# websocket地址
VITE_WEBSOCKET_URL=http://118.190.144.92:9110/signalr/notification

35
apps/web-antd/index.html Normal file
View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="description" content="A Modern Back-end Management System" />
<meta name="keywords" content="Vben Admin Vue3 Vite" />
<meta name="author" content="Vben" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
<title><%= VITE_APP_TITLE %></title>
<link rel="icon" href="/favicon.ico" />
<script>
// 生产环境下注入百度统计
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
var _hmt = _hmt || [];
(function () {
var hm = document.createElement('script');
hm.src =
'https://hm.baidu.com/hm.js?b38e689f40558f20a9a686d7f6f33edf';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,61 @@
{
"name": "@vben/web-antd",
"version": "5.5.3",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-antd"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"nswag": "openapi-ts -f ./src/api-client-config/config.ts",
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@microsoft/signalr": "^8.0.7",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"axios": "^1.7.7",
"clipboard": "^2.0.11",
"codemirror-editor-vue3": "^2.8.0",
"dayjs": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-request": "^2.0.4",
"vue-router": "catalog:",
"vue3-json-viewer": "^2.2.2"
},
"devDependencies": {
"@hey-api/client-axios": "^0.2.10",
"@hey-api/openapi-ts": "^0.55.3"
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,169 @@
/**
* 使 adapter/form 使便使
* vben-formvben-modalvben-drawer 使,
*/
import type { Component, SetupContext } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import { h } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
slots,
);
},
ApiTreeSelect: (props, { attrs, slots }) => {
return h(
ApiComponent,
{
placeholder: $t('ui.placeholder.select'),
...props,
...attrs,
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
slots,
);
},
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: (props, { attrs, slots }) => {
return h(
IconPicker,
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
slots,
);
},
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };

View File

@ -0,0 +1,47 @@
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };

View File

@ -0,0 +1,67 @@
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: true,
columnConfig: {
resizable: true,
},
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'totalCount',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small', // medium / small / mini
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';

View File

@ -0,0 +1,7 @@
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
client: '@hey-api/client-axios',
input: 'http://localhost:44315/swagger/IOT/swagger.json',
output: 'src/api-client',
});

View File

@ -0,0 +1,102 @@
import { useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue';
import axios from 'axios';
import { $t } from '#/locales';
import { antdLocale } from '#/locales/index';
import { useAuthStore } from '#/store';
const api = axios.create({
baseURL: import.meta.env.DEV
? '/proxy/'
: import.meta.env.VITE_APP_API_ADDRESS,
timeout: 1000 * 60,
responseType: 'blob', // 设置响应数据类型为blob
});
api.interceptors.request.use((request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore();
const authStore = useAuthStore();
const token = userStore.userInfo?.token;
// 设置请求头
if (request.headers) {
request.headers.__tenant = userStore.tenant?.tenantId;
request.headers['accept-language'] = antdLocale.value.locale;
}
// 如果token过期则跳转到登录页面
if (token && userStore.checkUserLoginExpire()) {
authStore.logout();
return Promise.reject($t('common.mesage401'));
}
// 设置请求头
if (request.headers) {
request.headers.Authorization = `Bearer ${token}`;
}
return request;
});
api.interceptors.response.use(
(response) => {
/**
*
* { status: 1, error: '', data: '' }
* status 1 0
* error
*/
if (response.data.status === 1) {
if (response.data.error !== '') {
// 错误提示
Message.error(response.data.error);
return Promise.reject(response.data);
}
} else {
// useUserStore().logout()
}
return Promise.resolve(response);
},
(error) => {
let message = error.message;
if (message === 'Network Error') {
message = $t('common.mesage500');
} else if (message.includes('timeout')) {
message = $t('common.timeOut');
} else
switch (error.status) {
case 400: {
message = error.response.data.error?.validationErrors[0].message;
break;
}
case 401: {
message = $t('common.mesage401');
// useUserStore().logout()
break;
}
case 403: {
message = $t('common.mesage403');
break;
}
case 500: {
message = error.response.data.error?.message;
break;
}
default: {
if (message.includes('Request failed with status code')) {
message = $t('common.mesage500');
}
}
}
Message.error(message);
return Promise.reject(error);
},
);
export default api;

View File

@ -0,0 +1,114 @@
import { useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue';
import { $t } from '#/locales';
import { antdLocale } from '#/locales/index';
import { useAuthStore } from '#/store';
import { client } from '../api-client/services.gen';
client.setConfig({
baseURL: import.meta.env.DEV
? '/proxy/'
: import.meta.env.VITE_APP_API_ADDRESS,
timeout: 1000 * 60,
responseType: 'json',
throwOnError: true,
});
client.instance.interceptors.request.use((request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore();
const token = userStore.userInfo?.token;
// 设置请求头
if (request.headers) {
request.headers.__tenant = userStore.tenant?.tenantId;
// todo vben5 没有提供统一获取当前语言的方式
request.headers['accept-language'] = antdLocale.value.locale;
}
// 如果token过期则跳转到登录页面
if (
request.url !== undefined &&
request.url.includes('/api/app/account/login')
) {
return request;
}
if (token && userStore.checkUserLoginExpire()) {
const authStore = useAuthStore();
authStore.logout();
Message.warn($t('common.mesage401'));
return Promise.reject($t('common.mesage401'));
}
// 设置请求头
if (request.headers) {
request.headers.Authorization = `Bearer ${token}`;
}
return request;
});
client.instance.interceptors.response.use(
(response) => {
/**
*
* { status: 1, error: '', data: '' }
* status 1 0
* error
*/
if (response.data.status === 1) {
if (response.data.error !== '') {
// 错误提示
Message.error(response.data.error);
return Promise.reject(response.data);
}
} else {
// useUserStore().logout()
}
return Promise.resolve(response);
},
(error) => {
let message = error.message;
if (message === 'Network Error') {
message = $t('common.mesage500');
} else if (message.includes('timeout')) {
message = $t('common.timeOut');
} else
switch (error.status) {
case 400: {
message = error.response.data.error?.validationErrors[0].message;
break;
}
case 401: {
message = $t('common.mesage401');
// useUserStore().logout()
break;
}
case 403: {
message = $t('common.mesage403');
break;
}
case 500: {
message = error.response.data.error?.message;
break;
}
default: {
if (message.includes('Request failed with status code')) {
message = $t('common.mesage500');
}
}
}
Message.error(message);
return Promise.reject(error);
},
);
export default client;

View File

@ -0,0 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './schemas.gen';
export * from './services.gen';
export * from './types.gen';

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
*
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
*
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}

View File

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

View File

@ -0,0 +1,10 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@ -0,0 +1,10 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
*
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}

View File

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

View File

@ -0,0 +1,113 @@
/**
*
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
*
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 0,
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });

39
apps/web-antd/src/app.vue Normal file
View File

@ -0,0 +1,39 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAntdDesignTokens } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
const { tokens } = useAntdDesignTokens();
const tokenTheme = computed(() => {
const algorithm = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd
if (preferences.app.compact) {
algorithm.push(theme.compactAlgorithm);
}
return {
algorithm,
token: tokens,
};
});
</script>
<template>
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
<App>
<RouterView />
</App>
</ConfigProvider>
</template>

View File

@ -0,0 +1,68 @@
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initTippy } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
// https://github.com/rennzhang/codemirror-editor-vue3
import { InstallCodeMirror } from 'codemirror-editor-vue3';
import JsonViewer from 'vue3-json-viewer';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import App from './app.vue';
import { router } from './router';
import 'vue3-json-viewer/dist/index.css';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App);
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置 json-viewer
app.use(JsonViewer);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.use(InstallCodeMirror);
app.mount('#app');
}
export { bootstrap };

View File

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

View File

@ -0,0 +1,78 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import { Spin } from 'ant-design-vue';
import { SizeEnum } from './typing';
defineOptions({ name: 'Loading' });
defineProps({
tip: {
type: String as PropType<string>,
default: '',
},
size: {
type: String as PropType<SizeEnum>,
default: SizeEnum.LARGE,
validator: (v: SizeEnum): boolean => {
return [SizeEnum.DEFAULT, SizeEnum.LARGE, SizeEnum.SMALL].includes(v);
},
},
absolute: {
type: Boolean as PropType<boolean>,
default: false,
},
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
background: {
type: String as PropType<string>,
},
theme: {
type: String as PropType<'dark' | 'light'>,
},
});
</script>
<template>
<section
v-show="loading"
:class="{ absolute, [`${theme}`]: !!theme }"
:style="[background ? `background-color: ${background}` : '']"
class="full-loading"
>
<Spin v-bind="$attrs" :size="size" :spinning="loading" :tip="tip" />
</section>
</template>
<style lang="less" scoped>
.full-loading {
display: flex;
position: fixed;
z-index: 200;
top: 0;
left: 0;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: #f0f2f566;
&.absolute {
position: absolute;
z-index: 300;
top: 0;
left: 0;
}
}
html[data-theme='dark'] {
.full-loading:not(.light) {
background-color: rgba(0, 0, 0, 0.5);
}
}
.full-loading.dark {
background-color: rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -0,0 +1,84 @@
import type { LoadingProps } from './typing';
import { createVNode, defineComponent, h, reactive, render } from 'vue';
import Loading from './Loading.vue';
// 创建一个加载组件的函数
export function createLoading(
props?: Partial<LoadingProps>,
target?: HTMLElement,
wait = false,
) {
let vm: any = null;
const data = reactive({
tip: '',
loading: true,
...props,
});
// 定义加载组件的包装
const LoadingWrap = defineComponent({
render() {
return h(Loading, { ...data });
},
});
vm = createVNode(LoadingWrap);
let container: any = null;
// 根据wait参数决定何时渲染Loading组件
if (wait) {
setTimeout(() => {
render(vm, (container = document.createElement('div')));
}, 0);
} else {
render(vm, (container = document.createElement('div')));
}
// 关闭加载组件的函数
function close() {
if (vm?.el && vm.el.parentNode) {
vm.el.remove();
}
}
// 打开加载组件并将其附加到目标元素上的函数
function open(target: HTMLElement = document.body) {
if (!vm || !vm.el) {
return;
}
target.append(vm.el as HTMLElement);
}
// 销毁加载组件的函数
function destroy() {
container && render(null, container);
container = vm = null;
}
// 如果提供了目标元素,则打开加载组件
if (target) {
open(target);
}
// 返回加载组件的实例及控制方法
return {
vm,
close,
open,
destroy,
setTip: (tip: string) => {
data.tip = tip;
},
setLoading: (loading: boolean) => {
data.loading = loading;
},
get loading() {
return data.loading;
},
get $el() {
return vm?.el as HTMLElement;
},
};
}

View File

@ -0,0 +1,14 @@
export enum SizeEnum {
DEFAULT = 'default',
LARGE = 'large',
SMALL = 'small',
}
export interface LoadingProps {
tip: string;
size: SizeEnum;
absolute: boolean;
loading: boolean;
background: string;
theme: 'dark' | 'light';
}

View File

@ -0,0 +1,61 @@
import type { LoadingProps } from './typing';
import type { Ref } from 'vue';
import { unref } from 'vue';
import { tryOnUnmounted } from '@vueuse/core';
import { createLoading } from './createLoading';
export interface UseLoadingOptions {
target?: any;
props?: Partial<LoadingProps>;
}
interface Fn {
(): void;
}
export function useLoading(
props: Partial<LoadingProps>,
): [Fn, Fn, (arg0: string) => void];
export function useLoading(
opt: Partial<UseLoadingOptions>,
): [Fn, Fn, (arg0: string) => void];
export function useLoading(
opt: Partial<LoadingProps> | Partial<UseLoadingOptions>,
): [Fn, Fn, (arg0: string) => void] {
let props: Partial<LoadingProps>;
let target: HTMLElement | Ref<any> = document.body;
if (Reflect.has(opt, 'target') || Reflect.has(opt, 'props')) {
const options = opt as Partial<UseLoadingOptions>;
props = options.props || {};
target = options.target || document.body;
} else {
props = opt as Partial<LoadingProps>;
}
const instance = createLoading(props, undefined, false);
const open = (): void => {
const t = unref(target as Ref<any>);
if (!t) return;
instance.open(t);
};
const close = (): void => {
instance.close();
};
const setTip = (tip: string) => {
instance.setTip(tip);
};
tryOnUnmounted(() => {
instance.destroy();
});
return [open, close, setTip];
}

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed, h } from 'vue';
import { IconifyIcon as VbenIcon } from '@vben/icons';
const props = defineProps({
icon: {
type: String,
default: '',
},
size: {
type: [String, Number],
default: '16px',
},
});
const iconComp = computed(() => {
if (props.icon.startsWith('http')) {
return () => h('img', { src: props.icon, class: 'm-icon__' });
}
return '';
});
const styles = computed(() => {
return {
fontSize: props.size.toString().endsWith('px')
? props.size
: `${props.size}px`,
};
});
</script>
<template>
<component :is="iconComp" v-if="iconComp" :style="styles" />
<VbenIcon v-else :icon="props.icon" :style="styles" class="m-icon__" />
</template>
<style lang="less" scoped>
.m-icon__ {
display: inline-flex;
align-items: center;
width: 1em;
height: 1em;
font-style: normal;
line-height: 0;
color: inherit;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,225 @@
<script setup lang="ts">
import type { ButtonType } from 'ant-design-vue/es/button';
import type { ActionItem, PopConfirm } from './types';
import { computed, type PropType, toRaw } from 'vue';
import { useAccess } from '@vben/access';
import { isBoolean, isFunction } from '@vben/utils';
import { Button, Dropdown, Menu, Popconfirm, Space } from 'ant-design-vue';
import { Icon } from '#/components/icon';
const props = defineProps({
actions: {
type: Array as PropType<ActionItem[]>,
default() {
return [];
},
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default() {
return [];
},
},
divider: {
type: Boolean,
default: true,
},
});
const MenuItem = Menu.Item;
const { hasAccessByCodes } = useAccess();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action) => {
const { popConfirm } = action;
return {
// getPopupContainer: document.body,
type: 'link' as ButtonType,
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
const getDropdownList = computed((): any[] => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
})
.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider:
index < props.dropDownActions.length - 1 ? props.divider : false,
};
});
});
const getPopConfirmProps = (attrs: PopConfirm) => {
const originAttrs: any = attrs;
delete originAttrs.icon;
if (attrs.confirm && isFunction(attrs.confirm)) {
originAttrs.onConfirm = attrs.confirm;
delete originAttrs.confirm;
}
if (attrs.cancel && isFunction(attrs.cancel)) {
originAttrs.onCancel = attrs.cancel;
delete originAttrs.cancel;
}
return originAttrs;
};
const getButtonProps = (action: ActionItem) => {
const res = {
type: action.type || 'primary',
...action,
};
delete res.icon;
return res;
};
const handleMenuClick = (e: any) => {
const action = getDropdownList.value[e.key];
if (action.onClick && isFunction(action.onClick)) {
action.onClick();
}
};
</script>
<template>
<div class="m-table-action">
<Space
:size="
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
"
>
<template v-for="(action, index) in getActions" :key="index">
<Popconfirm
v-if="action.popConfirm"
v-bind="getPopConfirmProps(action.popConfirm)"
>
<template v-if="action.popConfirm.icon" #icon>
<Icon :icon="action.popConfirm.icon" />
</template>
<Button v-bind="getButtonProps(action)">
<template v-if="action.icon" #icon>
<Icon :icon="action.icon" />
</template>
{{ action.label }}
</Button>
</Popconfirm>
<Button v-else v-bind="getButtonProps(action)" @click="action.onClick">
<template v-if="action.icon" #icon>
<Icon :icon="action.icon" />
</template>
{{ action.label }}
</Button>
</template>
</Space>
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
<slot name="more">
<Button size="small" type="link">
<template #icon>
<Icon class="icon-more" icon="ant-design:more-outlined" />
</template>
</Button>
</slot>
<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem v-for="(action, index) in getDropdownList" :key="index">
<template v-if="action.popConfirm">
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
<template v-if="action.popConfirm.icon" #icon>
<Icon :icon="action.popConfirm.icon" />
</template>
<div
:class="
action.disabled === true
? 'cursor-not-allowed text-gray-300'
: ''
"
>
<Icon v-if="action.icon" :icon="action.icon" />
<span class="ml-1">{{ action.text }}</span>
</div>
</Popconfirm>
</template>
<template v-else>
<div
:class="
action.disabled === true
? 'cursor-not-allowed text-gray-300'
: ''
"
>
<Icon v-if="action.icon" :icon="action.icon" />
{{ action.label }}
</div>
</template>
</MenuItem>
</Menu>
</template>
</Dropdown>
</div>
</template>
<style lang="less">
/** 修复 iconify 位置问题 **/
.m-table-action {
.ant-btn > .iconify + span,
.ant-btn > span + .iconify {
margin-inline-start: 8px;
}
.ant-btn > .iconify {
display: inline-flex;
align-items: center;
width: 1em;
height: 1em;
font-style: normal;
line-height: 0;
color: inherit;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
</style>

View File

@ -0,0 +1,26 @@
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
export interface PopConfirm {
title: string;
okText?: string;
cancelText?: string;
confirm: Fn;
cancel?: Fn;
icon?: string;
disabled?: boolean;
}
export interface ActionItem extends ButtonProps {
onClick?: Fn;
label?: string;
color?: 'error' | 'success' | 'warning';
icon?: string;
popConfirm?: PopConfirm;
disabled?: boolean;
divider?: boolean;
// 权限编码控制是否显示
auth?: string[];
// 业务控制是否显示
ifShow?: ((action: ActionItem) => boolean) | boolean;
tooltip?: string | TooltipProps;
}

View File

@ -0,0 +1,125 @@
import { useEventbus } from '@vben/hooks';
import { useUserStore } from '@vben/stores';
import * as signalR from '@microsoft/signalr';
import { notification } from 'ant-design-vue';
const eventbus = useEventbus();
let connection: signalR.HubConnection;
export function useSignalR() {
/**
* SignalR
*/
async function startConnect() {
try {
connectionsignalR();
await connection.start();
} catch (error) {
console.error(error);
setTimeout(() => startConnect(), 5000);
}
}
/**
* SignalR连接
*/
function closeConnect(): void {
8;
if (connection) {
connection.stop();
}
}
async function connectionsignalR() {
const userStore = useUserStore();
const token = userStore.userInfo?.token;
connection = new signalR.HubConnectionBuilder()
.withUrl(import.meta.env.VITE_WEBSOCKET_URL, {
accessTokenFactory: () => token,
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
// 重连规则:重连次数<300间隔1s;重试次数<3000:间隔3s;重试次数>3000:间隔30s
const count = retryContext.previousRetryCount / 300;
if (count < 1) {
// 重试次数<300,间隔1s
return 1000;
} else if (count < 10) {
// 重试次数>300:间隔5s
return 1000 * 5;
} // 重试次数>3000:间隔30s
else {
return 1000 * 30;
}
},
})
.configureLogging(signalR.LogLevel.Debug)
.build();
// 接收普通文本消息
connection.on('ReceiveTextMessageAsync', ReceiveTextMessageHandlerAsync);
// 接收广播消息
connection.on(
'ReceiveBroadCastMessageAsync',
ReceiveBroadCastMessageHandlerAsync,
);
}
/**
*
* @param message
*/
function ReceiveTextMessageHandlerAsync(message: any) {
// 发布事件
eventbus.publish('ReceiveTextMessageHandlerAsync', message);
if (message.messageLevel === 10) {
notification.warn({
description: message.content,
message: message.title,
});
}
if (message.messageLevel === 20) {
notification.info({
message: message.title,
description: message.content,
});
}
if (message.messageLevel === 30) {
notification.error({
message: message.title,
description: message.content,
});
}
}
/**
* 广
* @param message
*/
function ReceiveBroadCastMessageHandlerAsync(message: any) {
// 发布事件
eventbus.publish('ReceiveTextMessageHandlerAsync', message);
if (message.messageLevel === 10) {
notification.warn({
message: message.title,
description: message.content,
});
}
if (message.messageLevel === 20) {
notification.info({
message: message.title,
description: message.content,
});
}
if (message.messageLevel === 30) {
notification.error({
message: message.title,
description: message.content,
});
}
}
return { startConnect, closeConnect };
}

View File

@ -0,0 +1,207 @@
<script setup lang="ts">
import type { VbenFormProps } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { Button, message as Message, Modal, Space, Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
postNotificationNotificationPage,
postNotificationRead,
} from '#/api-client';
import { $t } from '#/locales';
defineOptions({
name: 'AbpNotifyItem',
});
const formOptions: VbenFormProps = {
schema: [
{
component: 'Input',
fieldName: 'messageType',
label: 'messageType',
defaultValue: 20,
dependencies: {
show: () => false,
triggerFields: ['messageType'],
},
},
{
component: 'Input',
fieldName: 'title',
label: $t('abp.message.title'),
},
{
component: 'Input',
fieldName: 'content',
label: $t('abp.message.content'),
},
{
component: 'Select',
fieldName: 'messageLevel',
label: $t('abp.message.level'),
componentProps: {
options: [
{
label: $t('common.warning'),
value: 10,
},
{
label: $t('common.info'),
value: 20,
},
{
label: $t('common.error'),
value: 30,
},
],
},
},
{
component: 'Select',
fieldName: 'read',
label: $t('abp.message.isRead'),
componentProps: {
options: [
{
label: $t('common.yes'),
value: true,
},
{
label: $t('common.no'),
value: false,
},
],
},
},
],
wrapperClass: 'grid-cols-5',
};
const userStore = useUserStore();
const gridOptions: VxeGridProps<any> = {
checkboxConfig: {},
columns: [
{ title: $t('common.seq'), type: 'seq', width: 50 },
{ field: 'title', title: $t('abp.message.title'), minWidth: '150' },
{ field: 'content', title: $t('abp.message.content'), minWidth: '150' },
// { field: 'messageTypeName', title: '', minWidth: '150' },
{
field: 'messageLevelName',
title: $t('abp.message.level'),
minWidth: '150',
slots: { default: 'messageLevel' },
},
{
field: 'senderUserName',
title: $t('abp.message.sender'),
minWidth: '150',
},
{
field: 'receiveUserName',
title: $t('abp.message.receiver'),
minWidth: '150',
},
{
field: 'read',
title: $t('abp.message.isRead'),
minWidth: '150',
slots: { default: 'read' },
},
{
field: 'creationTime',
title: $t('common.createTime'),
minWidth: '150',
formatter: ({ cellValue }) => {
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
},
},
{
title: $t('common.action'),
field: 'action',
fixed: 'right',
minWidth: '150',
slots: { default: 'action' },
},
],
height: 'auto',
keepSource: true,
pagerConfig: {},
toolbarConfig: {
custom: true,
},
customConfig: {
storage: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const { data } = await postNotificationNotificationPage({
body: {
pageIndex: page.currentPage,
pageSize: page.pageSize,
receiverUserId: userStore.userInfo?.id,
...formValues,
},
});
return data;
},
},
},
};
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
const onRead = (row: any) => {
// if (row.read) {
// Message.info(',');
// return;
// }
Modal.confirm({
title: $t('abp.message.confirmRead'),
onOk: async () => {
await postNotificationRead({ body: { id: row.id } });
gridApi.reload();
Message.success($t('common.success'));
},
});
};
</script>
<template>
<Page auto-content-height>
<Grid>
<template #messageLevel="{ row }">
<Tag v-if="row.messageLevel === 10" color="yellow">
{{ row.messageLevelName }}
</Tag>
<Tag v-if="row.messageLevel === 20" color="green">
{{ row.messageLevelName }}
</Tag>
<Tag v-if="row.messageLevel === 30" color="red">
{{ row.messageLevelName }}
</Tag>
</template>
<template #read="{ row }">
<Tag v-if="row.read" color="green">{{ $t('abp.message.read') }} </Tag>
<Tag v-else color="red"> {{ $t('abp.message.unread') }} </Tag>
</template>
<template #action="{ row }">
<Space>
<Button size="small" type="primary" @click="onRead(row)">
{{ $t('abp.message.setRead') }}
</Button>
</Space>
</template>
</Grid>
</Page>
</template>
<style scoped></style>

View File

@ -0,0 +1,23 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source);
</script>
<template>
<AuthPageLayout
:app-name="appName"
:logo="logo"
:page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')"
>
<!-- 自定义工具栏 -->
<!-- <template #toolbar></template> -->
</AuthPageLayout>
</template>

View File

@ -0,0 +1,256 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { useEventbus, useRefresh, useWatermark } from '@vben/hooks';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { message as Message } from 'ant-design-vue/es/components';
import dayjs from 'dayjs';
import {
postNotificationBatchRead,
postNotificationNotificationPage,
postNotificationRead,
} from '#/api-client';
import { useSignalR } from '#/hooks/useSignalR';
import { $t } from '#/locales';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
import MyProfile from './my-profile.vue';
import NotifyItem from './NotifyItem.vue';
const notifications = ref<NotificationItem[]>([]);
const { startConnect, closeConnect } = useSignalR();
function convertToNotificationItem(message: any): NotificationItem {
return {
avatar: '',
date: dayjs(message.creationTime).format('YYYY-MM-DD HH:mm:ss'),
isRead: message.read,
message: message.content,
title: message.title,
id: message.id,
};
}
const eventbus = useEventbus();
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const router = useRouter();
const { refresh } = useRefresh();
onMounted(async () => {
//
if (
userStore?.userInfo?.id ||
userStore?.userInfo?.token ||
userStore.checkUserLoginExpire()
) {
//
await router.replace({
path: LOGIN_PATH,
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
},
});
}
// SignalR
await startConnect();
let refreshCount = userStore?.applicationInfo?.refreshCount ?? -1;
if (refreshCount === -1) {
userStore.setApplicationInfo({ refreshCount: refreshCount + 1 });
} else {
refreshCount = refreshCount + 1;
//
userStore.setApplicationInfo({ refreshCount });
}
//
if (import.meta.env.VITE_REFRESH_ROLE && refreshCount > 0) {
// todo 2
// await authStore.getApplicationConfiguration();
await refresh();
}
eventbus.subscribe('ReceiveTextMessageHandlerAsync', (content) => {
const item: NotificationItem = {
avatar: '',
date: '',
message: content.contnet,
title: content.title,
id: content.id,
isRead: false,
};
notifications.value.push(item);
});
await loadMessage();
});
onBeforeUnmount(async () => {
await closeConnect();
});
const { destroyWatermark, updateWatermark } = useWatermark();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const [MyProfileModal, myProfileModalApi] = useVbenModal({
draggable: true,
onConfirm: () => {},
onBeforeClose: () => {},
});
const [NotifyItemModal, notifyItemModalApi] = useVbenModal({
draggable: true,
onConfirm: () => {},
onBeforeClose: () => {},
});
const menus = computed(() => [
{
handler: () => {
myProfileModalApi.open();
},
icon: 'ph:user',
text: $t('abp.user.myAccount'),
},
]);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
async function handleLogout() {
await authStore.logout(false);
}
function handleNoticeClear() {
notifications.value = [];
}
async function handleMakeAll() {
// notifications api
const readIds = notifications.value
.filter((item) => item.isRead)
.map((item) => item.id);
if (readIds.length > 0) {
await postNotificationBatchRead({ body: { ids: readIds } });
Message.success($t('common.editSuccess'));
}
notifications.value.forEach((item) => (item.isRead = true));
}
async function handleRead(row: NotificationItem) {
if (row.isRead) {
return;
}
await postNotificationRead({ body: { id: row.id } });
notifications.value.forEach((item) => {
if (item.id === row.id) {
item.isRead = true;
}
});
}
function handleViewAll() {
notifyItemModalApi.open();
}
watch(
() => preferences.app.watermark,
async (enable) => {
if (enable) {
await updateWatermark({
content: `${userStore.userInfo?.userName}`,
});
} else {
destroyWatermark();
}
},
{
immediate: true,
},
);
async function loadMessage() {
notifications.value = [];
const message = await postNotificationNotificationPage({
body: {
pageIndex: 1,
pageSize: 4,
messageType: 20,
receiverUserId: userStore.userInfo?.id,
},
});
message.data?.items?.forEach((item) => {
notifications.value.push(convertToNotificationItem(item));
// isRead
notifications.value.sort((a, b) => {
if (a.isRead === b.isRead) {
return 0;
}
return a.isRead ? 1 : -1;
});
});
}
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown>
<UserDropdown
:avatar
:menus
:tag-text="userStore.tenant?.name"
:text="userStore.userInfo?.name"
@logout="handleLogout"
/>
</template>
<template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@icon-click="loadMessage"
@make-all="handleMakeAll"
@read="handleRead"
@view-all="handleViewAll"
/>
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
<MyProfileModal
:show-cancel-button="false"
:show-confirm-button="false"
:title="$t('abp.user.myAccount')"
class="h-[410px] w-[800px]"
>
<MyProfile />
</MyProfileModal>
<NotifyItemModal
:fullscreen="true"
:show-cancel-button="false"
:show-confirm-button="false"
>
<NotifyItem />
</NotifyItemModal>
</template>

View File

@ -0,0 +1,6 @@
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };

View File

@ -0,0 +1,216 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { z } from '@vben/common-ui';
import {
Button,
Col,
Image,
message as Message,
Row,
Spin,
Step,
Steps,
TabPane,
Tabs,
} from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
postUsersChangePassword,
postUsersMyProfile,
} from '#/api-client';
import { $t } from '#/locales';
defineOptions({
name: 'MyProfile',
});
const activeName = ref(0);
const loading = ref(false);
const [ProfileForm, profileFormApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: () => {},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.userName'),
},
fieldName: 'userName',
label: $t('abp.user.userName'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.userName'),
}),
disabled: true,
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.name'),
},
fieldName: 'name',
disabled: true,
label: $t('abp.user.name'),
},
// {
// component: 'VbenInput',
// componentProps: {
// placeholder: $t('common.pleaseInput') + $t('abp.user.surname'),
// },
// fieldName: 'surname',
// label: $t('abp.user.surname'),
// rules: z.string().min(1, {
// message: $t('common.pleaseInput') + $t('abp.user.surname'),
// }),
// },
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.email'),
},
fieldName: 'email',
disabled: true,
label: $t('abp.user.email'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.email'),
}),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.phone'),
},
fieldName: 'phoneNumber',
disabled: true,
label: $t('abp.user.phone'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.phone'),
}),
},
],
// showCollapseButton: false,
// showDefaultActions: false,
wrapperClass: 'grid-cols-1',
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
show: false,
},
});
const [ResetPasswordForm, resetPasswordApi] = useVbenForm({
//
collapsed: false,
//
commonConfig: {
//
componentProps: {
class: 'w-4/5',
},
},
//
handleSubmit: async () => {
//
const { valid } = await resetPasswordApi.validate();
if (!valid) return;
const formValues = await resetPasswordApi.getValues();
if (formValues.currentPassword === formValues.confirmPassword) {
Message.warn($t('abp.user.newPasswordAndCurrentPasswordNotAlike'));
return;
}
if (formValues.newPassword !== formValues.confirmPassword) {
Message.warn($t('abp.user.newPasswordAndConfirmPasswordNotMatch'));
return;
}
await postUsersChangePassword({ body: formValues });
Message.success($t('common.editSuccess'));
await resetPasswordApi.resetForm();
},
layout: 'horizontal',
schema: [
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
},
fieldName: 'currentPassword',
label: $t('abp.user.currentPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.currentPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.newPassword'),
},
fieldName: 'newPassword',
label: $t('abp.user.newPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.newPassword'),
}),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
},
fieldName: 'confirmPassword',
label: $t('abp.user.comfirmPassword'),
rules: z.string().min(1, {
message: $t('common.pleaseInput') + $t('abp.user.comfirmPassword'),
}),
},
],
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
content: '确认',
},
wrapperClass: 'grid-cols-1',
});
onMounted(async () => {
try {
loading.value = true;
const resp = await postUsersMyProfile();
await profileFormApi.setValues({ ...resp.data });
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
<Spin :spinning="loading" tip="loading...">
<div class="bg-card px-8">
<Tabs
v-model:active-key="activeName"
tab-position="left"
@change="activeChange"
>
<TabPane :key="0" :tab="$t('abp.user.myProfile')">
<ProfileForm />
</TabPane>
<TabPane :key="1" :tab="$t('abp.user.changePassword')">
<ResetPasswordForm />
</TabPane>
</Tabs>
</div>
</Spin>
</div>
</template>

View File

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

View File

@ -0,0 +1,102 @@
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref<Locale>(antdDefaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
*
*
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
*
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, setupI18n };

View File

@ -0,0 +1,131 @@
{
"login": {
"selectTenant": "Please select Tenant and ignore the non-tenant mode",
"oidcTip": "Login......"
},
"menu": {
"system": "SystemManagement",
"user": "UserManagement",
"role": "RoleManagement",
"tenant": "TenantManagement",
"tenantList": "TenantList",
"language": "LanguageManagement",
"auditLog": "AuditLog",
"loginLog": "LoginLog",
"feature": "FeatureManagement",
"setting": "SettingsManagement",
"organizationUnit": "OrganizationUnitManagement",
"languageText": "LanguageTextManagement",
"dataDictionary": "DataDictionaryManagement",
"notification": "NotificationManagement",
"message": "MessageManagement",
"code": "CodeManagement",
"code-project": "ProjectList",
"code-template": "TemplateList",
"code-genarate": "CodeGenarate",
"code-entity": "Entity",
"code-template-detail": "TemplateDetail",
"code-Preview": "Preview"
},
"user": {
"user": "User",
"userName": "UserName",
"name": "Name",
"surname": "Surname",
"email": "Email",
"phone": "Phone",
"password": "Password",
"currentPassword": "CurrentPassword",
"newPassword": "NewPassword",
"confirmNewPassword": "ConfirmNewPassword",
"changePassword": "ChangePassword",
"status": "Status",
"comfirmPassword": "ComfirmPassword",
"comfirmDeleteUser": "Are you sure you want to delete the user",
"newPasswordAndConfirmPasswordNotMatch": "New password and confirm password not match",
"newPasswordAndCurrentPasswordNotAlike": "The old and new passwords cannot be the same",
"myProfile": "MyProfile",
"myAccount": "MyAccount",
"twoFactor": "TwoFactor",
"verifyAuthenticator": "Verify the Authenticator",
"twoFactorDesc": "Your two-factor authentication app will generate a code. Please enter the code and confirm it.",
"code": "TwoFactor Code",
"twoFactorEnabled": "Two-factor authentication for your account has been successfully enabled. You will be required to enter the code generated by the application when logging in.",
"twoFactorDisabled": "Two-factor authentication for your account has been turned off. You will no longer be required to enter the code generated by the application when logging in.",
"twoFactorEnabledDesc": "Two-factor authentication has been enabled for your account. If you need to turn it off, please enter the two-factor authentication code and then disable it.",
"resetTwoFactor": "ResetTwoFactor"
},
"role": {
"role": "Role",
"roleName": "RoleName",
"isDefault": "IsDefault",
"permissions": "Permissions"
},
"log": {
"loginTime": "LoginTime",
"userName": "UserName",
"tenant": "Tenant",
"executionTime": "ExecutionTime",
"responseTime": "ResponseTime(ms)",
"clientIp": "ClientIp",
"exception": "Exception",
"applicationName": "ApplicationName",
"loginMode": "LoginMode",
"loginUrl": "LoginUrl",
"detail": "Detail"
},
"message": {
"title": "Title",
"content": "Content",
"type": "Type",
"level": "Level",
"sender": "Sender",
"receiver": "Receiver",
"isRead": "IsRead",
"sendMessage": "SendMessage",
"sendNotification": "SendNotification",
"setRead": "SetRead",
"confirmRead": "Are you sure you want to set the message as read?",
"read": "Read",
"unread": "UnRead"
},
"language": {
"language": "LanguageName",
"showLanguage": "ShowLanguageName",
"icon": "Icon",
"resourceName": "ResourceName",
"value": "Value",
"name": "Name"
},
"dataDictionary": {
"codeName": "Code|Name",
"code": "Code",
"name": "Name",
"order": "Order",
"status": "Status",
"description": "Description",
"type": "Type"
},
"organizationunit": {
"organizationunit": "Organizationunit",
"add": "Add",
"member": "Member",
"role": "Role",
"userName": "UserName",
"email": "Email",
"name": "Name",
"selectNode": "Select a node to operate on"
},
"tenant": {
"tenant": "Tenant",
"notExist": "Tenant Does not exist",
"name": "Tenant Name",
"adminEmail": "Admin Email",
"adminPassword": "Admin Password",
"mangeConnectionString": "MangeConnectionString",
"addorEdit": "AddorEdit",
"featureManagement": "FeatureManagement",
"connectionStringName": "ConnectionStringName",
"connectionString": "ConnectionString"
}
}

View File

@ -0,0 +1,37 @@
{
"companyName": "CompanyName",
"projectName": "ProjectName",
"projectEnglishName": "ProjectEnglishName",
"namespace": "Namespace",
"remark": "Remark",
"templateName": "TemplateName",
"model": "Model",
"property": "Property",
"enum": "Enum",
"isRequired": "IsRequired",
"maxLength": "MaxLength",
"minLength": "MinLength",
"decimalPrecision18": "Accuracy (18,6) of 18",
"decimalPrecision6": "Accuracy (18,6) of 6",
"pleaseSelectEntity": "Please select entity",
"pleaseSelectEnum": "Please select enum",
"detail": "Detail",
"copy": "Copy",
"addFolder": "AddFolder",
"addFile": "AddFile",
"desc": "Description",
"name": "Name",
"templateType": "TemplateType",
"supportTenant": "Support Multi-Tenant",
"project": "Project",
"template": "Template",
"preview": "Preview",
"download": "Download",
"description": "Please select the target and project to generate",
"autoGenerate": "Auto Generate Code",
"code": "Code",
"type": "Type",
"value": "Value",
"relational": "Relational",
"dataType": "DataType"
}

View File

@ -0,0 +1,39 @@
{
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"search": "Search",
"export": "Export",
"save": "save",
"seq": "Seq",
"isEnable": "IsEnable",
"enabled": "Enabled",
"disabled": "Disabled",
"action": "Action",
"createTime": "CreationTime",
"pleaseInput": "Please input",
"addSuccess": "Add Success",
"editSuccess": "Edit Success",
"deleteSuccess": "Delete Success",
"yes": "yes",
"no": "no",
"confirmDelete": "Confirm to delete",
"askConfirmDelete": "Confirm to delete?",
"warning": "warning",
"info": "info",
"error": "error",
"success": "Success",
"keyword": "Keyword",
"mesage403": "You don't have permission to access this page or function!",
"mesage401": "You are not logged in or the login has timed out. Please log in again!",
"mesage500": "Internal server error!",
"mesage404": "The page you are trying to access does not exist!",
"mesage400": "Request error!",
"mesage405": "Incorrect request method!",
"timeOut": "Request timed out!",
"expandAll": "EexpandAll",
"collapseAll": "CollapseAll",
"description": "description",
"comfirm": "Comfirm",
"valid": "Valid"
}

View File

@ -0,0 +1,15 @@
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password",
"thirdPartyLogin": "Third Party Login"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}

View File

@ -0,0 +1,131 @@
{
"login": {
"selectTenant": "请选择租户,非租户模式请忽略",
"oidcTip": "登陆中......"
},
"menu": {
"system": "系统管理",
"user": "用户管理",
"role": "角色管理",
"tenant": "租户管理",
"tenantList": "租户列表",
"language": "语言管理",
"auditLog": "审计日志",
"loginLog": "登录日志",
"feature": "功能管理",
"setting": "设置管理",
"organizationUnit": "组织机构管理",
"languageText": "语言文本管理",
"dataDictionary": "数据字典管理",
"notification": "通告管理",
"message": "消息管理",
"code": "代码管理",
"code-project": "项目列表",
"code-template": "模板列表",
"code-genarate": "代码生成",
"code-entity": "实体",
"code-template-detail": "模板详情",
"code-Preview": "预览"
},
"user": {
"user": "用户",
"userName": "用户名",
"name": "名称",
"surname": "姓氏",
"email": "邮箱",
"phone": "手机号",
"password": "密码",
"currentPassword": "当前密码",
"newPassword": "新密码",
"confirmNewPassword": "确认新密码",
"changePassword": "修改密码",
"status": "状态",
"comfirmPassword": "确认密码",
"comfirmDeleteUser": "确认删除用户",
"newPasswordAndConfirmPasswordNotMatch": "新密码与确认密码不匹配",
"newPasswordAndCurrentPasswordNotAlike": "新旧密码不能一样",
"myProfile": "个人信息",
"myAccount": "我的账户",
"twoFactor": "双因素验证",
"verifyAuthenticator": "验证身份验证器",
"twoFactorDesc": "您的双因素身份验证应用程序将生成一个代码,请输入该代码并确认.",
"code": "双因素验证码",
"twoFactorEnabled": "您的账户已经成功开启双因素验证,登录时要求输入应用程序生成的代码.",
"twoFactorDisabled": "您的账户已经关闭双因素验证,登录时不在要求输入应用程序生成的代码.",
"twoFactorEnabledDesc": "您的账户已经开启双因素验证,如果需要关闭,请输入双因素验证码,然后在禁用.如果忘记验证码,请联系管理员.",
"resetTwoFactor": "重置双因素验证"
},
"role": {
"role": "角色",
"roleName": "角色名称",
"isDefault": "是否默认",
"permissions": "授权"
},
"log": {
"loginTime": "登录时间",
"userName": "用户名",
"tenant": "租户",
"executionTime": "执行时间",
"responseTime": "响应时间(毫秒)",
"clientIp": "客户端Ip",
"exception": "异常",
"applicationName": "应用名称",
"loginMode": "登录方式",
"loginUrl": "登录地址",
"detail": "详情"
},
"message": {
"title": "标题",
"content": "内容",
"type": "类型",
"level": "级别",
"sender": "发送人",
"receiver": "接收人",
"isRead": "是否已读",
"sendMessage": "发送消息",
"sendNotification": "发送通告",
"setRead": "设置已读",
"confirmRead": "确认设置已读?",
"read": "已读",
"unread": "未读"
},
"language": {
"language": "语言名称",
"showLanguage": "显示名称",
"icon": "图标",
"resourceName": "资源名称",
"value": "值",
"name": "名称"
},
"dataDictionary": {
"codeName": "编码|名称",
"code": "编码",
"name": "名称",
"order": "排序",
"status": "状态",
"description": "描述",
"type": "字典类型"
},
"organizationunit": {
"organizationunit": "组织机构",
"add": "新增根机构",
"member": "成员",
"role": "角色",
"userName": "用户名",
"email": "邮箱",
"name": "名称",
"selectNode": "请先选择一个节点再进行操作"
},
"tenant": {
"tenant": "租户",
"notExist": "租户不存在",
"name": "租户名称",
"adminEmail": "管理员邮箱",
"adminPassword": "管理员密码",
"mangeConnectionString": "管理链接字符串",
"addorEdit": "新增或编辑",
"featureManagement": "功能管理",
"connectionStringName": "连接名称",
"connectionString": "连接字符串"
}
}

View File

@ -0,0 +1,37 @@
{
"companyName": "公司名称",
"projectName": "项目名称",
"projectEnglishName": "项目英文名称",
"namespace": "命名空间",
"remark": "备注",
"templateName": "模板名称",
"model": "模型",
"property": "属性",
"enum": "枚举",
"isRequired": "是否必填",
"maxLength": "最大长度",
"minLength": "最小长度",
"decimalPrecision18": "精度(18,6)中的18",
"decimalPrecision6": "精度(18,6)中的6",
"pleaseSelectEntity": "请选择实体",
"pleaseSelectEnum": "请选择枚举",
"detail": "详情",
"copy": "复制",
"addFolder": "新增文件夹",
"addFile": "新增文件",
"desc": "描述",
"name": "名称",
"templateType": "模板类型",
"supportTenant": "支持多租户",
"project": "项目",
"template": "模板",
"preview": "预览",
"download": "下载",
"description": "请选择要生成的目标和项目",
"autoGenerate": "自动生成代码",
"code": "编码",
"type": "类型",
"value": "值",
"relational": "关系",
"dataType": "数据类型"
}

View File

@ -0,0 +1,39 @@
{
"add": "新增",
"edit": "编辑",
"delete": "删除",
"search": "搜索",
"save": "保存",
"export": "导出",
"isEnable": "是否启用",
"enabled": "启用",
"disabled": "禁用",
"seq": "序号",
"action": "操作",
"createTime": "创建时间",
"pleaseInput": "请输入",
"addSuccess": "新增成功!",
"editSuccess": "编辑成功!",
"deleteSuccess": "删除成功!",
"yes": "是",
"no": "否",
"confirmDelete": "确认删除",
"askConfirmDelete": "确认删除吗?",
"warning": "警告",
"info": "正常",
"error": "错误",
"success": "成功",
"keyword": "关键字",
"mesage403": "您没有权限访问此页面或功能!",
"mesage401": "未登录或登录已超时,请重新登录!",
"mesage500": "服务器内部错误!",
"mesage404": "您访问的页面不存在!",
"mesage400": "请求错误!",
"mesage405": "请求方法错误!",
"timeOut": "请求超时!",
"expandAll": "展开全部",
"collapseAll": "折叠全部",
"description": "描述",
"comfirm": "确认",
"valid": "验证"
}

View File

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

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

@ -0,0 +1,35 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
// eslint-disable-next-line unused-imports/no-unused-imports
import client from '#/api-client-config/index';
// eslint-disable-next-line unused-imports/no-unused-imports
import clientblob from '#/api-client-config/index-blob';
/**
*
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();

View File

@ -0,0 +1,23 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description
* 使
* !!!
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
// 是否开启检查更新
enableCheckUpdates: false,
// 检查更新的时间间隔,单位为分钟
checkUpdatesInterval: 1,
},
theme: {
mode: 'light',
},
copyright: {
companyName: '集社',
},
});

View File

@ -0,0 +1,42 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@ -0,0 +1,138 @@
import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
*
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 访
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
DEFAULT_HOME_PATH,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === DEFAULT_HOME_PATH
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
// const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
// const userRoles = userInfo.roles ?? [];
const refreshCount = userStore?.applicationInfo?.refreshCount ?? -1;
if (import.meta.env.VITE_REFRESH_ROLE && refreshCount > 0) {
await authStore.getApplicationConfiguration();
}
const userRoles = accessStore.accessCodes ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === DEFAULT_HOME_PATH
? DEFAULT_HOME_PATH
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
*
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View File

@ -0,0 +1,37 @@
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };

View File

@ -0,0 +1,96 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { AuthPageLayout, BasicLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
*
* 使BasicLayout
*
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: DEFAULT_HOME_PATH,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: Login,
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@ -0,0 +1,37 @@
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 404
* */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };

View File

@ -0,0 +1,38 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,144 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'lucide:layout-dashboard',
order: 1,
title: $t('abp.menu.system'),
authority: ['AbpIdentity'],
},
name: 'system',
path: '/system',
children: [
{
name: 'abpUser',
path: 'user',
component: () => import('#/views/system/abpuser/index.vue'),
meta: {
// affixTab: true,
icon: 'ph:user',
title: $t('abp.menu.user'),
authority: ['AbpIdentity.Users'],
},
},
{
name: 'abpRole',
path: 'role',
component: () => import('#/views/system/abprole/index.vue'),
meta: {
icon: 'oui:app-users-roles',
title: $t('abp.menu.role'),
authority: ['AbpIdentity.Roles'],
},
},
{
name: 'OrganizationUnit',
path: 'organizationUnit',
component: () => import('#/views/system/abporganizationunit/index.vue'),
meta: {
title: $t('abp.menu.organizationUnit'),
authority: ['AbpIdentity.OrganizationUnitManagement'],
icon: 'ant-design:team-outlined',
},
},
{
name: 'abpSetting',
path: 'setting',
component: () => import('#/views/system/abpsetting/index.vue'),
meta: {
icon: 'uil:setting',
title: $t('abp.menu.setting'),
authority: ['AbpIdentity.Setting'],
},
},
{
name: 'abpfeature',
path: 'Feature',
component: () => import('#/views/system/abpfeature/index.vue'),
meta: {
icon: 'ant-design:tool-outlined',
title: $t('abp.menu.feature'),
authority: ['AbpIdentity.FeatureManagement'],
},
},
{
name: 'AbpAuditLog',
path: 'auditlog',
component: () => import('#/views/system/abplog/audit.vue'),
meta: {
title: $t('abp.menu.auditLog'),
authority: ['AbpIdentity.AuditLog'],
icon: 'ant-design:snippets-twotone',
},
},
{
name: 'AbpLoginLog',
path: 'loginlog',
component: () => import('#/views/system/abplog/login.vue'),
meta: {
title: $t('abp.menu.loginLog'),
authority: ['AbpIdentity.IdentitySecurityLogs'],
icon: 'ant-design:file-protect-outlined',
},
},
{
name: 'AboLanguage',
path: 'language',
component: () => import('#/views/system/abplanguage/language.vue'),
meta: {
title: $t('abp.menu.language'),
authority: ['AbpIdentity.Languages'],
icon: 'ant-design:read-outlined',
},
},
{
name: 'AboLanguageText',
path: 'languagetext',
component: () => import('#/views/system/abplanguage/languagetext.vue'),
meta: {
title: $t('abp.menu.languageText'),
authority: ['AbpIdentity.LanguageTexts'],
icon: 'ant-design:font-size-outlined',
},
},
{
name: 'DataDictionary',
path: 'data-dictionary',
component: () => import('#/views/system/abpdatadictionary/index.vue'),
meta: {
title: $t('abp.menu.dataDictionary'),
authority: ['AbpIdentity.DataDictionaryManagement'],
icon: 'ant-design:table-outlined',
},
},
{
name: 'AbpNotification',
path: 'notification',
component: () =>
import('#/views/system/abpnotification/notification.vue'),
meta: {
title: $t('abp.menu.notification'),
authority: ['AbpIdentity.NotificationSubscriptionManagement'],
icon: 'ant-design:comment-outlined',
},
},
{
name: 'AbpMessage',
path: 'message',
component: () => import('#/views/system/abpnotification/message.vue'),
meta: {
title: $t('abp.menu.message'),
authority: ['AbpIdentity.NotificationManagement'],
icon: 'ant-design:customer-service-twotone',
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,32 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ant-design:switcher-filled',
order: 2,
title: $t('abp.menu.tenant'),
authority: ['AbpTenantManagement'],
},
name: 'tenant',
path: '/tenant',
children: [
{
name: 'abpTenant',
path: 'page',
component: () => import('#/views/system/abptenant/index.vue'),
meta: {
icon: 'ph:user',
title: $t('abp.menu.tenantList'),
authority: ['AbpTenantManagement.Tenants'],
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,152 @@
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { message as Message, notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import {
type ApplicationAuthConfigurationDto,
type ApplicationConfigurationDto,
getApiAbpApplicationConfiguration,
postApiAppAccountLogin,
postTenantsFind,
} from '#/api-client';
import { useSignalR } from '#/hooks/useSignalR';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
*
* Asynchronously handle the login process
* @param params
*/
async function authLogin(
params: Recordable<any>,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
// 判断是否租户登录
if (params.tenant) {
const tenantResult = await postTenantsFind({
body: {
name: params.tenant,
},
});
if (tenantResult.data?.success) {
userStore.setTenantInfo(tenantResult.data as any);
} else {
Message.error(`${params.tenant}$t('abp.tenant.notExist')`);
userStore.setTenantInfo(null);
delete params.tenant;
return;
}
}
const { data = {} } = await postApiAppAccountLogin({
body: {
...params,
},
});
// 如果成功获取到 accessToken
if (data.token) {
accessStore.setAccessToken(data.token);
userInfo = data as any;
userStore.setUserInfo(userInfo as any);
await getApplicationConfiguration();
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(DEFAULT_HOME_PATH);
}
if (userInfo?.userName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.userName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} catch {
userStore.setTenantInfo(null);
userStore.setUserInfo(null);
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
try {
// await logoutApi();
const { closeConnect } = useSignalR();
closeConnect();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
const userInfo: null | UserInfo = null;
// userInfo = await getUserInfoApi();
// userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
async function getApplicationConfiguration() {
const { data: authData } = await getApiAbpApplicationConfiguration({
query: { IncludeLocalizationResources: false },
});
const { auth } = authData as ApplicationConfigurationDto;
const accessCodes = Object.keys(
(auth as ApplicationAuthConfigurationDto)
.grantedPolicies as unknown as Record<string, any>,
);
accessStore.setAccessCodes(accessCodes);
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
getApplicationConfiguration,
};
});

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { About } from '@vben/common-ui';
defineOptions({ name: 'About' });
</script>
<template>
<About />
</template>

View File

@ -0,0 +1,69 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
codeLength: CODE_LENGTH,
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable<any>) {
// eslint-disable-next-line no-console
console.log(values);
}
</script>
<template>
<AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading"
@submit="handleLogin"
/>
</template>

View File

@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
}
</script>
<template>
<AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@ -0,0 +1,76 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import { computed, onBeforeMount, ref } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { getApiAbpApplicationConfiguration } from '#/api-client/index';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const showThirdPartyLogin = ref(false);
const thirdPartLoginList = ref([]);
const showTenant = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('abp.login.selectTenant'),
},
fieldName: 'tenant',
label: $t('abp.tenant.tenant'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'name',
label: $t('authentication.username'),
rules: z
.string()
.min(1, { message: $t('authentication.usernameTip') })
.default('admin'),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z
.string()
.min(1, { message: $t('authentication.passwordTip') })
.default('1q2w3E*'),
},
];
});
onBeforeMount(async () => {
const result = await getApiAbpApplicationConfiguration();
showThirdPartyLogin.value = result.data?.oidcConfiguration
?.enabled as boolean;
thirdPartLoginList.value = result.data?.oidcConfiguration
?.oidcConfiguration as [];
//
showTenant.value = result.data?.multiTenancy?.isEnabled ?? false;
});
</script>
<template>
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
:show-tenant-login="showTenant"
:show-third-party-login="showThirdPartyLogin"
:third-part-login-list="thirdPartLoginList as any"
@submit="authStore.authLogin"
/>
</template>

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

View File

@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, h, ref } from 'vue';
import { AuthenticationRegister, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'Register' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string({ required_error: $t('authentication.passwordTip') })
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class: 'vben-link ml-1 ',
href: '',
},
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: Recordable<any>) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
}
</script>
<template>
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
@submit="handleSubmit"
/>
</template>

View File

@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback403Demo' });
</script>
<template>
<Fallback status="403" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback500Demo' });
</script>
<template>
<Fallback status="500" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback404Demo' });
</script>
<template>
<Fallback status="404" />
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'FallbackOfflineDemo' });
</script>
<template>
<Fallback status="offline" />
</template>

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

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

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