admin 初始化

This commit is contained in:
zoujiandong 2023-06-05 09:25:16 +08:00
parent 12ecc21e37
commit 51d369d4c1
82 changed files with 7662 additions and 11 deletions

1
.env.development Normal file
View File

@ -0,0 +1 @@
VITE_BASE_URL= 'https://vue3.go-admin.dev'

0
.env.production Normal file
View File

15
.eslintrc.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
'plugin:vue/base',
'plugin:vue/vue3-essential',
// 'plugin:vue/vue3-strongly-recommended'
//'plugin:vue/vue3-recommended',
// 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}
}

36
.gitignore vendored
View File

@ -1,11 +1,31 @@
# ---> Vue
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# TODO: where does this rule come from?
docs/_book
yarn.lock
# TODO: where does this rule come from?
test/
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# node_modules
node_modules
package-lock.json
package-lock.json

320
README.md
View File

@ -1,4 +1,318 @@
# hospital-admin
# Vue 3 + Vite
互联网医院
前端
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
# go-admin
<img align="right" width="320" src="https://gitee.com/mydearzwj/image/raw/master/img/go-admin.svg">
[![Build Status](https://github.com/wenjianzhang/go-admin/workflows/build/badge.svg)](https://github.com/go-admin-team/go-admin)
[![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases)
[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/go-admin-team/go-admin)
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | 简体中文
基于Gin + Vue + Argo Design UI的前后端分离权限管理系统,系统初始化极度简单只需要配置文件中修改数据库连接系统支持多指令操作迁移指令可以让初始化数据库信息变得更简单服务指令可以很简单的启动api服务
[在线文档](https://doc.go-admin.dev)
[github在线文档](https://wenjianzhang.github.io)
[gitee在线文档](http://mydearzwj.gitee.io/go-admin-doc/)
[后端项目](https://github.com/go-admin-team/go-admin)
[视频教程](https://space.bilibili.com/565616721/channel/detail?cid=125737)
## ✨ 特性
- 遵循 RESTful API 设计规范
- 基于 GIN WEB API 框架提供了丰富的中间件支持用户认证、跨域、访问日志、追踪ID等
- 基于Casbin的 RBAC 访问控制模型
- JWT 认证
- 支持 Swagger 文档(基于swaggo)
- 基于 GORM 的数据库存储,可扩展多种类型数据库
- 配置文件简单的模型映射,快速能够得到想要的配置
- 代码生成工具
- 表单构建工具
- 多指令模式
- TODO: 单元测试
## 🎁 内置
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识,接口权限等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
9. 登录日志:系统登录日志记录查询包含登录异常。
1. 接口文档根据业务代码自动生成相关的api接口文档。
1. 代码生成:根据数据表结构生成对应的增删改查相对应业务,全程可视化操作,让基本业务可以零代码实现。
1. 表单构建:自定义页面样式,拖拉拽实现页面布局。
1. 服务监控:查看一些服务器的基本信息。
1. 内容管理demo功能下设分类管理、内容管理。可以参考使用方便快速入门。
## 准备工作
你需要在本地安装 [go] [gin] [node](http://nodejs.org/) 和 [git](https://git-scm.com/)
同时配套了系列教程包含视频和文档,如何从下载完成到熟练使用,强烈建议大家先看完这些教程再来实践本项目!!!
### 轻松实现go-admin写出第一个应用 - 文档教程
[步骤一 - 基础内容介绍](http://doc.zhangwj.com/go-admin-site/guide/intro/tutorial01.html)
[步骤二 - 实际应用 - 编写增删改查](http://doc.zhangwj.com/go-admin-site/guide/intro/tutorial02.html)
### 手把手教你从入门到放弃 - 视频教程
[如何启动go-admin](https://www.bilibili.com/video/BV1z5411x7JG)
[使用生成工具轻松实现业务](https://www.bilibili.com/video/BV1Dg4y1i79D)
[v1.1.0版本代码生成工具-释放双手](https://www.bilibili.com/video/BV1N54y1i71P) [进阶]
[多命令启动方式讲解以及IDE配置](https://www.bilibili.com/video/BV1Fg4y1q7ph)
[go-admin菜单的配置说明](https://www.bilibili.com/video/BV1Wp4y1D715) [必看]
[如何配置菜单信息以及接口信息](https://www.bilibili.com/video/BV1zv411B7nG) [必看]
[go-admin权限配置使用说明](https://www.bilibili.com/video/BV1rt4y197d3) [必看]
[go-admin数据权限使用说明](https://www.bilibili.com/video/BV1LK4y1s71e) [必看]
**如有问题请先看上述使用文档和文章,若不能满足,欢迎 issue 和 pr ,视频教程和文档持续更新中**
## 📦 本地开发
### 环境要求
go 1.17
node版本: v16 +
npm版本: 6.14.11
### 开发目录创建
```bash
# 创建开发目录
mkdir goadmin
cd goadmin
```
### 获取代码
> 重点注意:两个项目必须放在同一文件夹下;
```bash
# 获取后端代码
git clone https://github.com/go-admin-team/go-admin.git
# 获取前端代码
git clone https://github.com/go-admin-team/go-admin-ui.git
```
### 启动说明
#### 前端启动说明
```bash
# 安装依赖
yarn install 或 npm install
# 运行
yarn run dev 或 npm run dev
# 编译
yarn run build 或 npm run build
```
#### 服务端启动说明
```bash
# 进入 go-admin 后端项目
cd ./go-admin
# 编译项目
go build
# 修改配置
# 文件路径 go-admin/config/settings.yml
vi ./config/setting.yml
# 1. 配置文件中修改数据库信息
# 注意: settings.database 下对应的配置数据
# 2. 确认log路径
```
:::tip ⚠️注意 在windows环境如果没有安装中CGO会出现这个问题
```bash
E:\go-admin>go build
# github.com/mattn/go-sqlite3
cgo: exec /missing-cc: exec: "/missing-cc": file does not exist
```
or
```bash
D:\Code\go-admin>go build
# github.com/mattn/go-sqlite3
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
```
[解决cgo问题进入](https://doc.go-admin.dev/guide/other/faq.html#_5-cgo-exec-missing-cc-exec-missing-cc-file-does-not-exist)
:::
#### 初始化数据库,以及服务启动
``` bash
# 首次配置需要初始化数据库资源信息
# macOS or linux 下使用
$ ./go-admin migrate -c=config/settings.dev.yml
# ⚠️注意:windows 下使用
$ go-admin.exe migrate -c=config/settings.dev.yml
# 启动项目也可以用IDE进行调试
# macOS or linux 下使用
$ ./go-admin server -c config/settings.yml
# ⚠️注意:windows 下使用
$ go-admin.exe server -c config/settings.yml
```
#### 使用docker 编译启动
```shell
# 编译镜像
docker build -t go-admin .
# 启动容器第一个go-admin是容器名字第二个go-admin是镜像名称
# -v 映射配置文件 本地路径:容器路径
docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server
```
#### 文档生成
```bash
go generate
```
#### 交叉编译
```bash
# windows
env GOOS=windows GOARCH=amd64 go build main.go
# or
# linux
env GOOS=linux GOARCH=amd64 go build main.go
```
### UI交互端启动说明
```bash
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
```
## 🎬 在线体验
> admin / 123456
演示地址:[http://www.go-admin.dev](http://www.go-admin.dev/#/login)
## 📨 互动
<table>
<tr>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq.png" width="200px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
</tr>
<tr>
<td>微信</td>
<td>此群已满</td>
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
</tr>
</table>
## 💎 主要成员
<a href="https://github.com/wenjianzhang"> <img src="https://avatars.githubusercontent.com/u/3890175?s=460&u=20eac63daef81588fbac611da676b99859319251&v=4" width="80px"></a>
<a href="https://github.com/lwnmengjing"> <img src="https://avatars.githubusercontent.com/u/12806223?s=400&u=a89272dce50100b77b4c0d5c81c718bf78ebb580&v=4" width="80px"></a>
<a href="https://github.com/chengxiao"> <img src="https://avatars.githubusercontent.com/u/1379545?s=460&u=557da5503d0ac4a8628df6b4075b17853d5edcd9&v=4" width="80px"></a>
<a href="https://github.com/bing127"> <img src="https://avatars.githubusercontent.com/u/31166183?s=460&u=c085bff88df10bb7676c8c0351ba9dcd031d1fb3&v=4" width="80px"></a>
## JetBrains 开源证书支持
`go-admin` 项目一直以来都是在 JetBrains 公司旗下的 GoLand 集成开发环境中进行开发,基于 **free JetBrains Open Source license(s)** 正版免费授权,在此表达我的谢意。
<a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
## 🤝 特别感谢
1. [chengxiao](https://github.com/chengxiao)
2. [gin](https://github.com/gin-gonic/gin)
2. [casbin](https://github.com/casbin/casbin)
2. [spf13/viper](https://github.com/spf13/viper)
2. [gorm](https://github.com/jinzhu/gorm)
2. [gin-swagger](https://github.com/swaggo/gin-swagger)
2. [jwt-go](https://github.com/dgrijalva/jwt-go)
2. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
2. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
2. [form-generator](https://github.com/JakHuang/form-generator)
## 🤟 打赏
> 如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 :tropical_drink:
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
## 🤝 链接
[Go开发者成长线路图](http://www.golangroadmap.com/)
## 🔑 License
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
Copyright (c) 2020 wenjianzhang

16
components.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
DeleteModal: typeof import('./src/components/DeleteModal.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

14
jsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"allowSyntheticDefaultImports": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": [
"node_modules"
]
}

4
mock/index.js Normal file
View File

@ -0,0 +1,4 @@
import user from './sys_post'
export default [
user,
]

36
mock/sys_post.js Normal file
View File

@ -0,0 +1,36 @@
import Mock from 'mockjs'
const mock = {
'list|10': [
{
'roleId|+1': 1,
'roleName|1': [
'系统管理员',
'福州系统运维',
'产品管理',
'前端开发',
'后端开发'
],
'roleKey': 'admin',
'roleSort|+1': 0,
'status|0-1': 0,
'createAt': `${Mock.Random.date()} ${Mock.Random.time()}`
}
],
}
const login = {
url: '/api/admin/post',
method: 'get',
timeout: 500,
statusCode: 200,
response: {
code: 200,
message: '登陆成功',
data: Mock.toJSONSchema(mock).template
}
}
export default [
login,
]

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "quark_arco",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@codemirror/lang-html": "^6.4.3",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@vueuse/core": "^10.1.2",
"axios": "^1.4.0",
"codemirror": "^6.0.1",
"cropperjs": "^1.5.13",
"i": "^0.3.7",
"js-cookie": "^3.0.5",
"npm": "^9.6.6",
"pinia": "^2.0.36",
"vue": "^3.2.47",
"vue-codemirror": "^6.1.1",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@arco-design/web-vue": "^2.45.3",
"@vitejs/plugin-vue": "^4.2.1",
"unplugin-vue-components": "^0.24.1",
"eslint": "^8.40.0",
"eslint-plugin-vue": "^9.12.0",
"mockjs": "^1.1.0",
"sass": "^1.62.1",
"vite": "^4.3.5",
"vite-plugin-mock": "^3.0.0",
"vite-svg-loader": "^4.0.0"
}
}

19
prettier.config.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/login_left_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

3
src/App.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

30
src/api/admin/login.js Normal file
View File

@ -0,0 +1,30 @@
import request from '../../utils/request';
export function login(data) {
return request({
url:'/api/v1/login',
method: 'post',
data
})
}
export function getCaptcha() {
return request({
url:'/api/v1/captcha',
method: 'get'
})
}
export function getAppConfig() {
return request({
url:'/api/v1/app-config',
method: 'get'
})
}
// 根据角色获取菜单
export function getUserMenuRole() {
return request({
url:'/api/v1/menurole',
method: 'get'
})
}

48
src/api/admin/menu.js Normal file
View File

@ -0,0 +1,48 @@
import request from '../../utils/request'
const url = '/api/v1/menu';
export function getMenu(params) {
return request({
url,
method: 'get',
params
})
}
export function getMenuDetails(menuId) {
return request({
url: `${url}/${menuId}`,
method: 'get'
})
}
export function addMenu(data) {
return request({
url,
method: 'post',
data
})
}
export function removeMenu(data) {
return request({
url,
method: 'delete',
data
})
}
/**
* 修改菜单
* @param {Object} data
* @param {Number} id
* @returns
*/
export function updateMenu(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data
})
}

35
src/api/admin/post.js Normal file
View File

@ -0,0 +1,35 @@
import request from '../../utils/request'
const url = '/api/v1/post';
export function getPost(params) {
return request({
url,
method: 'get',
params
})
}
export function addPost(data) {
return request({
url,
method: 'post',
data
})
}
export function removePost(data) {
return request({
url,
method: 'delete',
data
})
}
export function updatePost(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data
})
}

50
src/api/admin/role.js Normal file
View File

@ -0,0 +1,50 @@
import request from '../../utils/request';
const url = '/api/v1/role';
export function getRole(params) {
return request({
url,
method: 'get',
params
})
}
export function addRole(data) {
return request({
url,
method: 'post',
data
})
}
export function removeRole(data) {
return request({
url,
method: 'delete',
data
})
}
export function updateRole(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data,
})
}
export function updateRoleScoped(data) {
return request({
url:'/api/v1/roledatascope',
method: 'put',
data
})
}
export function getRoleMenuTree(params, roleId) {
return request({
url:`/api/v1/roleMenuTreeselect/${roleId}`,
method: 'get',
params
})
}

35
src/api/admin/sys-api.js Normal file
View File

@ -0,0 +1,35 @@
import request from '../../utils/request';
const url = '/api/v1/sys-api';
export function getSysApi(params) {
return request({
url,
method: 'get',
params
})
}
export function addSysApi(data) {
return request({
url,
method: 'post',
data
})
}
export function removeSysApi(data) {
return request({
url,
method: 'delete',
data
})
}
export function updateSysApi(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data,
})
}

View File

@ -0,0 +1,35 @@
import request from '../../utils/request';
const url = '/api/v1/config';
export function getSysConfig(params) {
return request({
url,
method: 'get',
params
})
}
export function addSysConfig(data) {
return request({
url,
method: 'post',
data
})
}
export function removeSysConfig(data) {
return request({
url,
method: 'delete',
data
})
}
export function updateSysConfig(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data,
})
}

35
src/api/admin/sys-dept.js Normal file
View File

@ -0,0 +1,35 @@
import request from '../../utils/request';
const url = '/api/v1/dept';
export function getDept(params) {
return request({
url,
method: 'get',
params
})
}
export function addDept(data) {
return request({
url,
method: 'post',
data
})
}
export function removeDept(data) {
return request({
url,
method: 'delete',
data
})
}
export function updateDept(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data,
})
}

View File

@ -0,0 +1,35 @@
import request from '@/utils/request';
const url = '/api/v1/dict/data';
export function getDictData(params) {
return request({
url,
method: 'get',
params
})
}
export function addDictData(data) {
return request({
url,
method: 'post',
data
})
}
export function updateDictData(data, dictCode) {
return request({
url: `${url}/${dictCode}`,
method: 'put',
data
})
}
export function deleteDictData(data) {
return request({
url,
method: 'delete',
data
})
}

43
src/api/admin/sys-dict.js Normal file
View File

@ -0,0 +1,43 @@
import request from '../../utils/request';
const url = '/api/v1/dict/type';
export function getDictType(params) {
return request({
url,
method: 'get',
params
})
}
export function addDictType(data) {
return request({
url,
method: 'post',
data
})
}
export function removeDictType(data) {
return request({
url,
method: 'delete',
data
})
}
export function updateDictType(data, id) {
return request({
url: `${url}/${id}`,
method: 'put',
data,
})
}
// 获取字典选择框列表
export function optionselect() {
return request({
url: '/api/v1/dict/type-option-select',
method: 'get'
})
}

View File

@ -0,0 +1,19 @@
import request from '../../utils/request';
const url = '/api/v1/sys-login-log';
export function getSysLoginLog(params) {
return request({
url,
method: 'GET',
params
})
}
export function removeSysLoginLog(data) {
return request({
url,
method: 'DELETE',
data
})
}

View File

@ -0,0 +1,19 @@
import request from '../../utils/request';
const url = '/api/v1/sys-opera-log';
export function getSysOperaLog(params) {
return request({
url,
method: 'get',
params,
});
}
export function removeSysOperaLog(data) {
return request({
url,
method: 'delete',
data,
});
}

67
src/api/admin/sys-user.js Normal file
View File

@ -0,0 +1,67 @@
import request from '../../utils/request';
const url = '/api/v1/sys-user';
export function getUser(params) {
return request({
url,
method: 'get',
params
})
}
export function getInfo() {
return request({
url:'/api/v1/getinfo',
method: 'get'
})
}
export function addUser(data) {
return request({
url,
method: 'post',
data
})
}
export function removeUser(data) {
return request({
url,
method: 'delete',
data
})
}
export function updateUser(data) {
return request({
url,
method: 'put',
data,
})
}
export function updateUserStatus(data) {
return request({
url: '/api/v1/user/status',
method: 'put',
data
})
}
export function resetUserPwd(data) {
return request({
url:'/api/v1/user/pwd/reset',
method: 'put',
data
})
}
// 获取当前登录用户信息
export function getCurrentUser(uid) {
return request({
url: `${url}/${uid}`,
method: 'get'
})
}

View File

@ -0,0 +1,18 @@
import request from '../../utils/request';
// 获取用户信息
export function getUserProfile() {
return request({
url: '/api/v1/user/profile',
method: 'get'
})
}
// 修改用户密码
export function putUserPwd(data) {
return request({
url: '/api/v1/user/pwd/set',
method: 'put',
data
})
}

62
src/api/sys-job.js Normal file
View File

@ -0,0 +1,62 @@
import request from '@/utils/request'
// 查询SysJob列表
export function listSysJob(query) {
return request({
url: '/api/v1/sysjob',
method: 'get',
params: query
})
}
// 查询SysJob详细
export function getSysJob(jobId) {
return request({
url: '/api/v1/sysjob/' + jobId,
method: 'get'
})
}
// 新增SysJob
export function addSysJob(data) {
return request({
url: '/api/v1/sysjob',
method: 'post',
data: data
})
}
// 修改SysJob
export function updateSysJob(data) {
return request({
url: '/api/v1/sysjob',
method: 'put',
data: data
})
}
// 删除SysJob
export function delSysJob(data) {
return request({
url: '/api/v1/sysjob',
method: 'delete',
data: data
})
}
// 移除SysJob
export function removeJob(jobId) {
return request({
url: '/api/v1/job/remove/' + jobId,
method: 'get'
})
}
// 启动SysJob
export function startJob(jobId) {
return request({
url: '/api/v1/job/start/' + jobId,
method: 'get'
})
}

View File

@ -0,0 +1,10 @@
import request from '../../utils/request'
const url = '/api/v1/server-monitor';
export function getServerMonitor() {
return request({
url,
method: 'GET',
})
}

103
src/api/tools/gen.js Normal file
View File

@ -0,0 +1,103 @@
import request from '@/utils/request'
// 查询生成表数据
export function listTable(query) {
return request({
url: '/api/v1/sys/tables/page',
method: 'get',
params: query
})
}
// 查询db数据库列表
export function listDbTable(query) {
return request({
url: '/api/v1/db/tables/page',
method: 'get',
params: query
})
}
// 查询表详细信息
export function getGenTable(tableId) {
return request({
url: '/api/v1/sys/tables/info/' + tableId,
method: 'get'
})
}
export function getGenTableInfo(tablename) {
return request({
url: '/api/v1/sys/tables?tableName=' + tablename,
method: 'get'
})
}
// 修改代码生成信息
export function updateGenTable(data) {
return request({
url: '/api/v1/sys/tables/info',
method: 'put',
data: data
})
}
// 导入表
export function importTable(data) {
return request({
url: '/api/v1/sys/tables/info',
method: 'post',
params: data
})
}
// 预览生成代码
export function previewTable(tableId) {
return request({
url: '/api/v1/gen/preview/' + tableId,
method: 'get'
})
}
// 删除表数据
export function delTable(tableId) {
return request({
url: '/api/v1/sys/tables/info/' + tableId,
method: 'delete'
})
}
// 生成代码到项目
export function toProjectTable(tableId) {
return request({
url: '/api/v1/gen/toproject/' + tableId,
method: 'get'
})
}
// 生成接口数据到迁移脚本
export function apiToFile(tableId) {
return request({
url: '/api/v1/gen/apitofile/' + tableId,
method: 'get'
})
}
export function toProjectTableCheckRole(tableId, ischeckrole) {
return request({
url: '/api/v1/gen/toproject/' + tableId + '?ischeckrole=' + ischeckrole,
method: 'get'
})
}
// 生成菜单到数据库
export function toDBTable(tableId) {
return request({
url: '/api/v1/gen/todb/' + tableId,
method: 'get'
})
}
export function getTableTree() {
return request({
url: '/api/v1/gen/tabletree',
method: 'get'
})
}

View File

@ -0,0 +1,104 @@
<template>
<a-modal v-model:visible="deleteVisible" :title="data.length > 1 ? '批量删除' : '删除'" title-align="start" @close="handleClose" width="400px">
<a-form :model="data" :rules="rules" ref="modalFormRef" size="medium" label-align="left" auto-label-width>
<a-alert type="warning" style="margin-bottom: 16px;" v-if="data.length > 1">您正在批量删除请确认删除数据是否正确</a-alert>
<a-form-item label="摘要" label-col-flex="100px">
<a-tag color="gray" style="padding: 0 0px; margin-right: 5px">
<template #icon>
<a-tag color="arcoblue">数量</a-tag>
</template>
<a-tag style="margin-left: -4px">{{ data.length }}</a-tag>
</a-tag>
</a-form-item>
<a-form-item field="deleteConfirmation" label="删除确认" label-col-flex="100px">
<a-input v-model.trim="data.deleteConfirmation" placeholder="请输入 DELETE 以确认删除"></a-input>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleClose"><template #icon><icon-close /></template>取消</a-button>
<a-button type="primary" @click="handleConfirm"><template #icon><icon-check /></template>确认</a-button>
</template>
</a-modal>
</template>
<script setup>
import { ref, reactive, toRefs, watch, unref, getCurrentInstance } from 'vue';
import { Message } from '@arco-design/web-vue';
const { proxy } = getCurrentInstance();
const props = defineProps({
//
data: {
type: Array,
},
//
visible: {
type: Boolean,
default: false,
},
//
apiDelete: {
type: Function,
},
});
const { data, visible, apiDelete } = toRefs(props);
const emits = defineEmits(['deleteVisibleChange']);
// Akiraka 20230210
const deleteVisible = ref(false);
// Akiraka 20230210
const rules = reactive({
deleteConfirmation: [
{
required: true,
validator: (value, cb) => {
return new Promise(resolve => {
setTimeout(() => {
if ( "DELETE" !== value) {
cb('请输入 DELETE')
}
resolve()
}, 500)
})
}
}
],
});
// Akiraka 20230210
const handleClose = () => {
emits('deleteVisibleChange', deleteVisible.value = false);
// Akiraka 20230210
deleteVisible.value = false;
}
// Akiraka 20230210
watch(() => props.visible,(value) => {
if (value) {
// Akiraka 20230210
deleteVisible.value = value;
}
})
// Akiraka 20230210 =>
const handleConfirm = () => {
proxy.$refs.modalFormRef.validate((valid) => {
if (!valid) {
// Akiraka 20230210
props.apiDelete({ ids: data.value }).then(response => {
// Akiraka 20230210
deleteVisible.value = false;
Message.success("数据删除成功");
})
}
})
}
</script>
<style lang="less" scoped>
</style>

View File

@ -0,0 +1,22 @@
import { useUserStore } from '@/store/userInfo';
export default {
checkPermission(el, binding) {
const store = useUserStore();
const { value } = binding;
const all_permission = '*:*:*'
const permissions = store.userInfo && store.userInfo.permissions;
if (typeof value === 'string') {
const hasPermission = permissions.some((permission) => {
return all_permission === permission || value === permission;
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}

0
src/icons/index.js Normal file
View File

View File

@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 48 48" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 15a1 1 0 001 1h2a1 1 0 001-1V8h7a1 1 0 001-1V5a1 1 0 00-1-1H6a2 2 0 00-2 2v9zm4 18a1 1 0 00-1-1H5a1 1 0 00-1 1v9a2 2 0 002 2h9a1 1 0 001-1v-2a1 1 0 00-1-1H8v-7zm35-17a1 1 0 001-1V6a2 2 0 00-2-2h-9a1 1 0 00-1 1v2a1 1 0 001 1h7v7a1 1 0 001 1h2zm1 17a1 1 0 00-1-1h-2a1 1 0 00-1 1v7h-7a1 1 0 00-1 1v2a1 1 0 001 1h9a2 2 0 002-2v-9zM32.835 11h-6.108c-6.512 0-11.882 4.804-12.636 11h-1.992c-.382 0-.52.046-.66.134a.855.855 0 00-.325.378c-.074.162-.114.324-.114.77v1.436c0 .446.04.608.114.77.075.163.185.291.324.378.14.088.279.134.66.134h2.157c1.179 5.706 6.315 10 12.472 10h6.108c.405 0 .552-.041.7-.12a.819.819 0 00.344-.337c.079-.145.121-.29.121-.688V31h.901c.382 0 .52-.046.66-.134a.855.855 0 00.325-.378c.074-.162.114-.324.114-.77v-1.436c0-.446-.04-.608-.114-.77a.855.855 0 00-.325-.378c-.14-.088-.278-.134-.66-.134H34v-7h.901c.382 0 .52-.046.66-.134a.855.855 0 00.325-.378c.074-.162.114-.324.114-.77v-1.436c0-.446-.04-.608-.114-.77a.855.855 0 00-.325-.378c-.14-.088-.278-.134-.66-.134H34v-3.855c0-.398-.042-.543-.121-.688a.819.819 0 00-.344-.338c-.148-.078-.295-.119-.7-.119zm-2.744 3.571h-3.637c-5.02 0-9.09 3.998-9.09 8.929s4.07 8.929 9.09 8.929h3.637V14.57z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,12 @@
<template>
<div class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<component :is="Component" :key="route" />
</transition>
</router-view>
</div>
</template>
<style lang="scss">
</style>

View File

@ -0,0 +1,76 @@
<template>
<a-dropdown position="bl" :style="{ top: '52px' }">
<div class="avatar-container">
<a-avatar :size="32" :style="{ backgroundColor: '#3370ff' }">
<img alt="avatar" :src="userInfo.avatar" />
</a-avatar>
<div class="user-info">
<div class="user-info-name">{{ userInfo.name }}</div>
<!-- <div class="user-info-desc">{{ userInfo.introduction }}</div> -->
</div>
</div>
<template #content>
<a-doption @click="$router.push('/profile')">
<template #icon>
<icon-settings />
</template>
<template #default>用户设置</template>
</a-doption>
<a-doption @click="handleLogout()">
<template #icon>
<icon-export />
</template>
<template #default>退出登陆</template>
</a-doption>
</template>
</a-dropdown>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
import { storeToRefs } from 'pinia';
import {
IconSettings,
IconExport,
} from '@arco-design/web-vue/es/icon';
import { useUserStore } from '@/store/userInfo';
import { clearLocalStorage } from '@/utils/storage';
const store = useUserStore();
const { userInfo } = storeToRefs(store);
const { proxy } = getCurrentInstance();
const handleLogout = () => {
proxy.$modal.warning({
title: '提示',
content: '确定注销并退出登陆系统吗?',
hideCancel: false,
onOk: () => {
window.sessionStorage.removeItem('token');
clearLocalStorage();
proxy.$router.push('/login');
},
});
};
</script>
<style lang="scss" scoped>
.avatar-container {
display: flex;
cursor: pointer;
.user-info {
margin-left: 10px;
&-name {
color: var(--color-text-1);
font-weight: 700;
padding-top: 8px;
}
&-desc {
color: var(--color-text-1);
font-size: 12px;
line-height: 1.4;
}
}
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="sider-logo">
<img :src="store.sysConfig.sys_app_logo" />
<span class="sider-title" v-if="!props.collapsed">{{store.sysConfig.sys_app_name}}</span>
</div>
<a-menu
class="menu"
@menu-item-click="handleMenuClick"
:default-open-keys="['/admin']"
:default-selected-keys="defaultSelectKeys"
:auto-open-selected="true"
>
<sub-menu :menu-list="permissionStore.menuList" />
</a-menu>
</template>
<script setup>
import { ref, onBeforeMount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/store/userInfo';
import { usePermissionStore } from '@/store/permission';
import SubMenu from './SubMenu.vue';
const store = useUserStore();
const permissionStore = usePermissionStore();
const props = defineProps({
collapsed: {
type: Boolean,
default: false,
},
});
const route = useRoute();
const router = useRouter();
//
const defaultSelectKeys = ref([]);
//
const keepDefaultSelect = () => {
defaultSelectKeys.value = [];
defaultSelectKeys.value.push(route.fullPath);
};
const handleMenuClick = (key) => {
router.push(key);
};
onBeforeMount(() => {
keepDefaultSelect();
});
</script>
<style lang="scss" scoped>
.sider-logo {
margin: 10px 0;
display: flex;
justify-content: center;
padding-bottom: 8px;
border-bottom: 1px solid var(--color-text-4);
& img {
height: 32px;
}
}
.sider-title {
margin-left: 10px;
display: flex;
align-items: center;
font-size: 16px;
color: var(--color-text-4);
}
.left-side {
height: 50px;
width: 50px;
font-size: 18px;
line-height: 50px;
text-align: center;
transition: all 0.3s ease-in-out;
cursor: pointer;
&:hover {
background-color: #e5e5e5;
}
}
</style>
<style lang="scss">
.arco-menu-indent {
width: 30px;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<template v-for="menu in props.menuList" :key="menu.menuId">
<a-sub-menu
v-if="menu.children && menu.menuType == 'M' && menu.visible == 0"
:key="menu.path"
>
<template #icon>
<component :is="menu.icon" />
</template>
<template #title>{{ menu.title }}</template>
<sub-menu :menuList="menu.children" />
</a-sub-menu>
<a-menu-item
v-if="menu.menuType == 'C' && menu.visible && isRouteParams(menu.path)"
:key="menu.path"
>{{ menu.title }}</a-menu-item>
</template>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
menuList: {
type: Array,
default: () => [],
},
});
//
const isRouteParams = computed(() => {
return (path) => {
if (path.indexOf(':') == -1) return true;
return false;
}
})
</script>

View File

@ -0,0 +1,93 @@
<template>
<div class="navbar">
<div class="left-side" @click="onCollapse">
<icon-menu-fold v-if="!props.collapsed" />
<icon-menu-unfold v-else />
</div>
<ul class="right-side">
<li>
<a-button shape="circle" @click="handleDarkTheme">
<template #icon>
<component
:is="!darkTheme ? IconSunFill : IconMoonFill"
></component>
</template>
</a-button>
</li>
<li>
<Avatar />
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Avatar from './Avatar/index.vue';
import {
IconMenuFold,
IconMenuUnfold,
IconSunFill,
IconMoonFill,
} from '@arco-design/web-vue/es/icon';
const darkTheme = ref(false);
const props = defineProps({
collapsed: Boolean,
});
const emit = defineEmits(['onCollapse']);
const onCollapse = () => {
emit('onCollapse');
};
//
const handleDarkTheme = () => {
const theme = document.body.getAttribute('arco-theme');
if (!theme) {
document.body.setAttribute('arco-theme', 'dark');
darkTheme.value = true;
} else {
document.body.removeAttribute('arco-theme');
darkTheme.value = false;
}
};
</script>
<style lang="scss" scoped>
.navbar {
display: flex;
justify-content: space-between;
height: 50px;
border-bottom: 1px solid #e5e6eb;
box-shadow: 0 2px 5px 0 rgba(0,0,0, .1);
.left-side {
height: 50px;
width: 50px;
font-size: 18px;
line-height: 50px;
text-align: center;
transition: all 0.3s ease-in-out;
cursor: pointer;
&:hover {
background-color: #e5e5e5;
}
}
.right-side {
list-style: none;
display: flex;
padding-right: 30px;
& > li {
display: flex;
align-items: center;
padding: 10px;
}
}
}
</style>

View File

@ -0,0 +1,2 @@
export { default as AppMain } from './AppMain.vue';
export { default as Navbar } from './Navbar.vue';

37
src/layout/index.vue Normal file
View File

@ -0,0 +1,37 @@
<template>
<a-layout :style="{ height: '100vh' }">
<a-layout-sider theme="dark" breakpoint="lg" :width="220" collapsible :collapsed="collapsed" @collapse="onCollapse">
<Menu :collapsed="collapsed" />
</a-layout-sider>
<a-layout>
<a-layout-header>
<Navbar :collapsed="collapsed" @on-collapse="onCollapse" />
</a-layout-header>
<a-layout-content class="layout-content">
<AppMain />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup>
import { ref } from 'vue';
import { AppMain, Navbar } from './components';
import Menu from './components/Menu/Menu.vue';
const collapsed = ref(false);
const onCollapse = () => {
collapsed.value = !collapsed.value;
};
</script>
<style lang="scss">
@import '../style/index.scss';
@import '../style/transition.scss';
@import '../style/dark-theme.scss';
.layout-content {
padding: 16px;
background-color: #f2f3f5;
}
</style>

39
src/main.js Normal file
View File

@ -0,0 +1,39 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import ArcoVue from '@arco-design/web-vue';
import { Message, Modal, Notification } from '@arco-design/web-vue';
import '@arco-design/web-vue/dist/arco.css';
import router from './router/';
import { parseTime } from '@/utils/parseTime';
// Directive
import permission from '@/directive/permission/permission';
// 引入 Arco 图标库
import * as ArcoIconModules from '@arco-design/web-vue/es/icon';
console.log(import.meta.env);
// Initialize the Pinia instance
const pinia = createPinia();
const app = createApp(App);
app.directive('has', permission.checkPermission);
// 挂载全局变量
app.config.globalProperties.message = Message;
app.config.globalProperties.modal = Modal;
app.config.globalProperties.notification = Notification;
app.config.globalProperties.parseTime = parseTime;
// 挂载全局图标
for(const name in ArcoIconModules){
app.component(name,ArcoIconModules[name])
}
app.use(ArcoVue);
app.use(router);
app.use(pinia);
app.mount('#app');

115
src/router/index.js Normal file
View File

@ -0,0 +1,115 @@
import { createWebHashHistory, createRouter, createWebHistory } from 'vue-router';
import Layout from '../layout/index.vue';
import { useUserStore } from '../store/userInfo';
import { usePermissionStore } from '../store/permission';
import Watermark from '@/utils/watermark.js';
const routes = [
{
path: '/',
name: '/',
redirect: 'admin',
component: Layout,
children: [
{
path: '/profile',
name: 'profile',
component: () => import('../views/profile/index.vue'),
meta: {
title: '个人设置',
},
},
{
path: '/403',
name: '403',
component: () => import('../views/error-page/403.vue'),
meta: {
title: '找不到页面',
},
},
{
hide: true,
path: '/:catchAll(.*)',
component: () => import('../views/error-page/404.vue'),
meta: {
title: '找不到页面',
},
},
{
path: '/500',
name: '500',
component: () => import('../views/error-page/500.vue'),
meta: {
title: '找不到页面',
},
},
]
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/index.vue'),
},
];
const router = createRouter({
// createWebHashHistory URL 带井号
// createWebHistory URL 去井号
history: createWebHistory(),
routes: routes,
});
// beforeEach router
router.beforeEach(async (to, from, next) => {
const store = useUserStore();
const permissionStore = usePermissionStore();
// 获取系统配置信息
await store.getSysConfig();
// 判断用户Token是否获取
if (to.name !== 'login' && !store.token) {
next({ name: 'login' });
} else {
// 判断判断权限有无获取
if (store.token && store.roles.length === 0) {
store.getUserInfo();
await permissionStore.getMenuRole();
permissionStore.addRouters.forEach((route) => {
router.addRoute('/', route);
});
// next(to.fullPath);
// 如果 addRoute 并未完成,路由守卫会一层一层的执行执行,直到 addRoute 完成,找到对应的路由
next({ ...to, replace: true })
} else {
next();
}
}
});
// afterEach Router
router.afterEach((to) => {
const store = useUserStore();
// 修改网页标题
if (to.name !== 'login') {
document.title = `${to.meta.title} - ${store.sysConfig.sys_app_name}`;
} else {
document.title = store.sysConfig.sys_app_name;
}
// Vincent 2023004 修复加载水印的bug
if (store.userInfo != undefined){
if ( store.userInfo.name != undefined ) {
Watermark.set(store.userInfo.name)
} else {
Watermark.out() // 清除水印
}
} else{
Watermark.out() // 清除水印
}
});
export default router;

60
src/store/permission.js Normal file
View File

@ -0,0 +1,60 @@
import { defineStore } from 'pinia';
import { getUserMenuRole } from '@/api/admin/login';
const modules = import.meta.glob('../views/**/*.vue');
export const usePermissionStore = defineStore('permisson', {
state: () => {
return {
addRouters: [],
menuList: [],
};
},
getters: {
getRoutes: (state) => state.addRouters,
},
actions: {
setMenuList(menus) {
this.menuList = menus;
},
GenerateRoutes(routeList) {
const routes = [];
routeList.forEach((item) => {
const route = {};
// if (item.visible == 0) {
if (item.menuType === 'M' || item.menuType === 'C') {
route.path = item.path;
route.name = item.menuName;
if (item.menuType === 'M') {
route.component = modules[`../views/index.vue`];
} else if (item.menuType === 'C') {
route.component = modules[`../views${item.component}.vue`] || modules['../views/error-page/888.vue'];
}
route.meta = {
title: item.title,
permission: item.permission,
};
}
if (item.children) {
route.children = this.GenerateRoutes(item.children);
}
routes.push(route);
// }
});
return routes;
},
async getMenuRole() {
const res = await getUserMenuRole();
console.log(res.data);
this.setMenuList(res.data);
this.addRouters = await this.GenerateRoutes(res.data);
},
ClearMenuList() {
this.menuList = [];
}
},
});

48
src/store/userInfo.js Normal file
View File

@ -0,0 +1,48 @@
import { defineStore } from 'pinia';
import { setLocalStorage, getLocalStorage } from '@/utils/storage';
import { getInfo } from '@/api/admin/sys-user';
import { getAppConfig } from '@/api/admin/login';
export const useUserStore = defineStore('user', {
state: () => {
return {
token: window.sessionStorage.getItem('token') || '',
uid: window.sessionStorage.getItem('uid') || '',
sysConfig: getLocalStorage('sysConfig'),
userInfo: '',
}
},
getters: {
roles: (state) => state.userInfo.roles || [],
},
actions: {
setToken(token) {
this.token = token;
window.sessionStorage.setItem('token', token);
},
async getUserInfo() {
try {
const res = await getInfo();
// window.sessionStorage.setItem('uid', res.data.userId);
window.localStorage.setItem('uid', res.data.userId);
this.userInfo = res.data;
} catch (err) {
console.error(err);
}
},
async getSysConfig() {
try {
const res = await getAppConfig();
setLocalStorage('sysConfig', res.data);
this.sysConfig = res.data;
} catch (err) {
console.error(err);
}
}
},
userLogout() {
this.token = null;
this.userInfo = null;
}
})

34
src/style/dark-theme.scss Normal file
View File

@ -0,0 +1,34 @@
// Dark Theme
$dark-bg-1: #17171A;
$color-bg-2: #393A3C;
$color-bg-3: #2a2a2b;
$color-bg-4: #313132;
$color-bg-5: #373739;
$--color-text-1: rgba(255, 255, 255, 0.9);
$--color-text-2: rgba(255, 255, 255, 0.7);
$--color-text-3: rgba(255, 255, 255, 0.5);
$--color-text-4: rgba(255, 255, 255, 0.3);
body[arco-theme='dark'] {
.navbar {
background-color: $dark-bg-1;
}
.arco-layout-content {
background-color: $color-bg-2
}
.app-container {
background-color: $color-bg-3;
}
.navbar {
border-bottom: 1px solid $--color-text-4;
.left-side {
// background-color: $color-bg-2;
color: $--color-text-1;
&:hover {
background-color: $color-bg-5;
}
}
}
}

88
src/style/index.scss Normal file

File diff suppressed because one or more lines are too long

28
src/style/transition.scss Normal file
View File

@ -0,0 +1,28 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}

3
src/style/variables.scss Normal file
View File

@ -0,0 +1,3 @@
// base color
$primary-font-color: #1d2129;
$secondary-font-color: #86909c;

41
src/utils/parseTime.js Normal file
View File

@ -0,0 +1,41 @@
// 日期格式化
export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) {
return null
}
if (time.indexOf('01-01-01') > -1) {
return '-'
}
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}

58
src/utils/request.js Normal file
View File

@ -0,0 +1,58 @@
import axios from 'axios';
import { Message } from '@arco-design/web-vue';
import { useUserStore } from '../store/userInfo'
// create an axios instance
console.log("________"+import.meta.env.VITE_BASE_URL)
const service = axios.create({
baseURL:import.meta.env.VITE_BASE_URL,
timeout: 8000,
});
// request interceptor
service.interceptors.request.use(
(config) => {
// Store 必须在拦截器内部导入,在外部导入会显示 Pinia 未初始化
const store = useUserStore();
// 设置请求头部 Authorization
if (store.token) {
config.headers['Authorization'] = 'Bearer ' + store.token;
config.headers['Content-Type'] = 'application/json'
}
return config;
},
(error) => {
console.error(error);
return Promise.reject(error);
}
);
// response interceptor
service.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
const store = useUserStore();
const { code, msg } = error.response.data;
// 如果过期则退出登录
if (code === 401) {
Message.error({
content: 'Token 已过期, 请重新登陆',
duration: 3000
});
// 重定向路由到登陆页面
store.userLogout();
// Akiraka 20230410 重定向到登录页面
return router.push('/login');
} else {
Message.error({
content: error.message,
duration: 3000
})
return Promise.reject(msg);
}
}
);
export default service;

41
src/utils/storage.js Normal file
View File

@ -0,0 +1,41 @@
// LocalStorage
/**
* 存储LocalStorage
* @param {string, Object} name
* @param {*} value
*/
export const setLocalStorage = (name, value) => {
if (!name) throw new Error('name must be specified');
if (typeof value !== 'string') {
value = JSON.stringify(value);
}
window.localStorage.setItem(name, value);
}
/**
* 获取LocalStorage
* @param {string, Object} name
* @returns
*/
export const getLocalStorage = (name) => {
if(!name) throw new Error('name must be specified');
const value = window.localStorage.getItem(name);
return JSON.parse(value);
}
/**
* 移除指定name
* @param {*} name
*/
export const removeLocalStorage = (name) => {
if (!name) throw new Error('name must be specified');
window.localStorage.removeItem(name);
}
/**
* 清空所有LocalStorage
*/
export const clearLocalStorage = () => {
window.localStorage.clear();
}

60
src/utils/watermark.js Normal file
View File

@ -0,0 +1,60 @@
let watermark = {}
let setWatermark = (str) => {
let id = '1.23452384164.123412415'
if (document.getElementById(id) !== null) {
document.body.removeChild(document.getElementById(id))
}
let can = document.createElement('canvas')
can.width = 200
can.height = 120
let cans = can.getContext('2d')
cans.rotate(-15 * Math.PI / 150)
cans.font = '18px Vedana'
cans.fillStyle = 'rgba(200, 200, 200, 0.20)'
cans.textAlign = 'left'
cans.textBaseline = 'Middle'
cans.fillText(str, can.width / 8, can.height / 2)
let div = document.createElement('div')
div.id = id
div.style.pointerEvents = 'none'
div.style.top = '35px'
div.style.left = '200px'
div.style.position = 'fixed'
div.style.zIndex = '100000'
div.style.width = document.documentElement.clientWidth + 'px'
div.style.height = document.documentElement.clientHeight + 'px'
div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat'
document.body.appendChild(div)
return id
}
// 该方法只允许调用一次
watermark.set = (str) => {
let id = setWatermark(str)
setInterval(() => {
if (document.getElementById(id) === null) {
id = setWatermark(str)
}
}, 500)
window.onresize = () => {
setWatermark(str)
}
}
const outWatermark = (id) => {
if (document.getElementById(id) !== null) {
const div = document.getElementById(id)
div.style.display = 'none'
}
}
watermark.out = () => {
const str = '1.23452384164.123412415'
outWatermark(str)
}
export default watermark

View File

@ -0,0 +1,253 @@
<template>
<div class="app-container">
<a-form :model="queryForm" layout="inline">
<a-form-item field="dictType" label="字典名称">
<a-select v-model="queryForm.dictType" :options="dictTypeOptions" :field-names="{ value: 'dictType', label: 'dictName' }" />
</a-form-item>
<a-form-item field="dictTag" label="字典标签">
<a-input v-model="queryForm.dictTag" placeholder="请输入字典标签" />
</a-form-item>
<a-form-item field="status" label="字典状态">
<a-select v-model="queryForm.status" placeholder="请选择字典状态">
<a-option :value="2">正常</a-option>
<a-option :value="1">关闭</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button v-has="'admin:sysDictData:query'" type="primary">搜索</a-button>
<a-button>重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="table-action">
<a-button v-has="'admin:sysDictData:add'" type="primary" @click="handleAdd">新增</a-button>
</div>
<a-table
:data="tableData"
:columns="columns"
:pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }"
@page-change="handlePageChange"
>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #action="{ record }">
<a-button v-has="'admin:sysDictData:edit'" type="text" @click="handleEdit(record)">修改</a-button>
<a-button v-has="'admin:sysDictData:remove'" type="text" @click="() => { deleteVisible = true; deleteData = [record.dictCode]; }">删除</a-button>
</template>
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-else-if="record.status == 1" color="red">停用</a-tag>
</template>
</a-table>
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
title-align="start"
@close="$refs.modalFormRef.resetFields()"
@before-ok="handleBeforeOk"
@ok="handleSubmit"
>
<a-form :model="modalForm" :rules="rules" ref="modalFormRef">
<a-form-item field="dictType" label="字典类型">
<a-input v-model="modalForm.dictType" disabled></a-input>
</a-form-item>
<a-form-item field="dictLabel" label="数据标签">
<a-input
v-model="modalForm.dictLabel"
placeholder="请输入数据标签"
></a-input>
</a-form-item>
<a-form-item field="dictValue" label="数据键值">
<a-input
v-model="modalForm.dictValue"
placeholder="请输入数据键值"
></a-input>
</a-form-item>
<a-form-item field="dictSort" label="显示排序">
<a-input-number
v-model="modalForm.dictSort"
:default-value="0"
mode="button"
></a-input-number>
</a-form-item>
<a-form-item field="status" label="状态">
<a-radio-group v-model="modalForm.status">
<a-radio :value="2">正常</a-radio>
<a-radio :value="1">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="modalForm.remark"></a-textarea>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="deleteDictData"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, onBeforeMount, watch } from 'vue';
import { getDictData, addDictData, updateDictData, deleteDictData } from '@/api/admin/sys-dict-data';
import { getDictType } from '@/api/admin/sys-dict';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getDictDataInfo({...proxy.$route.params, ...pager});
}
})
const { proxy } = getCurrentInstance();
//
const currentPage = ref(1);
//
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
// Rules
const rules = {
dictLabel: [{ required: true, message: '请输入数据标签' }],
dictValue: [{ required: true, message: '请输入数据键值' }],
dictSort: [{ required: true, message: '请选择排序' }],
};
// Modal
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
// DictType Options
const dictTypeOptions = ref([]);
//
const queryForm = reactive({});
// Modal
const modalForm = reactive({
status: 2
});
//
const tableData = ref([]);
const columns = [
{ title: '字典编码', dataIndex: 'dictCode' },
{ title: '字典标签', dataIndex: 'dictLabel' },
{ title: '字典键值', dataIndex: 'dictValue' },
{ title: '字典排序', dataIndex: 'dictSort' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: '备注', dataIndex: 'remark' },
{ title: '创建时间', dataIndex: 'createdAt', slotName: 'createdAt' },
{ title: '操作', dataIndex: 'action', slotName: 'action' },
];
//
const handleAdd = () => {
modalVisible.value = true;
modalTitle.value = '新增字典数据';
Object.assign(modalForm, proxy.$route.params);
};
//
const handleEdit = (record) => {
modalVisible.value = true;
modalTitle.value = '修改字典数据';
Object.assign(modalForm, proxy.$route.params, record);
console.log(modalForm)
};
//
const handleBeforeOk = (done) => {
proxy.$refs.modalFormRef.validate((err) => {
if (err) {
proxy.$message.error('表单校验失败');
done(false);
} else {
done();
}
})
}
//
const handleSubmit = async () => {
if (modalForm.dictCode) {
const { code, msg } = await updateDictData(modalForm, modalForm.dictCode);
if ( code == 200 ) {
proxy.$notification.success('修改成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await addDictData(modalForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
}
getDictDataInfo({...proxy.$route.params, ...pager});
}
//
const handlePageChange = (pagerIndex) => {
currentPage.value = pagerIndex;
pager.pageIndex = pagerIndex;
getDictDataInfo({...proxy.$route.params, ...pager});
}
//
const getDictDataInfo = async (params = {}) => {
const { data, code, msg } = await getDictData(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
//
const getDictTypeInfo = async () => {
const res = await getDictType({ pageSize: 1000 });
dictTypeOptions.value = res.data.list;
Object.assign(queryForm, proxy.$route.params);
};
onBeforeMount(() => {
getDictDataInfo(proxy.$route.params);
getDictTypeInfo();
});
</script>
<style lang="scss" scoped>
.table-action {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,268 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="dictName" label="字典名称">
<a-input v-model="queryForm.dictName" placeholder="请输入字典名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="dictType" label="字典类型">
<a-input v-model="queryForm.dictType" placeholder="请输入字典类型" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="status" label="状态">
<a-select v-model="queryForm.status" placeholder="请选择字典状态">
<a-option :value="2">正常</a-option>
<a-option :value="1">停用</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button v-has="'admin:sysDictType:query'" type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button v-has="'admin:sysDictType:query'" @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="action">
<a-space>
<a-button v-has="'admin:sysDictType:add'" type="primary" @click="handleAdd"><icon-plus /> 新增</a-button>
<a-button v-has="'system:sysdicttype:remove'" type="primary" status="danger" @click="() => { deleteVisible = true; }"><icon-delete /> 批量删除</a-button>
<a-button type="primary" status="warning" disabled><icon-download /> 导出</a-button>
</a-space>
</div>
<!-- table -->
<a-table
:columns="tableColumns"
:data="tableData"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }"
row-key="id"
@selection-change="(selection) => {deleteData = selection;}"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #dictType="{ record }">
<router-link :to="`/admin/dict/data/${record.dictType}`">{{ record.dictType }}</router-link>
</template>
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-else color="red">停用</a-tag>
</template>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #action="{ record }">
<a-button v-has="'admin:sysDictType:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改</a-button>
<a-button v-has="'admin:sysDictType:remove'" type="text" @click="() => { deleteVisible = true; deleteData = [record.id]; }"><icon-edit /> 删除</a-button>
</template>
</a-table>
<!-- Modal弹框 -->
<a-modal
v-model:visible="modalVisible"
title-align="start"
:title="modalTitle"
@before-ok="handleSubmit"
@close="$refs.modalFormRef.resetFields()"
>
<a-form :model="modalForm" ref="modalFormRef">
<a-form-item field="dictName" label="字典名称">
<a-input
v-model="modalForm.dictName"
placeholder="请输入字典名称"
:disabled="modalForm.id ? true : false"
></a-input>
</a-form-item>
<a-form-item field="dictType" label="字典类型">
<a-input
v-model="modalForm.dictType"
placeholder="请输入字典类型"
:disabled="modalForm.id ? true : false"
></a-input>
</a-form-item>
<a-form-item field="status" label="状态">
<a-radio-group v-model="modalForm.status">
<a-radio :value="2">正常</a-radio>
<a-radio :value="1">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea
v-model="modalForm.remark"
placeholder="请输入内容备注"
></a-textarea>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removeDictType"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, nextTick, onMounted, watch } from 'vue';
import { getDictType, addDictType, removeDictType, updateDictType } from '@/api/admin/sys-dict';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getSysDictTypeInfo({ ...pager, ...queryForm });
}
})
const { proxy } = getCurrentInstance();
const currentPage = ref(1);
// pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
// form
const modalForm = reactive({
status: 2,
});
// Table
const tableData = ref([]);
const tableColumns = [
{ title: '编号', dataIndex: 'id' },
{ title: '字典名称', dataIndex: 'dictName' },
{ title: '字典类型', dataIndex: 'dictType', slotName: 'dictType' },
{ title: '字典状态', dataIndex: 'status', slotName: 'status' },
{ title: '备注', dataIndex: 'remark' },
{ title: '创建时间', dataIndex: 'createdAt', slotName: 'createdAt' },
{ title: '操作', slotName: 'action' },
];
// Modal
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
const { queryForm, handleQuery, handleResetQuery } = useQueryDict();
//
function useQueryDict() {
const queryForm = reactive({});
const handleQuery = () => {
getSysDictTypeInfo(queryForm);
};
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
currentPage.value = 1;
getSysDictTypeInfo(queryForm);
};
return {
queryForm,
handleQuery,
handleResetQuery,
};
}
//
const handlePageChange = (page) => {
pager.pageIndex = page;
currentPage.value = page;
getSysDictTypeInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getSysDictTypeInfo({ ...pager, ...queryForm });
};
//
const handleAdd = () => {
modalVisible.value = true;
modalTitle.value = '新增字典';
};
//
const handleUpdate = async (record) => {
modalVisible.value = true;
modalTitle.value = '修改字典';
await nextTick();
Object.assign(modalForm, record);
};
// Submit
const handleSubmit = (done) => {
proxy.$refs.modalFormRef.validate(async (err) => {
if (!err) {
try {
if (Reflect.has(modalForm, 'id')) {
const { code, msg } = await updateDictType(modalForm, modalForm.id);
if (code == 200 ) {
proxy.$notification.success('更新成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await addDictType(modalForm);
currentPage.value = Math.ceil(++pager.count / pager.pageSize);
pager.pageIndex = currentPage.value;
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
}
modalVisible.value = false;
getSysDictTypeInfo(pager);
done();
} catch (e) {
done(false);
}
} else {
proxy.$message.error('表单校验失败!');
done(false);
}
});
};
//
const getSysDictTypeInfo = async (params = {}) => {
const { data, code, msg } = await getDictType(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
onMounted(() => {
getSysDictTypeInfo();
});
</script>
<style lang="scss">
.action {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="title" label="标题">
<a-input v-model="queryForm.title" placeholder="请输入标题" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="path" label="地址">
<a-input v-model="queryForm.path" placeholder="请输入地址" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="action" label="类型">
<a-select v-model="queryForm.action" placeholder="请选择类型">
<a-option>GET</a-option>
<a-option>POST</a-option>
<a-option>PUT</a-option>
<a-option>DELETE</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space size="medium">
<a-button v-has="'admin:sysApi:query'" type="primary" @click="handleQuery">
<template #icon>
<icon-search />
</template>
搜索
</a-button>
<a-button @click="handlResetQuery">
<template #icon>
<icon-loop />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<!-- Table -->
<a-table
:columns="columns"
:data="tableData"
:pagination="{
'show-total': true,
'show-jumper': true,
'show-page-size': true,
total: pager.count,
current: currentPage,
}"
row-key="id"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #reqType="{ record }">
<a-tag v-if="record.action.toLowerCase() === 'get'" color="cyan">{{record.action}}</a-tag>
<a-tag v-else-if="record.action.toLowerCase() === 'post'" color="gold" >{{ record.action }}</a-tag>
<a-tag v-else-if="record.action.toLowerCase() === 'put'" color="green" >{{ record.action }}</a-tag>
<a-tag v-else-if="record.action.toLowerCase() === 'delete'" color="pinkpurple" >{{ record.action }}</a-tag>
</template>
<template #createdAt="{ record }"> {{ parseTime(record.createdAt) }}</template>
<template #action="{ record }">
<a-button v-has="'admin:sysApi:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改 </a-button>
</template>
</a-table>
<!-- Drawer -->
<a-drawer
:visible="drawerVisible"
:width="450"
@before-ok="handleDrawerSubmit"
@cancel="handleDrawerCancel"
>
<template #title> 修改接口管理 </template>
<a-form :model="drawerForm" ref="drawerFormRef" :rules="rules">
<a-form-item field="handle" label="Handle">
<a-input v-model="drawerForm.handle" placeholder="请输入Handle" />
</a-form-item>
<a-form-item field="title" label="标题">
<a-input v-model="drawerForm.title" placeholder="请输入标题" />
</a-form-item>
<a-form-item field="type" label="类型">
<a-select
v-model="drawerForm.type"
placeholder="请选择类型"
allow-clear
>
<a-option>SYS</a-option>
<a-option>BUS</a-option>
</a-select>
</a-form-item>
<a-form-item field="action" label="方式">
<a-select
v-model="drawerForm.action"
placeholder="请选择请求方式"
allow-clear
>
<a-option>GET</a-option>
<a-option>POST</a-option>
<a-option>PUT</a-option>
<a-option>DELETE</a-option>
</a-select>
</a-form-item>
<a-form-item field="path" label="路径">
<a-input v-model="drawerForm.path" disabled />
</a-form-item>
</a-form>
</a-drawer>
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, onMounted, nextTick } from 'vue';
import { IconSearch, IconLoop } from '@arco-design/web-vue/es/icon';
import { getSysApi,addSysApi,updateSysApi } from '@/api/admin/sys-api';
const { proxy } = getCurrentInstance();
//
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
// From
const queryForm = reactive({});
const drawerForm = reactive({});
// From
const rules = {
title: [{ required: true, message: '请输入标题' }],
method: [{ required: true, message: '请选择方式' }],
};
//
const drawerVisible = ref(false);
// Table Columns
const columns = [
{ title: '标题', dataIndex: 'title' },
{ title: '请求类型', dataIndex: 'action', slotName: 'reqType' },
{ title: '请求信息', dataIndex: 'path' },
{ title: '创建时间', dataIndex: 'createdAt', slotName: 'createdAt' },
{ title: '操作', slotName: 'action' },
];
// Table Data
const tableData = ref([]);
// Drawer
// Modal done()
const handleDrawerSubmit = (done) => {
proxy.$refs.drawerFormRef.validate(async (valid) => {
if (!valid) {
let res;
if (!drawerForm.id) {
const { code, msg } = await addSysApi(drawerForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await updateSysApi(drawerForm, drawerForm.id);
if (code == 200 ) {
proxy.$notification.success('修改成功');
} else {
proxy.$notification.error(msg);
}
}
drawerVisible.value = false;
done();
getSysApiInfo(pager);
} else {
proxy.$message.error('表单校验失败');
done(false);
}
});
};
// Drawer
const handleDrawerCancel = () => {
drawerVisible.value = false;
};
//
const handleQuery = async () => {
getSysApiInfo(queryForm);
currentPage.value = 1;
};
//
const handlResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
handlePageChange(1);
};
//
const handleUpdate = async (record) => {
drawerVisible.value = true;
// updateSysApi(record);
await nextTick();
Object.assign(drawerForm, record);
};
/**
* 分页改变
* @param {Number} [page]
*/
const handlePageChange = (page) => {
pager.pageIndex = page;
//
currentPage.value = page;
getSysApiInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getSysApiInfo({ ...pager, ...queryForm });
};
//
const getSysApiInfo = async (params = {}) => {
const { data, code, msg } = await getSysApi(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
onMounted(() => {
getSysApiInfo(pager);
});
</script>
<style lang="scss">
.pagination {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
// Table
.arco-table-th:last-child > span {
margin-left: 15px;
}
</style>

View File

@ -0,0 +1,328 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="configName" label="参数名称">
<a-input
v-model="queryForm.configName"
placeholder="请输入参数名称"
></a-input>
</a-form-item>
<a-form-item field="configKey" label="参数键名">
<a-input
v-model="queryForm.configKey"
placeholder="请输入参数键名"
></a-input>
</a-form-item>
<a-form-item field="configType" label="系统内置">
<a-select v-model="queryForm.configType" placeholder="选择系统内置">
<a-option value="Y"></a-option>
<a-option value="N"></a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery()"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery()"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 分割线 -->
<a-divider />
<div class="action">
<a-space>
<a-button v-has="'admin:sysConfig:add'" type="primary" @click="handleAdd"><icon-plus /> 新增</a-button>
<a-button v-has="'admin:sysConfig:remove'" type="primary" status="danger" disabled><icon-delete /> 删除</a-button>
<a-button type="primary" status="warning" disabled><icon-download /> 导出</a-button>
</a-space>
</div>
<a-table
:data="tableData"
:columns="columns"
:pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #configType="{ record }">
<a-tag v-if="record.configType === 'Y'" color="green"></a-tag>
<a-tag v-else-if="record.configType === 'N'" color="red"></a-tag>
</template>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #action="{ record }">
<a-button v-has="'admin:sysConfig:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改</a-button>
<a-button v-has="'admin:sysConfig:edit'" type="text" @click="() => { deleteVisible = true; deleteData = [record.id]; }"><icon-delete /> 删除</a-button>
</template>
</a-table>
<a-modal
v-model:visible="visible"
:title="title"
title-align="start"
@before-ok="handleBeforeOk"
@close="handleResetModalForm"
>
<a-form :model="modalForm" :rules="rules" ref="modalFormRef">
<a-form-item field="configName" label="参数名称">
<a-input
v-model="modalForm.configName"
placeholder="请输入参数名称"
/>
</a-form-item>
<a-form-item field="configKey" label="参数键名">
<a-input v-model="modalForm.configKey" placeholder="请输入参数键名" />
</a-form-item>
<a-form-item field="configValue" label="参数键值">
<a-input
v-model="modalForm.configValue"
placeholder="请输入参数键值"
/>
</a-form-item>
<a-form-item field="configType" label="系统内置">
<a-radio-group v-model="modalForm.configType">
<a-radio value="Y"></a-radio>
<a-radio value="N"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="isFrontend" label="前台显示">
<a-radio-group v-model="modalForm.isFrontend">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea
v-model="modalForm.remark"
placeholder="请输入备注内容"
></a-textarea>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removeSysConfig"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { onMounted, reactive, ref, getCurrentInstance, watch } from 'vue';
import { getSysConfig, addSysConfig, removeSysConfig, updateSysConfig } from '@/api/admin/sys-config';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getSysConfigInfo(pager);
}
})
const { proxy } = getCurrentInstance();
const currentPage = ref(1);
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
const { queryForm, handleQuery, handleResetQuery } = useQueryForm();
const { tableData, columns, handlePageChange, handlePageSizeChange } =
useTableData();
const {
rules,
title,
visible,
modalForm,
handleAdd,
handleUpdate,
handleBeforeOk,
} = useModal();
function useQueryForm() {
const queryForm = reactive({});
const handleQuery = () => {
getSysConfigInfo(queryForm);
};
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
handlePageChange(1);
};
return {
queryForm,
handleQuery,
handleResetQuery,
};
}
function useTableData() {
const tableData = ref([]);
const columns = [
{ title: '编码', dataIndex: 'id', width: 70 },
{ title: '名称', dataIndex: 'configName' },
{ title: '键名', dataIndex: 'configKey' },
{
title: '内置',
dataIndex: 'configType',
slotName: 'configType',
width: 100,
},
{
title: '备注',
dataIndex: 'remark',
width: 350,
ellipsis: true,
tooltip: true,
},
{ title: '创建时间', dataIndex: 'createdAt', slotName: 'createdAt' },
{ title: '操作', dataIndex: 'action', slotName: 'action' },
];
//
const handlePageChange = (page) => {
pager.pageIndex = page;
getSysConfigInfo(pager);
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getSysConfigInfo(pager);
};
return {
columns,
tableData,
handlePageChange,
handlePageSizeChange,
};
}
function useModal() {
const visible = ref(false);
const title = ref('默认标题');
const modalForm = reactive({
configType: 'Y',
isFrontend: 1,
});
const rules = {
configName: [{ required: true, message: '请输入参数名称' }],
configKey: [{ required: true, message: '请输入参数键名' }],
configValue: [{ required: true, message: '请输入参数键值' }],
};
const handleAdd = () => {
visible.value = true;
title.value = '新增参数';
};
const handleUpdate = async (record) => {
visible.value = true;
title.value = '修改参数';
Object.assign(modalForm, record);
};
const handleBeforeOk = (done) => {
proxy.$refs.modalFormRef.validate(async (err) => {
if (!err) {
try {
const msg = await handleSubmit(modalForm);
proxy.$message.success(msg);
done();
getSysConfigInfo();
} catch (e) {
proxy.$message.error(e);
done(false);
}
} else {
proxy.message.error('表单校验失败');
done(false);
}
});
};
return {
rules,
title,
visible,
modalForm,
handleAdd,
handleUpdate,
handleBeforeOk,
};
}
//
const getSysConfigInfo = async (params = {}) => {
const { data, code, msg } = await getSysConfig(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
//
const handleSubmit = (data) => {
return new Promise(async (resolve, reject) => {
try {
let res;
if (!data.id) {
const { code, msg } = await addSysConfig(data);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await updateSysConfig(data, data.id);
if (code == 200 ) {
proxy.$notification.success('修改成功');
} else {
proxy.$notification.error(msg);
}
}
} catch (err) {
reject(err);
}
});
};
//
const handleResetModalForm = () => {
proxy.$refs.modalFormRef.resetFields();
modalForm.id = null;
}
onMounted(() => {
getSysConfigInfo();
});
</script>
<style lang="scss">
.action {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="deptName" label="部门名称">
<a-input v-model="queryForm.deptName" placeholder="请输入部门名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="status" label="部门状态">
<a-select v-model="queryForm.status" placeholder="请选择部门状态">
<a-option :value="2">正常</a-option>
<a-option :value="1">停用</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button v-has="'admin:sysDept:query'" type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="action">
<a-button v-has="'admin:sysDept:add'" type="primary" @click="handleAdd()"><icon-plus /> 新增</a-button>
</div>
<!-- 异步数据需要defualt-expanded-keys 传入所有行Key才能默认展开 -->
<a-table
:columns="columns"
:data="tableData"
:pagination="false"
:default-expanded-keys="[1]"
row-key="deptId"
>
<template #status="{ record }">
<a-tag color="green" v-if="record.status === 2">正常</a-tag>
<a-tag color="red" v-else> 停用 </a-tag>
</template>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #action="{ record }">
<a-button v-has="'admin:sysDept:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改</a-button>
<a-button v-has="'admin:sysDept:add'" type="text" @click="handleAdd(record)"><icon-plus /> 新增</a-button>
<a-button v-has="'admin:sysDept:remove'" type="text" @click="() => { deleteVisible = true; deleteData = [record.deptId]; }"><icon-delete /> 删除</a-button>
</template>
</a-table>
<!-- Modal -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
title-align="start"
@before-ok="handleBeforeOk"
@close="() => $refs.modalFormRef.resetFields()"
>
<a-form :model="modalForm" :rules="rules" ref="modalFormRef">
<a-form-item field="parentId" label="上级部门">
<a-tree-select
v-model="modalForm.parentId"
:data="tableData"
:field-names="{ key: 'deptId', title: 'deptName' }"
placeholder="请选择上级部门"
allow-clear
/>
</a-form-item>
<a-form-item field="deptName" label="部门名称">
<a-input v-model="modalForm.deptName" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item field="leader" label="负责人">
<a-input v-model="modalForm.leader" placeholder="请输入负责人" />
</a-form-item>
<a-form-item field="phone" label="联系电话">
<a-input v-model="modalForm.phone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item field="email" label="邮箱">
<a-input v-model="modalForm.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item field="status" label="部门状态">
<a-radio-group v-model="modalForm.status">
<a-radio :value="2">正常</a-radio>
<a-radio :value="1">停用</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removeDept"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, onMounted, getCurrentInstance, nextTick, watch } from 'vue';
import { getDept, addDept, removeDept, updateDept } from '@/api/admin/sys-dept';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getDeptInfo(queryForm);
}
})
const { proxy } = getCurrentInstance();
// Modal
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
// Form
const queryForm = reactive({});
const modalForm = reactive({
status: 2,
});
// rules
const rules = {
parentId: [{ required: true, message: '请选择上级部门' }],
deptName: [{ required: true, message: '请输入部门名称' }],
leader: [{ required: true, message: '请输入负责人' }],
};
// Columns
const columns = [
{
title: '部门名称',
dataIndex: 'deptName',
},
{
title: '排序',
dataIndex: 'sort',
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
},
{
title: '创建时间',
dataIndex: 'createdAt',
slotName: 'createdAt',
},
{
title: '操作',
slotName: 'action',
},
];
// Table Data
const tableData = ref([]);
//
const handleQuery = async () => {
const res = await getDept(queryForm);
tableData.value = deepDelChildren(res.data);
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getDeptInfo(queryForm);
}
//
const handleAdd = ({ deptId, status = 2 } = {}) => {
modalVisible.value = true;
modalTitle.value = '新增部门';
if (deptId) Object.assign(modalForm, {parentId: deptId, status});
};
//
const handleUpdate = async (record) => {
modalVisible.value = true;
modalTitle.value = '修改部门信息';
await nextTick();
const { parentId, deptName, leader, phone, email, status, deptId } = record;
Object.assign(modalForm, {
parentId,
deptName,
leader,
phone,
email,
status,
deptId,
});
};
// Children
function deepDelChildren(data) {
const depts = data;
let len = depts?.length;
// let len = depts && depts.length;
for (let i = 0; i < len; i++) {
if (depts[i].children.length > 0) {
deepDelChildren(depts[i].children);
} else {
delete depts[i].children;
}
}
return depts;
}
// Modal
const handleBeforeOk = (done) => {
proxy.$refs.modalFormRef.validate(async (err) => {
if (!err) {
let res;
if (Reflect.has(modalForm, 'deptId')) {
const { code, msg } = await updateDept(modalForm, modalForm.deptId);
if (code == 200 ) {
proxy.$notification.success('修改成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await addDept(modalForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
}
done();
getDeptInfo();
} else {
proxy.$message.error('数据校验失败');
done(false);
}
});
};
//
const getDeptInfo = async (params = {}) => {
const { data, code, msg } = await getDept(params);
if ( code == 200 ) {
tableData.value = deepDelChildren(data);
} else {
proxy.$notification.error(msg);
}
};
onMounted(() => {
getDeptInfo();
});
</script>
<style lang="scss">
.action {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="username" label="用户名">
<a-input v-model="queryForm.username" placeholder="请输入用户名" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="status" label="状态">
<a-select v-model="queryForm.status" placeholder="请选择系统登录日志状态">
<a-option :value="2">正常</a-option>
<a-option :value="1">关闭</a-option>
</a-select>
</a-form-item>
<a-form-item field="ipaddr" label="IP地址">
<a-input v-model="queryForm.ipaddr" placeholder="请输入IP地址" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="action">
<a-space>
<a-button
type="primary"
status="danger"
:disabled="selectedKeys.length === 0"
@click="handleBatchDelete"
><icon-delete /> 批量删除</a-button
>
</a-space>
</div>
<a-table
:data="tableData"
:columns="columns"
row-key="id"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:pagination="{
'show-total': true,
'show-jumper': true,
'show-page-size': true,
current: currentPage,
total: pager.count,
}"
v-model:selectedKeys="selectedKeys"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-if="record.status == 1" color="red">失败</a-tag>
</template>
<template #loginTime="{ record }">
{{ parseTime(record.loginTime) }}
</template>
<template #action="{ record }">
<a-popconfirm
content="是否删除当前数据?"
@ok="removeSysLoginLogInfo([record.id])"
>
<a-button type="text" status="danger">删除</a-button>
</a-popconfirm>
</template>
</a-table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { getSysLoginLog, removeSysLoginLog } from '@/api/admin/sys-login-log';
const { proxy } = getCurrentInstance();
//
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
//
const selectedKeys = ref([]);
const queryForm = reactive({});
const tableData = ref([]);
const columns = [
{ title: '用户名', dataIndex: 'username' },
{ title: '类型', dataIndex: 'msg' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: 'IP地址', dataIndex: 'ipaddr' },
{ title: '登陆时间', dataIndex: 'loginTime', slotName: 'loginTime' },
{ title: '操作', slotName: 'action', slotName: 'action' },
];
//
const handleQuery = () => {
getSysLoginLogInfo({ ...pager, ...queryForm });
}
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
handlePageChange(1);
};
//
const handlePageChange = (page) => {
currentPage.value = page;
pager.pageIndex = page;
getSysLoginLogInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getSysLoginLogInfo({ ...pager, ...queryForm });
};
//
const handleBatchDelete = () => {
proxy.$modal.confirm({
title: '注意',
content: '是否删除当前所有勾选数据?',
onOk: () => {
removeSysLoginLogInfo(selectedKeys.value);
},
onCancel: () => {
proxy.$message.info('已取消操作');
}
})
}
//
const getSysLoginLogInfo = async (params = {}) => {
const res = await getSysLoginLog(params);
const { count, list, pageIndex, pageSize } = res.data;
tableData.value = list;
Object.assign(pager, { total: count, pageIndex, pageSize });
};
//
const removeSysLoginLogInfo = async (ids) => {
const res = await removeSysLoginLog({ ids });
proxy.$message.success(res.msg);
getSysLoginLogInfo();
};
onMounted(() => {
getSysLoginLogInfo();
});
</script>
<style lang="scss">
.action {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,350 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="title" label="菜单名称">
<a-input v-model="queryForm.title" placeholder="请输入菜单名称" @press-enter="handleQuery"/>
</a-form-item>
<a-form-item field="visible" label="状态">
<a-select v-model="queryForm.visible" placeholder="请选择菜单状态">
<a-option value="1">显示</a-option>
<a-option value="0">隐藏</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery">搜索</a-button>
<a-button @click="$refs.queryFormRef.resetFields()">重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<!-- 动作 -->
<div class="action">
<a-space>
<a-button type="primary" @click="handleAddMenu()">新增菜单</a-button>
</a-space>
</div>
<!-- 菜单管理列表 -->
<a-table :columns="columns" :data="tableData" row-key="menuId" :pagination="false">
<template #icon="{ record }">
<component :is="record.icon" :style="{ fontSize: '18px' }"></component>
</template>
<template #menutype="{ record }">
<a-tag color="purple" v-if="record.menuType === 'M'">目录</a-tag>
<a-tag color="orange" v-else-if="record.menuType === 'C'">菜单</a-tag>
<a-tag color="blue" v-else-if="record.menuType === 'F'">按钮</a-tag>
</template>
<template #isFrame="{ record }">
<a-tag v-if="record.isFrame == '1'" color="green">内部</a-tag>
<a-tag v-else color="red">外部</a-tag>
</template>
<template #visible="{ record }">
<a-tag v-if="record.visible == '0'" color="green">显示</a-tag>
<a-tag v-else color="red">隐藏</a-tag>
</template>
<template #action="{ record }">
<a-button v-has="'admin:sysMenu:add'" type="text" @click="handleAddMenu(record.menuId)">新增</a-button>
<a-button v-has="'admin:sysMenu:edit'" type="text" @click="handleUpdate(record)">修改</a-button>
<a-button v-has="'admin:sysMenu:remove'" type="text" @click="() => { deleteVisible = true; deleteData = [record.menuId]; }">删除</a-button>
</template>
</a-table>
<!-- 菜单管理新增与提交弹窗 -->
<a-modal v-model:visible="modalVisible" :title="modalTitle" title-align="start" :width="800" modal-class="menu-modal" @before-ok="handleSubmit" @close="() => {$refs.modalFormRef.resetFields(); modalForm.menuId = null;}">
<a-form :model="modalForm" :rules="modalRules" ref="modalFormRef" auto-label-width label-align="left">
<a-form-item field="menuType" label="菜单类型">
<a-radio-group v-model="modalForm.menuType">
<a-radio value="M">目录</a-radio>
<a-radio value="C">菜单</a-radio>
<a-radio value="F">按钮</a-radio>
</a-radio-group>
</a-form-item>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item field="parentId" label="上级菜单">
<a-tree-select v-model="modalForm.parentId" :data="tableData" :field-names="{ key: 'menuId', icon: '_' }" :allow-search="true" :filter-tree-node="filterTreeNode" placeholder="请选择上级菜单"/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="title" label="菜单标题">
<a-input v-model="modalForm.title" placeholder="请输入菜单标题" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item field="icon" label="菜单图标">
<a-select v-model="modalForm.icon" :options="parseIconName" :field-names="{ name: 'value' }" :trigger-props="{ contentClass: 'iconselect-trigger' }" allow-search placeholder="请选择菜单图标">
<template #option="{ data }">
<component :is="data.value" />
</template>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="sort" label="显示排序">
<a-input-number v-model="modalForm.sort" mode="button" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12" v-if="modalForm.menuType !== 'F'">
<a-form-item field="menuName" label="路由名称">
<a-input v-model="modalForm.menuName" placeholder="请输入路由名称" />
</a-form-item>
</a-col>
<a-col :span="12" v-if="modalForm.menuType !== 'F'">
<a-form-item field="component" label="组件名称">
<a-input v-model="modalForm.component" placeholder="请输入组件名称"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12" v-if="modalForm.menuType !== 'F'">
<a-form-item field="path" label="路由地址">
<a-input v-model="modalForm.path" placeholder="请输入路由地址" />
</a-form-item>
</a-col>
<a-col :span="12" v-if="modalForm.menuType === 'C' || modalForm.menuType === 'F'">
<a-form-item field="permission" label="权限标识">
<a-input v-model="modalForm.permission" placeholder="请输入权限标识"/>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="isFrame" label="是否外链" v-if="modalForm.menuType !== 'F'">
<a-radio-group v-model="modalForm.isFrame">
<a-radio value="0"></a-radio>
<a-radio value="1"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="visible" label="菜单状态" v-if="modalForm.menuType !== 'F'">
<a-radio-group v-model="modalForm.visible">
<a-radio value="0">显示</a-radio>
<a-radio value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="apis" label="API权限" v-if="modalForm.menuType !== 'M'">
<a-transfer v-model:model-value="modalForm.apis" :data="transferData" :title="['未授权', '已授权']" show-search />
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230210 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removeMenu"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230210 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { onMounted, reactive, ref, getCurrentInstance, computed, watch, watchEffect } from 'vue';
import * as ArcoIconModules from '@arco-design/web-vue/es/icon';
import { getMenu } from '@/api/admin/menu';
import { getSysApi } from '@/api/admin/sys-api';
import { addMenu, removeMenu, updateMenu, getMenuDetails } from '@/api/admin/menu';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getSysMenuInfo(pager);
}
})
const { proxy } = getCurrentInstance();
const { queryForm, handleQuery } = useQueryData();
//
const currentPage = ref(1);
//
const pager = {
count: 0,
current: 1,
pageSize: 10,
};
// QueryModel
function useQueryData() {
//
const queryForm = reactive({});
const handleQuery = () => {
getSysMenuInfo(queryForm);
};
return {
queryForm,
handleQuery,
};
}
//
const modalForm = reactive({
menuType: 'M',
sort: 0,
isFrame: '1',
visible: '0',
});
//
const modalRules = {
title: [{ required: true, message: '请输入菜单名称' }],
path: [{ required: true, message: '请输入菜单路由地址' }],
component: [
{ required: true, message: '请输入菜单路由地址' },
{ pattern: /^[\/A-Za-z.-.()*]+$/, trigger: 'blur', message: '校验规则: 只允许输入字母 a-z 或大写 A-Z 与 -'}
],
};
// 20220715
watchEffect(() => {
// Layout
if (modalForm.menuType == "M") {
modalForm.component = "Layout"
} else {
// Layout
if (modalForm.component == "Layout") {
modalForm.component = ""
//
} else {
modalForm.component = modalForm.component
}
}
})
// Table
const columns = [
{ title: '菜单名称', dataIndex: 'title',width: "220" },
// { title: '', dataIndex: 'icon', slotName: 'icon' },
{ title: '路由地址', dataIndex: 'path', width: "400" },
{ title: '组件地址', dataIndex: 'component', width: "300" },
{ title: '排序', dataIndex: 'sort', width: "80" },
{ title: '类型', dataIndex: 'menuType', slotName: 'menutype', width: "100" },
{ title: '是否外联', dataIndex: 'isFrame', slotName: 'isFrame', width: "100" },
{ title: '显示状态', dataIndex: 'visible', slotName: 'visible', width: "100" },
{ title: '操作', slotName: 'action' ,width: "220", fixed: "right" },
];
const tableData = ref([]);
// Transfer Data
const transferData = ref([]);
// Modal
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
//
const handleAddMenu = (parentId = null) => {
modalVisible.value = true;
modalTitle.value = '新增菜单';
if (parentId) modalForm.parentId = parentId;
getSysMenuInfo();
};
// TreeSearchFilter
const filterTreeNode = (searchVal, nodeData) => {
return nodeData.title.indexOf(searchVal) > -1;
};
//
const handleUpdate = async (record) => {
const res = await getMenuDetails(record.menuId);
Object.assign(modalForm, res.data);
modalTitle.value = '修改菜单';
modalVisible.value = true;
};
// handleSubmit 20220713
const handleSubmit = (done) => {
proxy.$refs.modalFormRef.validate(async (valid) => {
if (!valid) {
if (!modalForm.menuId) {
console.log("dasdasd",modalForm)
const { success } = await addMenu(modalForm);
if (success) proxy.$message.success('新增成功');
} else {
const { success } = await updateMenu(modalForm, modalForm.menuId);
if (success) proxy.$message.success('修改成功');
}
getSysMenuInfo(pager);
done();
} else {
proxy.$message.error('表单校验失败');
done(false);
}
});
};
//
const getSysMenuInfo = async (params = {}) => {
const { code, data, msg } = await getMenu(params);
if ( code == 200 ) {
tableData.value = data;
} else {
proxy.$notification.error(msg)
}
};
// API
const getSysApiInfo = async () => {
const { code, data, msg } = await getSysApi({ pageSize: 10000, type: 'BUS' });
if ( code == 200 ) {
transferData.value = data.list.map((item) => {
return { value: item.id, label: item.title };
});
} else {
proxy.$notification.error(msg)
}
};
// Icon Object list
const parseIconName = computed(() => {
const iconNameList = [];
for (let key in ArcoIconModules) {
if (ArcoIconModules[key].name) {
iconNameList.push({ value: ArcoIconModules[key].name });
}
}
return iconNameList;
});
onMounted(() => {
getSysMenuInfo();
getSysApiInfo();
});
</script>
<style lang="scss">
// select trigger
.iconselect-trigger .arco-select-dropdown-list {
display: flex;
flex-wrap: wrap;
}
.iconselect-trigger .arco-select-option {
width: auto;
}
// 穿
.menu-modal {
.arco-transfer-view {
height: 350px;
width: 250px;
}
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="status" label="状态">
<a-select
v-model="queryForm.status"
placeholder="请选择系统操作日志状态"
>
<a-option :value="2">正常</a-option>
<a-option :value="1">关闭</a-option>
</a-select>
</a-form-item>
<a-form-item field="createdAt" label="创建时间">
<a-range-picker
disabled
v-model="queryForm.createdAt"
style="width: 254px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery"
><icon-search /> 搜索</a-button
>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="action">
<a-space>
<a-button
type="primary"
status="danger"
@click="handleBatchDelete"
:disabled="selectedKeys.length === 0"
><icon-delete /> 批量删除</a-button
>
</a-space>
</div>
<a-table
:data="tableData"
:columns="columns"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
row-key="id"
:pagination="{
'show-total': true,
'show-jumper': true,
'show-page-size': true,
current: currentPage,
total: pager.count,
}"
v-model:selectedKeys="selectedKeys"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-if="record.status == 1" color="red">关闭</a-tag>
</template>
<template #operTime="{ record }">
{{ parseTime(record.operTime) }}
</template>
<template #action="{ record }">
<a-popconfirm
content="是否删除当前数据?"
type="warning"
@ok="removeSysOperaLogInfo([record.id])"
>
<a-button type="text" status="danger">删除</a-button>
</a-popconfirm>
</template>
</a-table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { getSysOperaLog, removeSysOperaLog } from '@/api/admin/sys-opera-log';
const { proxy } = getCurrentInstance();
//
const currentPage = ref(1);
//
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
//
const selectedKeys = ref([]);
const queryForm = reactive({});
const tableData = ref([]);
const columns = [
{ title: '编号', dataIndex: 'id' },
{ title: '请求信息', dataIndex: 'operUrl', width: 500 },
{ title: '操作人员', dataIndex: 'operName' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: '操作日期', dataIndex: 'operTime', slotName: 'operTime' },
{ title: '操作', slotName: 'action', slotName: 'action' },
];
//
const handleQuery = () => {
handlePageChange(1);
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
handlePageChange(1);
};
//
const handleBatchDelete = () => {
console.log(selectedKeys.value);
if (selectedKeys.value.length === 0) {
proxy.$message.error('请勾选需要删除的数据!');
return;
}
proxy.$modal.confirm({
title: '注意',
content: '是否批量删除选中?',
onOk: () => {
removeSysOperaLogInfo(selectedKeys.value);
},
onCancel: () => {
proxy.$message.info('已取消操作');
},
});
};
//
const handlePageChange = (page) => {
currentPage.value = page;
pager.pageIndex = page;
getSysOperaLogInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getSysOperaLogInfo({ ...pager, ...queryForm });
};
//
const getSysOperaLogInfo = async (params = {}) => {
const res = await getSysOperaLog(params);
const { count, list, pageIndex, pageSize } = res.data;
tableData.value = list;
Object.assign(pager, { total: count, pageIndex, pageSize });
};
/**
* 删除操作日志
* @params {array} ids
*/
const removeSysOperaLogInfo = async (ids) => {
const res = await removeSysOperaLog({ ids });
proxy.$message.success(res.msg);
getSysOperaLogInfo();
};
onMounted(() => {
getSysOperaLogInfo();
});
</script>
<style lang="scss">
.action {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,289 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="postCode" label="岗位编码">
<a-input v-model="queryForm.postCode" placeholder="请输入岗位编码" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="postName" label="岗位名称">
<a-input v-model="queryForm.postName" placeholder="请输入岗位名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="status" label="岗位状态">
<a-select v-model="queryForm.status" placeholder="请选择岗位状态" :style="{ width: '181px' }">
<a-option :value="2">正常</a-option>
<a-option :value="1">停用</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button v-has="'admin:sysPost:query'" type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<!-- action -->
<div class="action">
<a-space>
<a-button v-has="'admin:sysPost:add'" type="primary" @click="handleAdd"><icon-plus /> 新增 </a-button>
<a-button v-has="'admin:sysPost:remove'" type="primary" status="danger" @click="() => { deleteVisible = true; }"><icon-delete /> 批量删除 </a-button>
<a-button type="primary" status="warning" disabled><icon-download /> 导出 </a-button>
</a-space>
</div>
<!-- table -->
<a-table
:columns="columns"
:data="tableData"
:pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
row-key="postId"
@selection-change="(selection) => {deleteData = selection;}"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-else color="red">停用</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button v-has="'admin:sysPost:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改</a-button>
<a-button v-has="'admin:sysPost:remove'" type="text" @click="() => { deleteVisible = true; deleteData = [record.postId]; }"><icon-delete /> 删除</a-button>
</a-space>
</template>
</a-table>
<!-- Modal -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
title-align="start"
@before-ok="handleSubmit"
@close="() => {$refs.modalFormRef.resetFields(); modalForm.postId = null;}"
>
<a-form :model="modalForm" :rules="rules" ref="modalFormRef">
<a-form-item field="postName" label="岗位名称">
<a-input v-model="modalForm.postName" placeholder="请输入岗位名称" />
</a-form-item>
<a-form-item field="postCode" label="岗位编码">
<a-input v-model="modalForm.postCode" placeholder="请输入岗位编码" />
</a-form-item>
<a-form-item field="sort" label="岗位排序">
<a-input-number
v-model="modalForm.sort"
mode="button"
:default-value="0"
:style="{ width: '150px' }"
/>
</a-form-item>
<a-form-item field="status" label="岗位状态">
<a-radio-group v-model="modalForm.status">
<a-radio :value="2"> 正常 </a-radio>
<a-radio :value="1"> 停用 </a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="modalForm.remark" placeholder="请输入备注内容" />
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removePost"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, onMounted, nextTick, watch } from 'vue';
import { getPost, addPost, removePost, updatePost } from '@/api/admin/post';
import { parseTime } from '@/utils/parseTime';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getPostInfo(pager);
}
})
const { proxy } = getCurrentInstance();
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
// form
const queryForm = reactive({});
const modalForm = reactive({
sort: 0,
status: 2,
});
// Rules
const rules = {
postName: [{ required: true, message: '请输入岗位名称' }],
postCode: [{ required: true, message: '请输入岗位编码' }],
sort: [{ required: true, message: '请选择岗位排序' }],
};
// Modal
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
// Batch Del List
let batchList = [];
// Table Columns
const columns = [
{ title: '岗位编号', dataIndex: 'postId' },
{ title: '岗位编码', dataIndex: 'postCode' },
{ title: '岗位名称', dataIndex: 'postName' },
{ title: '岗位排序', dataIndex: 'sort' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: '创建时间', dataIndex: 'createdAt', slotName: 'createdAt' },
{ title: '操作', slotName: 'action' },
];
// Table Data
const tableData = ref([]);
//
const handleAdd = () => {
modalVisible.value = true;
modalTitle.value = '新增岗位';
};
//
const handleUpdate = async (record) => {
modalVisible.value = true;
modalTitle.value = '修改岗位';
await nextTick();
Object.assign(modalForm, record);
};
// Modal ok
// Modal done()
const handleSubmit = (done) => {
proxy.$refs.modalFormRef.validate(async (valid) => {
if (!valid) {
let res;
if (!modalForm.postId) {
const { code, msg } = await addPost(modalForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await updatePost(modalForm, modalForm.postId);
if (code == 200 ) {
proxy.$notification.success('更新成功');
} else {
proxy.$notification.error(msg);
}
}
done();
getPostInfo(pager);
} else {
proxy.$message.error('表单校验失败');
done(false);
}
});
};
//
const handleBatchDelete = () => {
if (batchList.length !== 0) {
proxy.$modal.warning({
title: '提示',
content: '是否批量删除以下选中的数据?',
hideCancel: false,
onOk: async () => {
const res = await removePost({ ids: batchList });
proxy.$message.success(res.msg);
getPostInfo(pager);
},
onCancel: () => {
proxy.$message.info('已取消批量删除数据');
},
});
} else {
proxy.$message.error('请勾选需要删除的数据!');
}
};
/**
* 分页改变
* @param {Number} [page]
*/
const handlePageChange = (page) => {
pager.pageIndex = page;
//
currentPage.value = page;
getPostInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getPostInfo({ ...pager, ...queryForm });
};
//
const getPostInfo = async (params = {}) => {
const { data, code, msg } = await getPost(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
//
const handleQuery = async () => {
const params = {
pageIndex: pager.pageIndex,
pageSize: pager.pageSize,
...queryForm,
};
getPostInfo(params);
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getPostInfo(queryForm);
}
onMounted(() => {
getPostInfo(pager);
});
</script>
<style lang="scss">
.action {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,382 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="roleName" label="角色名称">
<a-input v-model="queryForm.roleName" placeholder="请输入角色名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="roleKey" label="权限字符">
<a-input v-model="queryForm.roleKey" placeholder="请输入权限字符" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="status" label="状态">
<a-select v-model="queryForm.status" placeholder="请选择角色状态">
<a-option :value="2">正常</a-option>
<a-option :value="1">停用</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="action">
<a-space>
<a-button v-has="'admin:sysRole:add'" type="primary" @click="handleAdd"><icon-plus /> 新增</a-button>
<a-button v-has="'admin:sysRole:remove'" type="primary" status="danger" @click="() => { deleteVisible = true; }"><icon-delete /> 批量删除</a-button>
<a-button type="primary" status="warning" disabled><icon-download /> 导出</a-button>
</a-space>
</div>
<a-table
:columns="columns"
:data="tableData"
:pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }"
row-key="roleId"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@selection-change="(selection) => {deleteData = selection;}"
@select="handleSelect"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-else color="red">停用</a-tag>
</template>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #action="{ record }">
<a-space>
<a-button v-has="'admin:sysRole:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改</a-button>
<a-button v-has="'admin:sysRole:update'" type="text" @click="handleDataScope(record)"><icon-check-circle /> 数据权限 </a-button>
<a-button v-has="'admin:sysRole:remove'" type="text" @click="() => { deleteVisible = true; deleteData = [record.roleId]; }"><icon-check-circle /> 删除 </a-button>
</a-space>
</template>
</a-table>
<!-- Role Modal -->
<a-modal
v-model:visible="modalVisible"
:title="title"
title-align="start"
@before-ok="handleBeforeOk"
@close="handleCancel"
>
<a-form :model="modalForm" :rules="rules" ref="modalFormRef">
<a-form-item field="roleName" label="角色名称">
<a-input v-model="modalForm.roleName" placeholder="请输入角色名称" />
</a-form-item>
<a-form-item field="roleKey" label="权限字符">
<a-input v-model="modalForm.roleKey" placeholder="请输入权限字符" />
</a-form-item>
<a-form-item field="roleSort" label="角色排序">
<a-input-number
v-model="modalForm.roleSort"
mode="button"
:default-value="0"
:style="{ width: '150px' }"
/>
</a-form-item>
<a-form-item field="status" label="状态">
<a-radio-group v-model="modalForm.status">
<a-radio value="2">正常</a-radio>
<a-radio value="1">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="权限设置">
<a-tree
v-model:checked-keys="checkedKeys"
:checkable="true"
:check-strictly="false"
:data="treeData"
:default-expand-all="false"
:field-names="{ key: 'id', title: 'label' }"
/>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="modalForm.remark" placeholder="请输入备注内容" />
</a-form-item>
</a-form>
</a-modal>
<!-- DataScope Modal -->
<a-modal
v-model:visible="scopedModalVisible"
:title="title"
title-align="start"
@before-ok="handleScopeBeforeOk"
@close="handleCancel"
>
<a-form :model="scopeForm">
<a-form-item field="roleName" label="角色名称">
<a-input v-model="scopeForm.roleName" disabled />
</a-form-item>
<a-form-item field="roleKey" label="权限字符">
<a-input v-model="scopeForm.roleKey" disabled />
</a-form-item>
<a-form-item field="dataScope" label="权限范围">
<a-select v-model="scopeForm.dataScope" placeholder="请选择权限范围">
<a-option
v-for="item in dataScopeOptions"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removeRole"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { onMounted, reactive, ref, getCurrentInstance, nextTick, watch } from 'vue';
import { getRole, addRole, updateRole, removeRole, updateRoleScoped, getRoleMenuTree } from '@/api/admin/role';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getRoleInfo({ ...pager, ...queryForm });
}
})
const { proxy } = getCurrentInstance();
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
// Batch Delete List
const batchDeleteList = ref([]);
// Form
const queryForm = reactive({});
const modalForm = reactive({
sort: 0,
status: '2',
});
const scopeForm = reactive({});
// rules
const rules = {
roleName: [{ required: true, message: '请输入角色名称' }],
roleKey: [{ required: true, message: '请输入权限字符' }],
};
// ScopeOption
const dataScopeOptions = [
{
value: '1',
label: '全部数据权限',
},
{
value: '2',
label: '自定数据权限',
},
{
value: '3',
label: '本部门数据权限',
},
{
value: '4',
label: '本部门及以下数据权限',
},
{
value: '5',
label: '仅本人数据权限',
},
];
// Table Columns
const columns = [
{ title: '编号', dataIndex: 'roleId' },
{ title: '角色名称', dataIndex: 'roleName' },
{ title: '权限字符', dataIndex: 'roleKey' },
{ title: '排序', dataIndex: 'roleSort' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: '创建时间', dataIndex: 'createdAt', slotName: 'createdAt' },
{ title: '操作', slotName: 'action', width: 250 },
];
// Table Data;
const tableData = ref([]);
// Tree Data;
const checkedKeys = ref([]);
const treeData = ref([]);
// Modal
const modalVisible = ref(false);
const scopedModalVisible = ref(false);
const title = ref('默认标题');
// Table Select
const handleSelect = (rowKey) => {
batchDeleteList.value = rowKey;
};
//
const handleQuery = async () => {
const res = await getRole({ ...pager, ...queryForm });
const { count, list, pageIndex, pageSize } = res.data;
tableData.value = list;
Object.assign(pager, { count, pageIndex, pageSize });
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getRoleInfo(queryForm);
};
//
const handleAdd = () => {
modalVisible.value = true;
title.value = '创建角色';
};
//
const handleUpdate = async (record) => {
modalVisible.value = true;
title.value = '修改角色';
await nextTick();
Object.assign(modalForm, record);
// checkedKeys id
const menuIdsChecked = [];
record.sysMenu.forEach((item) => {
menuIdsChecked.push(item.menuId);
});
checkedKeys.value = menuIdsChecked;
};
//
const handleDataScope = async (record) => {
scopedModalVisible.value = true;
title.value = '分配数据权限';
const { roleKey, roleName, dataScope, roleId } = record;
await nextTick();
Object.assign(scopeForm, { roleKey, roleName, dataScope, roleId });
};
/**
* 分页改变
* @param {Number} [page]
*/
const handlePageChange = (page) => {
pager.pageIndex = page;
//
currentPage.value = page;
getRoleInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getRoleInfo({ ...pager, ...queryForm });
};
//
// 使 done()
const handleBeforeOk = (done) => {
proxy.$refs.modalFormRef.validate(async (valid) => {
// valid
if (!valid) {
modalForm.menuIds = checkedKeys.value;
let res;
if (modalForm.roleId) {
const { code, msg } = await updateRole(modalForm, modalForm.roleId);
if (code == 200 ) {
proxy.$notification.success('更新成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await addRole(modalForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
}
getRoleInfo();
done();
} else {
proxy.$message.error('数据校验失败');
done(false);
}
});
};
//
const handleScopeBeforeOk = async (done) => {
const res = await updateRoleScoped(scopeForm);
proxy.$message.success(res.msg);
done();
getRoleInfo();
};
//
const handleCancel = () => {
modalVisible.value = false;
modalForm.roleId = null;
scopeForm.roleId = null;
checkedKeys.value = [];
proxy.$refs.modalFormRef.resetFields();
};
//
const getRoleInfo = async (params = {}) => {
const { data, code, msg } = await getRole(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
//
const getRoleMenuTreeInfo = async () => {
const res = await getRoleMenuTree({}, 0);
treeData.value = res.data.menus;
checkedKeys.value = res.data.checkedKeys;
};
onMounted(() => {
getRoleInfo();
getRoleMenuTreeInfo();
});
</script>
<style setup>
.action {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<div class="app-container">
<a-tabs default-active-key="1" position="left" :style="{ textAlign: 'right' }">
<a-tab-pane key="1" title="系统内置">
<a-form :model="form" :style="{ width: '50%' }">
<a-form-item field="sys_app_name" label="系统名称">
<a-input v-model="form.sys_app_name"></a-input>
</a-form-item>
<a-form-item field="sys_app_logo" label="系统Logo">
<a-space>
<div class="upload-logo-preview">
<img width="150" height="150" />
</div>
<a-upload action="/">
<template #upload-button>
<div class="upload-logo-card">
<div class="upload-logo-card-text">
<IconPlus />
</div>
</div>
</template>
</a-upload>
</a-space>
</a-form-item>
<a-form-item field="sys_user_initPassword" label="初始密码">
<a-input-password v-model="form.sys_user_initPassword"></a-input-password>
</a-form-item>
<a-form-item field="sys_index_skinName" label="皮肤样式">
<a-select v-model="form.sys_index_skinName">
<a-option>蓝色</a-option>
</a-select>
</a-form-item>
<a-form-item field="sys_index_sideTheme" label="侧边栏主题">
<a-select v-model="form.sys_index_sideTheme">
<a-option>深色主题</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary">提交</a-button>
<a-button>重置</a-button>
</a-space>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" title="其它">暂无内容</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup>
import { reactive } from 'vue';
import { IconPlus } from '@arco-design/web-vue/es/icon'
const form = reactive({});
</script>
<style lang="scss">
.upload-logo-preview {
font-size: 0px;
}
.upload-logo-card {
display: flex;
align-items: center;
justify-content: center;
width: 150px;
height: 150px;
box-sizing: border-box;
border: 1px dashed #c0ccda;
border-radius: 5px;
color: #4e5969;
&-text {
font-size: 24px;
}
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<div class="tree-container">
<a-tree
:data="props.data"
:field-names="{ key: 'deptId', title: 'deptName' }"
block-node
@select="handleTreeSelect"
/>
</div>
</template>
<script setup>
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['nodeClick']);
const handleTreeSelect = (selectKeys) => {
emits('nodeClick', { deptId: `/${selectKeys}/` });
}
</script>
<style lang="scss">
.tree-container {
padding-right: 20px;
}
</style>

View File

@ -0,0 +1,621 @@
<template>
<div class="app-container">
<!-- Query -->
<a-form ref="queryFormRef" :model="queryForm" layout="inline">
<a-form-item field="deptName" label="部门名称">
<a-input v-model="queryForm.deptName" placeholder="请输入部门名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="username" label="用户名称">
<a-input v-model="queryForm.username" placeholder="请输入用户名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="phone" label="手机号码">
<a-input v-model="queryForm.phone" placeholder="请输入用户手机号" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="status" label="用户状态">
<a-select
v-model="queryForm.status"
placeholder="请选择用户状态"
:style="{ width: '205px' }"
>
<a-option value="2">正常</a-option>
<a-option value="1">停用</a-option>
</a-select>
</a-form-item>
<a-divider direction="vertical" :style="{ height: '30px' }" />
<a-form-item class="form-action">
<a-space size="medium">
<a-button v-has="'admin:sysUser:query'" type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- divider -->
<a-divider />
<!-- Table -->
<a-row>
<a-col :span="4">
<!-- tree组件只在组件第一次渲染时展开此处等待数据加载完成再渲染组件 -->
<tree-dept v-if="treeDeptData" :data="treeDeptData" @node-click="getSysUserInfo" />
</a-col>
<a-col :span="20">
<!-- Action -->
<a-space class="action">
<a-button v-has="'admin:sysUser:add'" type="primary" @click="handleAdd" data-test="newUser"><icon-plus /> 新增</a-button>
<a-button v-has="'admin:sysUser:remove'" type="primary" status="danger" @click="() => { deleteVisible = true; }"><icon-delete /> 批量删除</a-button>
</a-space>
<!-- Table -->
<a-table
:columns="columns"
:data="tableData"
:bordered="false"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }"
row-key="userId"
@selection-change="(selection) => {deleteData = selection;}"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #dept="{ record }">
{{ record.dept.deptName }}
</template>
<template #status="{ record }">
<a-switch
v-model="record.status"
checked-value="2"
unchecked-value="1"
@change="handleSwitchChange(record)"
/>
</template>
<template #createdAt="{ record }">
{{ parseTime(record.createdAt) }}
</template>
<template #action="{ record }">
<a-button v-has="'admin:sysUser:edit'" type="text" @click="handleUpdate(record)"><icon-edit /> 修改</a-button>
<a-button v-has="'admin:sysUser:edit'" type="text" @click="() => { deleteVisible = true; deleteData = [record.userId]; }"><icon-delete /> 删除</a-button>
<a-button v-has="'admin:sysUser:resetPassword'" type="text" @click="handleReset(record.userId)"><icon-refresh /> 重置</a-button>
</template>
</a-table>
</a-col>
</a-row>
<!-- Modal -->
<a-modal
v-model:visible="modalVisible"
title-align="start"
:width="600"
@cancel="handleModalCancel('modalFormRef')"
@before-ok="handleBeforeOk"
>
<template #title>
{{ modalTitle }}
</template>
<a-form
ref="modalFormRef"
:model="modalForm"
:rules="rules"
auto-label-width
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="nickName" label="用户昵称">
<a-input
v-model="modalForm.nickName"
placeholder="请输入用户昵称"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="deptId" label="所属部门">
<a-tree-select
v-model="modalForm.deptId"
:data="treeDeptData"
:field-names="{ key: 'deptId', title: 'deptName' }"
placeholder="请选择所属部门"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="phone" label="手机号码">
<a-input v-model="modalForm.phone" placeholder="请输入手机号码" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="email" label="邮箱">
<a-input v-model="modalForm.email" placeholder="请输入邮箱" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="username" label="用户名称">
<a-input
v-model="modalForm.username"
placeholder="请输入用户名称"
/>
</a-form-item>
</a-col>
<a-col v-if="!modalForm.userId" :span="12">
<a-form-item field="password" label="用户密码">
<a-input-password
v-model="modalForm.password"
placeholder="请输入用户密码"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="sex" label="用户性别">
<a-select v-model="modalForm.sex" placeholder="请选择用户性别">
<a-option value="0"> </a-option>
<a-option value="1"> </a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="status" label="状态">
<a-radio-group v-model="modalForm.status">
<a-radio value="2"> 正常 </a-radio>
<a-radio value="1"> 停用 </a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="postId" label="岗位">
<a-select v-model="modalForm.postId" placeholder="请选择岗位">
<a-option
v-for="item in postList"
:key="item.postId"
:value="item.postId"
:label="item.postName"
/>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="roleId" label="角色">
<a-select v-model="modalForm.roleId" placeholder="请选择角色">
<a-option
v-for="item in roleList"
:key="item.roleId"
:value="item.roleId"
:label="item.roleName"
/>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="remark" label="备注">
<a-textarea placeholder="请输入备注" allow-clear />
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:visible="resetPwdVisible"
title="重置密码"
@before-ok="handleResetPwd"
@cancel="$refs.resetPwdFormRef.resetFields()"
>
<a-form
ref="resetPwdFormRef"
:model="resetPwdForm"
:rules="resetPwdRules"
auto-label-width
>
<a-form-item field="password" label="新密码">
<a-input-password
v-model="resetPwdForm.password"
placeholder="请输入新密码"
/>
</a-form-item>
<a-form-item field="repeatPwd" label="确认密码">
<a-input-password
v-model="resetPwdForm.repeatPwd"
placeholder="请输入确认密码"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="removeUser"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, onMounted, watch } from 'vue';
import { IconSearch, IconLoop } from '@arco-design/web-vue/es/icon';
import TreeDept from './components/TreeDept.vue';
import { getUser, addUser, updateUser, removeUser, updateUserStatus, resetUserPwd } from '@/api/admin/sys-user';
import { getRole } from '@/api/admin/role';
import { getPost } from '@/api/admin/post';
import { getDept } from '@/api/admin/sys-dept';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getSysUserInfo({ ...pager, ...queryForm });
}
})
const { proxy } = getCurrentInstance();
// Query
const { queryForm, handleQuery, handleResetQuery } = useQueryData();
// ApiInfo
const { currentPage, getSysPostInfo, getSysRoleInfo, getSysDeptTreeInfo, getSysUserInfo } =
useApiInfo();
// Pager
const pager = reactive({
count: 0,
pageIndex: 1,
pageSize: 10
});
// Reset Pwd
const {
resetPwdForm,
resetPwdVisible,
resetPwdRules,
handleReset,
handleResetPwd,
} = useResetPwd();
// Table Operate
const {
columns,
tableData,
treeDeptData,
roleList,
postList,
handlePageChange,
handlePageSizeChange,
handleSwitchChange,
} = useTableList();
// ModalForm Operate
const {
rules,
modalForm,
modalVisible,
modalTitle,
handleAdd,
handleUpdate,
handleBeforeOk,
handleModalCancel,
} = useModalOperate();
function useQueryData() {
const queryForm = reactive({});
//
const handleQuery = () => {
getSysUserInfo(queryForm);
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getSysUserInfo(queryForm);
};
return { queryForm, handleQuery, handleResetQuery };
}
function useResetPwd() {
const resetPwdVisible = ref(false);
const resetPwdForm = reactive({});
// Rules
const resetPwdRules = {
password: [{ required: true, message: '请输入密码' }],
repeatPwd: [
{
required: true,
message: '请重复输入密码',
},
{
validator: (value, cb) => {
if (value !== resetPwdForm.password) {
cb('两次输入的密码不一致');
}
},
},
],
};
// API
const resetParams = {};
//
const handleReset = (userId) => {
resetPwdVisible.value = true;
resetParams.userId = userId;
};
//
const handleResetPwd = (done) => {
proxy.$refs.resetPwdFormRef.validate(async (err) => {
if (!err) {
resetParams.password = resetPwdForm.password;
const { msg } = await resetUserPwd(resetParams);
proxy.$message.success(msg);
done();
getSysUserInfo();
} else {
done(false);
}
});
};
return {
resetPwdForm,
resetPwdVisible,
resetPwdRules,
handleReset,
handleResetPwd,
};
}
function useApiInfo() {
const currentPage = ref(1);
/**
* 获取用户信息
* @param {*} [params]
*/
const getSysUserInfo = async (params = {}) => {
const { data, code, msg } = await getUser(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
//
const getSysRoleInfo = async () => {
const res = await getRole();
roleList.value = res.data.list;
};
//
const getSysPostInfo = async () => {
const res = await getPost();
postList.value = res.data.list;
};
//
const getSysDeptTreeInfo = async () => {
const res = await getDept();
treeDeptData.value = res.data;
};
return {
currentPage,
getSysPostInfo,
getSysRoleInfo,
getSysDeptTreeInfo,
getSysUserInfo,
};
}
function useTableList() {
//
const treeDeptData = ref();
const roleList = ref([]);
const postList = ref([]);
// Table columns
const columns = [
{
title: '编号',
dataIndex: 'userId',
},
{
title: '登录名',
dataIndex: 'username',
},
{
title: '昵称',
dataIndex: 'nickName',
},
{
title: '部门',
dataIndex: 'deptName',
slotName: 'dept',
},
{
title: '手机号',
dataIndex: 'phone',
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
},
{
title: '创建时间',
dataIndex: 'createdAt',
slotName: 'createdAt',
},
{
title: '操作',
slotName: 'action',
},
];
// Table Data
const tableData = ref([]);
/**
* 分页改变
* @param {Number} [page]
*/
const handlePageChange = (page) => {
pager.pageIndex = page;
//
currentPage.value = page;
getSysUserInfo({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getSysUserInfo({ ...pager, ...queryForm });
};
//
const handleSwitchChange = (record) => {
proxy.$modal.warning({
title: '注意',
content: `是否${record.status == 1 ? '停用' : '启用'} ${
record.username
} 用户`,
hideCancel: false,
onOk: async () => {
const params = { userId: record.userId, status: record.status };
const res = await updateUserStatus(params);
proxy.$message.success(res.msg);
},
onCancel: () => {
record.status = record.status == '2' ? '1' : '2';
},
});
};
return {
treeDeptData,
roleList,
postList,
columns,
tableData,
handlePageChange,
handlePageSizeChange,
handleSwitchChange,
};
}
function useModalOperate() {
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
// Form
const modalForm = reactive({ status: '2' });
// AddRules
const rules = {
nickName: [{ required: true, message: '请输入用户昵称' }],
deptId: [{ required: true, message: '请选择所属部门' }],
phone: [{ required: true, message: '请输入手机号' }],
email: [
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入正确的邮箱格式' },
],
username: [{ required: true, message: '请输入用户名称' }],
password: [{ required: true, message: '请输入用户密码' }],
};
// Modal
const handleModalCancel = (formEl) => {
modalVisible.value = false;
resetForm(formEl);
modalForm.userId = null;
};
//
const handleAdd = () => {
modalVisible.value = true;
modalTitle.value = '新增用户';
};
/**
* 修改用户
* @param {Object} val
*/
const handleUpdate = (val) => {
modalVisible.value = true;
Object.assign(modalForm, val);
};
// Form
const resetForm = (formEl) => {
if (!formEl) return;
proxy.$refs[formEl].resetFields();
};
//
const handleBeforeOk = (done) => {
proxy.$refs.modalFormRef.validate(async (valid) => {
if (!valid) {
if (!modalForm.userId) {
const { code, msg } = await addUser(modalForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await updateUser(modalForm, modalForm.userId);
if (code == 200 ) {
proxy.$notification.success('更新成功');
} else {
proxy.$notification.error(msg);
}
}
done();
proxy.$refs.modalFormRef.resetFields();
getSysUserInfo();
} else {
proxy.$message.error('表单校验失败');
done(false);
}
});
};
return {
rules,
modalForm,
modalVisible,
modalTitle,
handleAdd,
handleUpdate,
handleBeforeOk,
handleModalCancel,
};
}
onMounted(() => {
getSysUserInfo();
getSysDeptTreeInfo();
getSysRoleInfo();
getSysPostInfo();
});
</script>
<style lang="scss">
@media screen and (max-width: 1720px) {
.form-action {
margin-top: 12px;
}
}
.action {
margin-bottom: 8px;
}
</style>

View File

@ -0,0 +1,410 @@
<template>
<div class="app-container">
<a-form :model="dataInfo" :rules="rules" ref="modalFormRef" auto-label-width size="mini">
<a-card bordered>
<!-- 卡片插槽 开始 -->
<template #actions>
<a-button type="primary" @click="handleSubmit" long>提交</a-button>
<a-button type="primary" @click="close" long>返回</a-button>
</template>
<!-- 卡片插槽 结束 -->
<a-tabs default-active-key="2">
<a-tab-pane key="1" title="基本信息">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item field="tableName">
<template #label>
数据表名称
<a-tooltip content="数据库表名称针对gorm对应的table()使用,⚠️这里必须是蛇形结构">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.tableName" placeholder="请输入表名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="tableComment">
<template #label>
菜单名称
<a-tooltip content="同步的数据库表名称,生成配置数据时,用作菜单名称">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.tableComment" placeholder="请输入菜单名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="className">
<template #label>
结构体模型名称
<a-tooltip content="结构体模型名称代码中的struct名称定义使用">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.className" placeholder="请输入" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="functionAuthor" label="作者名称">
<a-input v-model="dataInfo.functionAuthor" placeholder="请输入作者名称" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item field="remark" label="备注">
<a-textarea v-model="dataInfo.remark" placeholder="请输入备注" />
</a-form-item>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="2" title="字段信息">
<a-alert type="warning" style="margin-bottom: 16px;">表字段中的idcreate_byupdate_bycreated_atupdated_atdeleted_at的字段在此列表中已经隐藏.</a-alert>
<a-table :data="tableData" :virtual-list-props="{height:600}" :pagination="false" :scroll="{x: '100%'}">
<template #columns>
<!-- <a-table-column title="序号" data-index="tableId" :width="180" ellipsis tooltip/> -->
<a-table-column title="字段列名" data-index="columnName"/>
<a-table-column title="字段描述" :width="120">
<template #cell="{ record }">
<a-input v-model="record.columnComment" />
</template>
</a-table-column>
<a-table-column title="物理类型" data-index="columnType" align="center" :width="120" />
<a-table-column title="go类型" align="center" :width="140">
<template #cell="{ record }">
<a-select v-model="record.goType" placeholder="请选择 ...">
<a-option value="int64">int64</a-option>
<a-option value="string">string</a-option>
</a-select>
</template>
</a-table-column>
<a-table-column title="go属性" align="center" >
<template #cell="{ record }">
<a-input v-model="record.goField"/>
</template>
</a-table-column>
<a-table-column title="json属性" align="center" >
<template #cell="{ record }">
<a-input v-model="record.jsonField"/>
</template>
</a-table-column>
<a-table-column title="编辑" align="center" :width="60">
<template #cell="{ record }">
<a-switch v-model="record.isInsert" type="large" checked-value="1" unchecked-value="0">
<template #checked-icon>
<icon-check/>
</template>
<template #unchecked-icon>
<icon-close/>
</template>
</a-switch>
</template>
</a-table-column>
<a-table-column title="列表" align="center" :width="60">
<template #cell="{ record }">
<a-switch v-model="record.isList" type="large" checked-value="1" unchecked-value="0">
<template #checked-icon>
<icon-check/>
</template>
<template #unchecked-icon>
<icon-close/>
</template>
</a-switch>
</template>
</a-table-column>
<a-table-column title="查询" align="center" :width="60">
<template #cell="{ record }">
<a-switch v-model="record.isQuery" type="large" checked-value="1" unchecked-value="0">
<template #checked-icon>
<icon-check/>
</template>
<template #unchecked-icon>
<icon-close/>
</template>
</a-switch>
</template>
</a-table-column>
<a-table-column title="查询方式" :width="90">
<template #cell="{ record }">
<a-select v-model="record.queryType" placeholder="请选择 ...">
<a-option label="=" value="EQ" />
<a-option label="!=" value="NE" />
<a-option label=">" value="GT" />
<a-option label=">=" value="GTE" />
<a-option label="<" value="LT" />
<a-option label="<=" value="LTE" />
<a-option label="LIKE" value="LIKE" />
</a-select>
</template>
</a-table-column>
<a-table-column title="必填" :width="70">
<template #cell="{ record }">
<!-- <a-checkbox v-model="record.isRequired"/> -->
<a-switch v-model="record.isRequired" type="large" checked-value="1" unchecked-value="0">
<template #checked-icon>
<icon-check/>
</template>
<template #unchecked-icon>
<icon-close/>
</template>
</a-switch>
</template>
</a-table-column>
<a-table-column title="显示类型" :width="140">
<template #cell="{ record }">
<a-select v-model="record.htmlType" placeholder="请选择 ...">
<a-option value="input">文本框</a-option>
<a-option value="select">下拉框</a-option>
<a-option value="radio">单选框</a-option>
<a-option value="textarea">文本域</a-option>
</a-select>
</template>
</a-table-column>
<a-table-column title="字典类型">
<template #cell="{ record }">
<a-select v-model="record.dictType" allow-clear allow-search placeholder="请选择">
<a-option v-for="dict in dictOptions" :key="dict.dictType" :label="dict.dictName" :value="dict.dictType" >
<a-descriptions :column="1" size="mini">
<a-descriptions-item :label="dict.dictName">
{{ dict.dictType }}
</a-descriptions-item>
</a-descriptions>
</a-option>
</a-select>
</template>
</a-table-column>
<a-table-column title="关系表">
<template #cell="{ record }">
<a-select v-model="record.fkTableName" allow-clear allow-search placeholder="请选择" @change="handleChangeConfig(record)">
<a-option v-for="table in tableTree" :key="table.tableName" :label="table.tableName" :value="table.tableName">
<span style="float: left; margin-right: 5px; font-size: 12px;">{{ table.tableName }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ table.tableComment }}</span>
</a-option>
</a-select>
</template>
</a-table-column>
<a-table-column title="关系表key">
<template #cell="{ record }">
<a-select v-model="record.fkLabelId" allow-clear allow-search placeholder="请选择">
<a-option v-for="column in record.fkCol" :key="column.columnName" :label="column.columnName" :value="column.jsonField">
<span style="float: left; margin-right: 5px; font-size: 12px;">{{ column.jsonField }}</span>
<span style="float: right; color: #8492a6; font-size: 12px;">{{ column.columnComment }}</span>
</a-option>
</a-select>
</template>
</a-table-column>
<a-table-column title="关系表value">
<template #cell="{ record }">
<a-select v-model="record.fkLabelName" allow-clear allow-search placeholder="请选择">
<a-option v-for="column in record.fkCol" :key="column.columnName" :label="column.columnName" :value="column.jsonField">
<span style="float: left; margin-right: 5px; font-size: 12px;">{{ column.jsonField }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ column.columnComment }}</span>
</a-option>
</a-select>
</template>
</a-table-column>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="3" title="生成信息">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item field="tplCategory" label="生成模板">
<a-select v-model="dataInfo.tplCategory">
<a-option label="关系表(增删改查)" value="crud" />
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="packageName">
<template #label>
应用名
<a-tooltip content="应用名例如在app文件夹下将该功能发到那个应用中默认admin">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.packageName" placeholder="请输入应用名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="businessName">
<template #label>
业务名
<a-tooltip content="可理解为功能英文名,例如 user">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.businessName" placeholder="请输入业务名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="functionName" label="功能描述">
<template #label>
功能描述
<a-tooltip content="同步的数据库表备注,用作类描述,例如:用户">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.functionName" placeholder="请输入功能描述" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="moduleName">
<template #label>
接口路径
<a-tooltip content="接口路径例如api/v1/{sys-user}">
<icon-question-circle-fill />
</a-tooltip>
</template>
<a-input v-model="dataInfo.moduleName" placeholder="请输入接口路径" >
<template #prepend>api/{version}/</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</a-card>
</a-form>
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, onMounted, unref } from 'vue';
import { optionselect as getDictOptionselect } from '@/api/admin/sys-dict'
import { getGenTable, updateGenTable, getTableTree } from '@/api/tools/gen';
const { proxy } = getCurrentInstance();
// Rules
const rules = {
tableName: [
{ required: true, message: '请输入表名称' },
{ match: /^[a-z\._]*$/g, message: '只允许小写字母,例如 sys_demo 格式'}
],
tableComment: [
{ required: true, message: '请输入菜单名称' },
],
className: [
{ required: true, message: '请输入模型名称' },
{ match: /^[A-Z][A-z0-9]*$/g, message: '只允许小写字母,例如 sys_demo 格式'}
],
functionAuthor: [
{ required: true, message: '请输入作者' },
{ match: /^[A-Za-z]+$/, message: '必须以大写字母开头,例如 SysDemo 格式'}
],
tplCategory: [
{ required: true, message: '请选择生成模板' },
],
packageName: [
{ required: true, message: '请输入生成包路径' },
],
moduleName: [
{ required: true, message: '请输入生成模块名' },
{ match: /^[a-z\-]*[a-z]$/g, message: '只允许小写字母,例如 sys-demo 格式'}
],
businessName: [
{ required: true, message: '请输入生成业务名' },
{ match: /^[a-z][A-Za-z]+$/, message: '校验规则: 只允许输入字母 a-z 或大写 A-Z ,并且小写字母开头'}
],
functionName: [
{ required: true, message: '请输入生成功能名' },
],
}
//
const stringToBool = (value) => {
if (value == "1") {
return true
} else {
return false
}
}
//
const dataInfo = ref({})
//
const tableData = ref([]);
//
const getGenTableQuery = async (params = {}) => {
const res = await getGenTable(params);
tableData.value = res.data.list;
// isInsert
// isList
// isQuery
// isRequired
dataInfo.value = res.data.info;
};
//
const tableTree = ref([]);
//
const getTableTreeQuery = async (params = {}) => {
getTableTree().then(response => {
tableTree.value = response.data
tableTree.value.unshift({ tableId: 0, className: '请选择' })
})
};
//
const handleChangeConfig = (row) => {
tableTree.value.filter(function(item) {
if (item.tableName === row.fkTableName) {
row.fkCol = item.columns
}
})
}
//
const dictOptions = ref([]);
//
const getDictOption = async () => {
const res =await getDictOptionselect();
dictOptions.value = res.data
};
const modalFormRef = ref(null);
// handleSubmit 20221101
const handleSubmit = () => {
const genTable = Object.assign({}, dataInfo.value)
genTable.columns = tableData.value
genTable.params = {
treeCode: genTable.treeCode,
treeName: genTable.treeName,
treeParentCode: genTable.treeParentCode
}
proxy.$refs.modalFormRef.validate(async (valid) => {
if (!valid) {
const { code, msg } = await updateGenTable(genTable);
if (code == 200 ) {
proxy.$notification.success('修改成功');
} else {
proxy.$notification.error(msg);
}
//
close()
} else {
proxy.$message.error('表单校验失败');
}
});
};
/** 关闭按钮 */
const close = () => {
proxy.$router.push({ path: '/dev-tools/gen', query: { t: Date.now() }})
}
onMounted(() => {
const { tableId } = proxy.$route.query
getGenTableQuery(tableId);
getTableTreeQuery()
getDictOption()
});
</script>
<style lang="scss">
</style>

View File

@ -0,0 +1,145 @@
<template>
<a-modal v-model:visible="visible" title="导入表" @ok="handleImportTable" :ok-loading="loading" @cancel="visible = false" width="50%">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="tableName" label="表名称">
<a-input v-model="queryForm.tableName" placeholder="请输入表名称" />
</a-form-item>
<a-form-item field="tableComment" label="表描述">
<a-input v-model="queryForm.tableComment" placeholder="请输入表描述" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<a-table
:data="tableData"
:pagination="{
'show-total': true,
'show-jumper': true,
'show-page-size': true,
total: pager.count,
current: currentPage,
}"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
row-key="tableName"
@selection-change="selectionChange"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #columns>
<a-table-column title="表名称" data-index="tableName" :width="180" ellipsis tooltip/>
<a-table-column title="表描述" data-index="tableComment" :width="180" ellipsis tooltip/>
<a-table-column title="创建时间" :width="180" ellipsis>
<template #cell="{ record }">
{{ parseTime(record.createTime) }}
</template>
</a-table-column>
<a-table-column title="更新时间" :width="180" ellipsis>
<template #cell="{ record }">
{{ parseTime(record.updateTime) }}
</template>
</a-table-column>
</template>
</a-table>
</a-modal>
</template>
<script setup>
import { reactive, ref, onMounted, getCurrentInstance } from 'vue';
import { listDbTable,importTable,listTable} from '@/api/tools/gen';
const { proxy } = getCurrentInstance();
//
const visible = ref(false);
//
const loading = ref(false)
//
const handleImportTable = () => {
loading.value = true
importTable({ tables: batchList.join(',') }).then(res => {
if (res.code === 200) {
proxy.$message.info(res.msg);
//
visible.value = false;
//
location.reload();
} else {
proxy.$message.info(res.msg);
}
loading.value = false
})
};
const queryForm = reactive({});
//
const handleQuery = async () => {
const params = {
pageIndex: pager.pageIndex,
pageSize: pager.pageSize,
...queryForm,
};
getDbTables(params);
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getDbTables(queryForm);
}
const tableData = ref([]);
// Batch Del List
let batchList = [];
// Table
const selectionChange = (rowKeys) => {
batchList = rowKeys;
};
/**
* 分页改变
* @param {Number} [page]
*/
const handlePageChange = (page) => {
pager.pageIndex = page;
//
currentPage.value = page;
getDbTables({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getDbTables({ ...pager, ...queryForm });
};
//
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
//
const getDbTables = async (params = {}) => {
const { data, code, msg } = await listDbTable(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
onMounted(() => {
getDbTables(pager);
});
</script>

View File

@ -0,0 +1,270 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="tableName" label="表名称">
<a-input v-model="queryForm.tableName" placeholder="请输入表名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="tableComment" label="表描述">
<a-input v-model="queryForm.tableComment" placeholder="请输入描述" @press-enter="handleQuery" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery"><icon-search /> 搜索</a-button>
<a-button @click="handleResetQuery"><icon-loop /> 重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<!-- action -->
<div class="action">
<a-space>
<a-button type="primary" @click="openImportTable"><icon-plus /> 导入 </a-button>
<a-button type="primary" status="danger" @click="() => { deleteVisible = true; }"><icon-delete /> 批量删除</a-button>
</a-space>
</div>
<!-- table -->
<a-table
:data="tableData"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
row-key="tableId"
@selection-change="(selection) => {deleteData = selection;}"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #columns>
<a-table-column title="序号" data-index="tableId" :width="180" ellipsis tooltip/>
<a-table-column title="表名称" data-index="tableName" :width="180" ellipsis tooltip/>
<a-table-column title="表描述" data-index="tableComment" :width="180" ellipsis tooltip/>
<a-table-column title="模型名称" data-index="className" :width="180" ellipsis tooltip/>
<a-table-column title="动作" :width="370" align="center" >
<template #cell="{ record }">
<a-button-group>
<a-button size="small" @click="handleEditTable(record)">编辑</a-button>
<a-button size="small" @click="handlePreview(record)">预览</a-button>
<a-popconfirm content="正在使用代码生成请确认?" okText="生成" type="warning" @ok="handleToProject(record)">
<a-button size="small">代码生成 </a-button>
</a-popconfirm>
<a-popconfirm content="正在使用【菜单以及API生成到数据库】请确认?" okText="写入DB" type="warning" @ok="handleToDB(record)">
<a-button size="small">生成配置 </a-button>
</a-popconfirm>
<a-popconfirm content="正在使用代码生成配置迁移脚本请确认?" okText="迁移" type="warning" @ok="handleToApiFile(record)">
<a-button size="small">生成迁移脚本 </a-button>
</a-popconfirm>
<a-button size="small" @click="() => { deleteVisible = true; deleteData = [record.jobId]; }">删除</a-button>
</a-button-group>
</template>
</a-table-column>
</template>
</a-table>
<!-- 导入对话框 -->
<import-table v-model:visible="modalVisible" />
<!-- 预览代码对话框 -->
<a-modal v-model:visible="codeVisible" title="预览代码" width="80%">
<a-space style="margin-bottom: 16px;">
<a-tag v-for="(value, key) of preview.data" :key="key" bordered color="#165dff" @click="codeChange(key)">{{ key.substring(key.lastIndexOf('/')+1,key.indexOf('.go.template')) }}</a-tag>
</a-space>
<!-- 代码编辑器 -->
<codemirror v-model="codestr" :style="{ height: '600px' }" :autofocus="true" :indent-with-tab="true" :tab-size="2" :extensions="extensions" />
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="delTable"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, getCurrentInstance, onMounted, nextTick, watch } from 'vue';
import importTable from './importTable.vue'
import { listTable,previewTable,delTable,apiToFile,toProjectTableCheckRole,toDBTable } from '@/api/tools/gen';
import { parseTime } from '@/utils/parseTime';
import { Codemirror } from 'vue-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getSysConfigInfo(pager);
}
})
const { proxy } = getCurrentInstance();
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
// form
const queryForm = reactive({});
const modalForm = reactive({
sort: 0,
status: 2,
});
const modalVisible = ref(false);
const openImportTable = () => {
modalVisible.value = true;
};
// Batch Del List
let batchList = [];
// Table Data
const tableData = ref([]);
//
const handleUpdate = async (record) => {
modalVisible.value = true;
modalTitle.value = '修改岗位';
await nextTick();
Object.assign(modalForm, record);
};
// Table
const selectionChange = (rowKeys) => {
batchList = rowKeys;
};
/**
* 分页改变
* @param {Number} [page]
*/
const handlePageChange = (page) => {
pager.pageIndex = page;
//
currentPage.value = page;
getListTable({ ...pager, ...queryForm });
};
//
const handlePageSizeChange = (pageSize) => {
pager.pageSize = pageSize;
getListTable({ ...pager, ...queryForm });
};
//
const getListTable = async (params = {}) => {
const { data, code, msg } = await listTable(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
//
const handleQuery = async () => {
const params = {
pageIndex: pager.pageIndex,
pageSize: pager.pageSize,
...queryForm,
};
getListTable(params);
};
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getListTable(queryForm);
}
//
//
const handleEditTable = (row) => {
const tableId = row.tableId || this.ids[0]
proxy.$router.push({ path: '/dev-tools/editTable', query: { tableId: tableId }})
}
//
//
const codeVisible = ref(false);
//
const preview = ref({
open: false,
title: '代码预览',
data: {},
activeName: 'api.go'
})
/** 预览按钮 */
const handlePreview = (row) => {
codeVisible.value = true;
previewTable(row.tableId).then(response => {
console.log("sadasdasdasd",response)
preview.value.data = response.data
preview.value.open = true
codeChange('template/api.go.template')
})
}
//
const cmOptions = ref({
tabSize: 4,
theme: 'material-palenight',
mode: 'text/javascript',
lineNumbers: true,
line: true
})
const extensions = [javascript(), oneDark];
const codestr = ref('')
//
const codeChange = (e) => {
if (e.indexOf('js') > -1) {
cmOptions.value.mode = 'text/javascript'
}
if (e.indexOf('model') > -1 || e.indexOf('router') > -1 || e.indexOf('api') > -1 || e.indexOf('service') > -1 || e.indexOf('dto') > -1) {
cmOptions.value.mode = 'text/x-go'
}
if (e.indexOf('vue') > -1) {
cmOptions.value.mode = 'text/x-vue'
}
codestr.value = preview.value.data[e]
}
//
const handleToProject = (row) => {
console.log("dasdasd",row)
toProjectTableCheckRole(row.tableId, false).then((response) => {
proxy.$message.success(response.msg);
}).catch(function() {})
}
//
const handleToDB = (row) => {
toDBTable(row.tableId).then((response) => {
proxy.$message.success(response.msg);
}).catch(function() {})
}
//
const handleToApiFile = (row) => {
apiToFile(row.tableId, true).then((response) => {
proxy.$message.success(response.msg);
}).catch(function() {})
}
onMounted(() => {
getListTable(pager);
});
</script>
<style lang="scss">
.action {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,11 @@
<template>
<iframe class="iframe-swagger" src="http://124.222.72.164:8000/swagger/index.html"></iframe>
</template>
<style lang="scss" scoped>
.iframe-swagger {
width: 100%;
height: calc(100vh - 90px);
border: none
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="app-container">
<a-result
status="403"
subtitle="对不起,您没有访问该资源的权限"
>
<template #extra>
<a-space>
<a-button type="primary">
返回
</a-button>
</a-space>
</template>
</a-result>
</div>
</template>
<style lang="scss" scoped>
.app-container {
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 122px);
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="app-container">
<a-result
status="404"
subtitle="抱歉,页面走丢了~"
>
<template #extra>
<a-space>
<a-button type="primary">
返回
</a-button>
</a-space>
</template>
</a-result>
</div>
</template>
<style lang="scss" scoped>
.app-container {
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 122px);
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="app-container">
<a-result
status="500"
subtitle="抱歉,服务器出了点问题~"
>
<template #extra>
<a-space>
<a-button type="primary">
返回
</a-button>
</a-space>
</template>
</a-result>
</div>
</template>
<style lang="scss" scoped>
.app-container {
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 122px);
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<div class="app-container">
<a-result
subtitle="找不到组件地址,请检查菜单: 菜单管理 > 对应菜单 > 组件管理"
>
<template #extra>
</template>
</a-result>
</div>
</template>
<style lang="scss" scoped>
.app-container {
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 122px);
}
</style>

3
src/views/index.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

290
src/views/login/index.vue Normal file
View File

@ -0,0 +1,290 @@
<template>
<div class="account">
<div class="account-container">
<div class="account-wrap-login">
<div class="login-pic">
<div>
<img
src="/public/login_left_bg.jpg"
/>
</div>
</div>
<div class="login-form" style="padding: 3rem !important">
<div class="login-form-container">
<div class="account-top">
<div class="account-top-logo">
<img :src="store.sysConfig.sys_app_logo" />
<span class="project-title">用户登录</span>
</div>
<div class="account-top-desc">横看成峰侧成岭 远近高低各不同</div>
</div>
<!-- 登录表单 -->
<a-form
:model="loginForm"
:rules="loginRules"
ref="loginFormRef"
layout="vertical"
@keyup.enter="handleLogin"
>
<a-form-item field="userName" hide-asterisk>
<a-input
v-model="loginForm.userName"
placeholder="请输入用户名"
>
<template #prefix>
<icon-user />
</template>
</a-input>
</a-form-item>
<a-form-item field="passWord" hide-asterisk>
<a-input-password
v-model="loginForm.passWord"
placeholder="请输入密码"
>
<template #prefix>
<icon-lock />
</template>
</a-input-password>
</a-form-item>
<div style="display: flex">
<div style="width: 65%">
<a-form-item field="code" hide-asterisk>
<a-input
v-model="loginForm.code"
placeholder="请输入验证码"
>
<template #prefix>
<icon-safe />
</template>
</a-input>
</a-form-item>
</div>
<div style="width: 5%"></div>
<div style="width: 20%">
<a-form-item field="code" hide-asterisk>
<img
:src="captchUrl"
class="captcha"
@click="loadCaptcha()"
/>
</a-form-item>
</div>
</div>
<a-space direction="vertical" size="medium">
<a-checkbox>记住密码</a-checkbox>
<a-button
size="large"
type="primary"
:loading="loading"
long
@click="handleLogin"
>登录</a-button
>
</a-space>
</a-form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { IconUser, IconLock, IconSafe } from '@arco-design/web-vue/es/icon';
import { login, getCaptcha } from '@/api/admin/login';
import { useUserStore } from '@/store/userInfo';
const { proxy } = getCurrentInstance();
const store = useUserStore();
// form
const loginForm = reactive({});
//
const captchUrl = ref(null);
// loading
const loading = ref(false);
// rules
const loginRules = {
userName: [{ required: true, message: '请输入用户名' }],
passWord: [{ required: true, message: '请输入密码' }],
code: [{ required: true, message: '请输入验证码' }],
};
//
const loadCaptcha = async () => {
const res = await getCaptcha();
captchUrl.value = res.data;
loginForm.uuid = res.id;
};
//
const handleLogin = () => {
loading.value = true;
// valid
proxy.$refs.loginFormRef.validate(async (valid) => {
if (!valid) {
try {
const { code, token, msg } = await login(loginForm);
if ( code == 200 ) {
await store.setToken(token);
proxy.$message.success({
content: '登陆成功',
duration: 2000,
});
setTimeout(() => {
proxy.$router.push('/admin/sys-api');
loading.value = false;
}, 500);
} else {
proxy.$message.error(`登陆失败:${msg}`);
}
} catch (err) {
//
loadCaptcha();
} finally {
loading.value = false;
}
}
});
};
onMounted(async () => {
await loadCaptcha();
});
</script>
<style lang="scss" scoped>
.captcha {
width: 100px;
height: 32px;
cursor: pointer;
}
*,
:before,
:after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
}
//
.arco-input-wrapper {
border-radius: 20px;
height: 40px;
border: 1px solid #ddd;
background: #fff;
}
//
.arco-input-wrapper:hover {
background: #fff;
border: 1px solid #1e6fff;
}
.account {
width: 100%;
margin: 0 auto;
}
.account-container {
width: 100%;
min-height: 100vh;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
padding: 15px;
background: #9053c7;
background: linear-gradient(-135deg, #c850c0, #4158d0);
}
.account-wrap-login {
width: 960px;
height: 554px;
// background: #fff;
border-radius: 10px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
// justify-content:space-between;
// padding:30px 95px 33px
}
.account-wrap-login .login-pic {
background-color: #0259e6 !important;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
width: 50%;
}
.account-wrap-login .login-pic img {
max-width: 100%;
}
.account-wrap-login .login-form {
width: 50%;
display: flex;
flex-direction: column;
background: #fff;
}
.account-wrap-login .login-form-container {
margin: auto;
width: 100%;
}
.account-wrap-login .login-form-title {
padding-bottom: 15px;
text-align: center;
}
@media (max-width: 991px) {
.account-wrap-login .login-pic {
display: none;
}
.account-wrap-login .login-form {
width: 100%;
margin: auto;
}
}
.account-wrap-login .account-top {
text-align: center;
}
.account-wrap-login .account-top-logo {
text-align: center;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.account-wrap-login .account-top-logo img {
width: 45px;
}
.account-wrap-login .account-top-logo .project-title {
background: linear-gradient(
92.06deg,
#33c2ff -17.9%,
#1e6fff 43.39%,
#1e6fff 99.4%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 24px;
line-height: 1.25;
font-weight: 500;
margin-left: 10px;
}
.account-wrap-login .account-top-desc {
font-size: 14px;
color: #808695;
margin-bottom: 20px;
}
@media (max-width: 640px) {
.account-wrap-login {
width: 100%;
padding: 30px;
height: auto;
}
}
// 20230105
:deep(.arco-input-wrapper .arco-input.arco-input-size-medium) {
box-shadow: 0 0 0px 1000px #fff inset;
}
</style>

View File

@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 48 48" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 15a1 1 0 001 1h2a1 1 0 001-1V8h7a1 1 0 001-1V5a1 1 0 00-1-1H6a2 2 0 00-2 2v9zm4 18a1 1 0 00-1-1H5a1 1 0 00-1 1v9a2 2 0 002 2h9a1 1 0 001-1v-2a1 1 0 00-1-1H8v-7zm35-17a1 1 0 001-1V6a2 2 0 00-2-2h-9a1 1 0 00-1 1v2a1 1 0 001 1h7v7a1 1 0 001 1h2zm1 17a1 1 0 00-1-1h-2a1 1 0 00-1 1v7h-7a1 1 0 00-1 1v2a1 1 0 001 1h9a2 2 0 002-2v-9zM32.835 11h-6.108c-6.512 0-11.882 4.804-12.636 11h-1.992c-.382 0-.52.046-.66.134a.855.855 0 00-.325.378c-.074.162-.114.324-.114.77v1.436c0 .446.04.608.114.77.075.163.185.291.324.378.14.088.279.134.66.134h2.157c1.179 5.706 6.315 10 12.472 10h6.108c.405 0 .552-.041.7-.12a.819.819 0 00.344-.337c.079-.145.121-.29.121-.688V31h.901c.382 0 .52-.046.66-.134a.855.855 0 00.325-.378c.074-.162.114-.324.114-.77v-1.436c0-.446-.04-.608-.114-.77a.855.855 0 00-.325-.378c-.14-.088-.278-.134-.66-.134H34v-7h.901c.382 0 .52-.046.66-.134a.855.855 0 00.325-.378c.074-.162.114-.324.114-.77v-1.436c0-.446-.04-.608-.114-.77a.855.855 0 00-.325-.378c-.14-.088-.278-.134-.66-.134H34v-3.855c0-.398-.042-.543-.121-.688a.819.819 0 00-.344-.338c-.148-.078-.295-.119-.7-.119zm-2.744 3.571h-3.637c-5.02 0-9.09 3.998-9.09 8.929s4.07 8.929 9.09 8.929h3.637V14.57z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

234
src/views/profile/index.vue Normal file
View File

@ -0,0 +1,234 @@
<template>
<a-row :gutter="8" class="dict-wrapper">
<a-col :span="8">
<a-card hoverable>
<div style="text-align: center;">
<a-space direction="vertical" fill>
<a-avatar :size="120">
<template v-if="userInfoForm.user.avatar == '' ">
<img alt="avatar" :src="store.sysConfig.sys_user_avatar" />
</template>
<template v-else>
<img alt="avatar" :src="userInfoForm.user.avatar"/>
</template>
</a-avatar>
<a-typography-title :heading="6">{{userInfoForm.user.nickName}}</a-typography-title>
</a-space>
</div>
<a-divider />
<a-card :bordered="false">
<a-descriptions title="用户详情" :column="1">
<a-descriptions-item label="用户昵称">{{userInfoForm.user.nickName}}</a-descriptions-item>
<a-descriptions-item label="登录账号">{{userInfoForm.user.username}}</a-descriptions-item>
<a-descriptions-item label="用户手机">{{userInfoForm.user.phone}}</a-descriptions-item>
<a-descriptions-item label="用户邮箱">{{userInfoForm.user.email}}</a-descriptions-item>
<a-descriptions-item label="用户性别">
<template v-if="userInfoForm.user.sex == '0' "><a-tag color="arcoblue"></a-tag></template>
<template v-else-if="userInfoForm.user.sex == '1' "><a-tag color="pinkpurple"></a-tag></template>
<template v-else><a-tag color="gold">中性</a-tag></template>
</a-descriptions-item>
<a-descriptions-item label="所属部门" v-for="item of userInfoForm.posts" >
<a-tag>{{ item.postName }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="用户角色" v-for="item of userInfoForm.roles" >
<a-tag>{{ item.roleName }}</a-tag>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-card>
</a-col>
<a-col :span="16">
<a-card hoverable>
<a-tabs>
<a-tab-pane key="1">
<template #title>
<icon-exclamation-circle /> 基础信息
</template>
<a-card hoverable :bordered="false">
<a-form :model="modalForm" :rules="rules" ref="modalFormRef" label-align="left" auto-label-width>
<a-form-item field="nickName" label="用户昵称">
<a-input v-model="modalForm.nickName" placeholder="请输入用户昵称" />
</a-form-item>
<a-form-item field="phone" label="手机号码">
<a-input v-model="modalForm.phone" placeholder="请输入手机号码" />
</a-form-item>
<a-form-item field="email" label="用户邮箱">
<a-input v-model="modalForm.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item field="sex" label="用户性别">
<a-radio-group v-model="modalForm.sex">
<a-radio value="0"></a-radio>
<a-radio value="1"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-space>
<a-button @click="handleSubmit" type="primary">保存</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</a-tab-pane>
<a-tab-pane key="2">
<template #title>
<icon-user /> 修改密码
</template>
<a-card hoverable :bordered="false">
<a-form :model="userPwdForm" :rules="rules" ref="userPwdFormRef" label-align="left" auto-label-width>
<a-form-item field="oldPassword" label="旧密码">
<a-input v-model="userPwdForm.oldPassword" placeholder="请输入旧密码" />
</a-form-item>
<a-form-item field="newPassword" label="新密码">
<a-input v-model="userPwdForm.newPassword" placeholder="请输入新密码" />
</a-form-item>
<a-form-item field="confirmPassword" label="确认密码">
<a-input v-model="userPwdForm.confirmPassword" placeholder="请输入确认密码" />
</a-form-item>
<a-form-item>
<a-space>
<a-button @click="handleSubmitUserPwd" type="primary">保存</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
</a-row>
</template>
<script setup>
import { onMounted, reactive, ref, getCurrentInstance } from 'vue';
import { useUserStore } from '@/store/userInfo';
import { updateUser } from '@/api/admin/sys-user';
import { getUserProfile,putUserPwd } from '@/api/profile/profile';
// use Store
const store = useUserStore();
//
const { proxy } = getCurrentInstance();
const userInfoForm = ref({
user: {},
posts: [],
roles: []
});
const userPwdForm = reactive({});
//
const modalForm = reactive({});
// Rules
const rules = {
nickName: [{ required: true, message: '请输入用户昵称' }],
phone: [
{ required: true, message: '请输入联系手机' },
// { match: /^1[3456789]\d{9}$/, message: ': 13011112222'}
],
email: [
{ required: true, message: '请输入邮箱' },
// { match: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/, message: ': aka@aka.com'}
],
sex: [
{ required: true, message: '请选择性别' },
// { match: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/, message: ': aka@aka.com'}
],
oldPassword: [
{ required: true, message: '输入旧密码' },
// { match: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/, message: ': aka@aka.com'}
],
newPassword: [
{ required: true, message: '输入新密码' },
// { match: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/, message: ': aka@aka.com'}
],
confirmPassword: [
{ required: true, message: '确认新密码', validator: (value, cb) => {
return new Promise(resolve => {
window.setTimeout(() => {
if (userPwdForm.newPassword !== value) {
cb('两次输入的密码不一致')
}
resolve()
}, 1)
})
} }
// { match: /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/, message: ': aka@aka.com'}
],
};
const equalToPassword = (value, callback) => {
if (modalForm.newPassword !== value) {
cb('两次输入的密码不一致')
console.log("aaa")
} else {
cb('密码输入正确')
console.log("bbb")
}
}
// handleSubmit 20220713
const handleSubmit = () => {
proxy.$refs.modalFormRef.validate(async (valid) => {
if (!valid) {
const { success } = await updateUser(modalForm);
if (success) proxy.$message.success('信息修改成功');
getCurrentUserInfo();
} else {
proxy.$message.error('表单校验失败');
}
});
};
// 20221025
const handleSubmitUserPwd = () => {
proxy.$refs.userPwdFormRef.validate(async (valid) => {
if (!valid) {
const { res } = await putUserPwd(userPwdForm);
console.loh("success",res)
if (success) proxy.$message.success('密码修改成功');
getCurrentUserInfo();
} else {
proxy.$message.error('表单校验失败');
}
});
};
const getCurrentUserInfo = async () => {
const res = await getUserProfile()
userInfoForm.value = res.data
Object.assign(modalForm, res.data.user);
};
onMounted(() => {
getCurrentUserInfo();
});
</script>
<style lang="scss">
.userinfo-row {
font-size: 14px;
line-height: 1.5;
.userinfo-item {
&-label {
width: 140px;
color: #4e5969;
text-align: right;
}
&-value {
width: 140px;
color: $primary-font-color;
}
div {
display: inline-block;
}
}
}
.userinfo-form {
width: 520px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<div class="app-container">
<a-form :model="queryForm" ref="queryFormRef" layout="inline">
<a-form-item field="jobName" label="任务名称">
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" @press-enter="handleQuery" />
</a-form-item>
<a-form-item field="jobGroup" label="任务分组">
<a-select v-model="queryForm.jobGroup" placeholder="请选择任务分组">
<a-option value="DEFAULT">默认</a-option>
<a-option value="SYSTEM">系统</a-option>
</a-select>
</a-form-item>
<a-form-item field="status" label="状态">
<a-select v-model="queryForm.status" placeholder="请选择任务状态">
<a-option :value="2">正常</a-option>
<a-option :value="1">关闭</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleQuery">查询</a-button>
<a-button @click="handleResetQuery">重置</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider />
<div class="table-action">
<a-button type="primary" @click="handleAdd">新增定时任务</a-button>
</div>
<a-table :data="tableData" :columns="columns" :pagination="{ 'show-total': true, 'show-jumper': true, 'show-page-size': true, total: pager.count, current: currentPage }">
<template #status="{ record }">
<a-tag v-if="record.status == 2" color="green">正常</a-tag>
<a-tag v-if="record.status == 1" color="red">停用</a-tag>
</template>
<template #action="{ record }">
<a-button type="text" @click="handleUpdate(record)">修改</a-button>
<a-button type="text" status="success" v-if="record.entry_id == 0" @click="handleStart(record.jobId)">启动</a-button>
<a-button type="text" status="danger" v-if="record.entry_id !== 0" @click="handleStop(record.jobId)">停止</a-button>
<a-button type="text" status="danger" @click="() => { deleteVisible = true; deleteData = [record.jobId]; }">删除</a-button>
</template>
</a-table>
<a-modal
v-model:visible="modalVisible"
title-align="start"
:title="modalTitle"
:on-before-ok="onBeforeOk"
@ok="handleOk"
@cancel="$refs.modalFormRef.resetFields()"
>
<a-form
:model="modalForm"
:rules="rules"
ref="modalFormRef"
auto-label-width
>
<a-form-item field="jobName" label="任务名称">
<a-input
v-model="modalForm.jobName"
placeholder="请输入任务名称"
></a-input>
</a-form-item>
<a-form-item field="jobGroup" label="任务分组">
<a-select v-model="modalForm.jobGroup" placeholder="请选择任务分组">
<a-option value="DEFAULT">默认</a-option>
<a-option value="SYSTEM">系统</a-option>
</a-select>
</a-form-item>
<a-form-item field="invokeTarget" label="调用目标">
<a-input
v-model="modalForm.invokeTarget"
placeholder="调用目标"
></a-input>
</a-form-item>
<a-form-item field="args" label="目标参数">
<a-input v-model="modalForm.args" placeholder="目标参iuu数"></a-input>
</a-form-item>
<a-form-item field="cronExpression" label="Cron表达式">
<a-input
v-model="modalForm.cronExpression"
placeholder="Cron表达式"
></a-input>
</a-form-item>
<a-form-item field="concurrent" label="是否并发">
<a-radio-group v-model="modalForm.concurrent" type="button">
<a-radio :value="0">允许</a-radio>
<a-radio :value="1">禁止</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="jobType" label="调用类型">
<a-radio-group v-model="modalForm.jobType" type="button">
<a-radio :value="1">接口</a-radio>
<a-radio :value="2">函数</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="misfirePolicy" label="执行策略">
<a-radio-group v-model="modalForm.misfirePolicy" type="button">
<a-radio :value="1">立即执行</a-radio>
<a-radio :value="2">执行一次</a-radio>
<a-radio :value="3">放弃执行</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="status" label="状态">
<a-select v-model="modalForm.status">
<a-option :value="2">正常</a-option>
<a-option :value="1">停用</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<!-- Akiraka 20230223 删除与批量删除 开始 -->
<DeleteModal
:data="deleteData"
:visible="deleteVisible"
:apiDelete="delSysJob"
@deleteVisibleChange="() => deleteVisible = false"
/>
<!-- Akiraka 20230223 删除与批量删除 结束 -->
</div>
</template>
<script setup>
import { reactive, ref, onMounted, getCurrentInstance, watch } from 'vue';
import { listSysJob, addSysJob, updateSysJob, delSysJob, startJob, removeJob } from '@/api/sys-job';
// Akiraka 20230210
const deleteData = ref([])
// Akiraka 20230210
const deleteVisible = ref(false)
// Akiraka 20230210
watch(() => deleteVisible.value ,(value) => {
if ( value == false ) {
getSysJobListInfo(queryForm);
}
})
const { proxy } = getCurrentInstance();
const currentPage = ref(1);
// Pager
const pager = {
count: 0,
pageIndex: 1,
pageSize: 10,
};
const queryForm = reactive({});
const modalForm = reactive({
concurrent: 1,
jobType: 1,
misfirePolicy: 1,
status: 2,
});
//
const rules = {
jobName: [{ required: true, message: '请输入任务名称' }],
jobGroup: [{ required: true, message: '请选择任务分组' }],
invokeTarget: [{ required: true, message: '请输入调用目标' }],
cronExpression: [{ required: true, message: '请输入Cron表达式' }],
status: [{ required: true, message: '请选择状态' }],
};
const modalVisible = ref(false);
const modalTitle = ref('默认标题');
const tableData = ref([]);
const columns = [
{ title: '编号', dataIndex: 'jobId' },
{ title: '任务名称', dataIndex: 'jobName' },
{ title: '任务分组', dataIndex: 'jobGroup' },
{ title: '任务表达式', dataIndex: 'cronExpression' },
{ title: '调用目标', dataIndex: 'invokeTarget' },
{ title: '状态', dataIndex: 'status', slotName: 'status' },
{ title: '操作', slotName: 'action' },
];
//
const handleQuery = () => {
getSysJobListInfo(queryForm);
}
//
const handleResetQuery = () => {
proxy.$refs.queryFormRef.resetFields();
getSysJobListInfo();
}
//
const handleAdd = () => {
modalVisible.value = true;
modalTitle.value = '新增任务';
};
//
const handleUpdate = (record) => {
modalVisible.value = true;
modalTitle.value = '修改任务';
Object.assign(modalForm, record);
};
//
const handleStart = async (jobId) => {
await startJob(jobId);
proxy.$notification.success('启动任务成功!')
getSysJobListInfo();
}
//
const handleStop = async (jobId) => {
await removeJob(jobId);
proxy.$notification.success('已停止任务!')
getSysJobListInfo();
}
// Modal oK
const onBeforeOk = (done) => {
proxy.$refs.modalFormRef.validate((err) => {
if (!err) {
return done();
} else {
proxy.$message.error('表单校验失败');
return done(false);
}
});
};
// Modal ok
const handleOk = async () => {
if (modalForm.jobId) {
const { code, msg } = await updateSysJob(modalForm);
if (code == 200 ) {
proxy.$notification.success('修改成功');
} else {
proxy.$notification.error(msg);
}
} else {
const { code, msg } = await addSysJob(modalForm);
if (code == 200 ) {
proxy.$notification.success('新增成功');
} else {
proxy.$notification.error(msg);
}
}
getSysJobListInfo();
};
//
const getSysJobListInfo = async (params = {}) => {
const { data, code, msg } = await listSysJob(params);
if ( code == 200 ) {
tableData.value = data.list;
Object.assign(pager, { count: data.count, pageIndex: data.pageIndex, pageSize: data.pageSize });
} else {
proxy.$notification.error(msg);
}
};
onMounted(() => {
getSysJobListInfo();
});
</script>
<style lang="scss" scoped>
.table-action {
margin-bottom: 12px;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<div class="card-wrapper">
<a-row :gutter="12">
<a-col :span="12">
<a-card title="服务器信息">
<div class="card-item card-item-server">
<span class="card-item-title">主机名称</span>
<span class="card-item-desc">{{ SystemInfo?.os?.hostName}}</span>
</div>
<div class="card-item card-item-server">
<span class="card-item-title">操作系统</span>
<span class="card-item-desc">{{ SystemInfo?.os?.goOs }}</span>
</div>
<div class="card-item card-item-server">
<span class="card-item-title">服务器IP</span>
<span class="card-item-desc">{{ SystemInfo?.os?.ip }}</span>
</div>
<div class="card-item card-item-server">
<span class="card-item-title">系统架构</span>
<span class="card-item-desc">{{ SystemInfo?.os?.arch }}</span>
</div>
<div class="card-item card-item-server">
<span class="card-item-title">CPU</span>
<span class="card-item-desc">{{ SystemInfo?.cpu?.cpuInfo[0]?.modelName }}</span>
</div>
<div class="card-item card-item-server">
<span class="card-item-title">当前时间</span>
<span class="card-item-desc">{{ SystemInfo?.os?.time }}</span>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card :title="SystemInfo.location">
<a-row :gutter="12">
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">系统</span>
<span class="card-item-desc">Linux</span>
</div>
</a-col>
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">时间</span>
<span class="card-item-desc">2022-05-06 15:32:22</span>
</div>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">内存</span>
<span class="card-item-desc">129MB/2349MB</span>
</div>
</a-col>
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">在线时间</span>
<span class="card-item-desc">{{ SystemInfo.bootTime }}分钟</span>
</div>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">交换</span>
<span class="card-item-desc">0/0</span>
</div>
</a-col>
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">硬盘</span>
<span class="card-item-desc">{{ SystemInfo?.disk?.used }}GB/{{ SystemInfo?.disk?.total }}GB</span>
</div>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">下载</span>
<span class="card-item-desc">{{ SystemInfo?.net?.in }}KB</span>
</div>
</a-col>
<a-col :span="12">
<div class="card-item">
<span class="card-item-title">上传</span>
<span class="card-item-desc">{{ SystemInfo?.net?.out }}KB</span>
</div>
</a-col>
</a-row>
<!-- progress -->
<div class="progress-wrapper">
<span class="progress-title">CPU</span>
<a-progress status="success" :percent="SystemInfo?.cpu?.percent / 100" />
</div>
<div class="progress-wrapper">
<span class="progress-title">RAM</span>
<a-progress status="warning" :percent="SystemInfo?.mem?.percent / 100" />
</div>
<div class="progress-wrapper">
<span class="progress-title">硬盘</span>
<a-progress :percent="SystemInfo?.disk?.percent / 100" />
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup>
import { onMounted, reactive } from 'vue';
import { getServerMonitor } from '@/api/sys-tools/monitor';
const SystemInfo = reactive({});
const getServerMonitorInfo = async () => {
const res = await getServerMonitor();
Object.assign(SystemInfo, res)
}
onMounted(() => {
getServerMonitorInfo();
});
</script>
<style lang="scss" scoped>
.card-wrapper {
width: 100%;
}
.card-item {
display: flex;
justify-content: space-between;
padding: 15px 0;
border-bottom: 1px solid rgb(229, 230, 235);
&-title {
color: $primary-font-color;
}
&-desc {
color: $secondary-font-color;
}
}
.card-item-server:last-child {
border-bottom: none;
}
.progress-wrapper {
margin-top: 15px;
display: flex;
.progress-title {
font-weight: bold;
margin-right: 10px;
width: 50px;
}
}
</style>

51
vite.config.js Normal file
View File

@ -0,0 +1,51 @@
import { join } from 'path'
import { defineConfig } from 'vite';
import { viteMockServe } from 'vite-plugin-mock';
import vue from '@vitejs/plugin-vue';
import svgLoader from 'vite-svg-loader';
import { VuetifyResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
viteMockServe({
mockPath: '/mock',
}),
svgLoader(),
viteMockServe({
mockPath: '/mock',
}),
Components({
resolvers: [VuetifyResolver()],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
}),
],
resolve: {
alias: {
'@': join(__dirname, 'src'),
}
},
server: {
host: true,
port: 1798,
//secure: false,
proxy: {
'/api': {
target: 'https://vue3.go-admin.dev',
changeOrigin: true, //开启跨域
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 引入全局scss变量
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/style/variables.scss";`
}
}
},
publicDir: '/public'
});