This commit is contained in:
2026-01-21 22:53:25 +08:00
parent 638c846aed
commit 155ec5c986
37 changed files with 3468 additions and 729 deletions

164
README.md
View File

@@ -1,44 +1,158 @@
# vueadmin
This template should help get you started developing with Vue 3 in Vite.
## 功能特性
## Recommended IDE Setup
### 1. 权限管理
- 基于角色的权限控制 (RBAC)
- 动态路由生成
- 菜单权限过滤
- 按钮级权限控制
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
### 2. 布局系统
- 响应式布局设计
- 顶部导航栏
- 侧边菜单栏
- 面包屑导航
- 标签页导航
- 设置面板
## Recommended Browser Setup
### 3. 国际化支持
- 多语言切换 (中文/英文)
- 动态加载语言包
- 支持自定义语言扩展
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
### 4. 组件库
- 自定义表格组件 (scTable)
- 自定义表单组件 (scForm)
- 自定义上传组件 (scUpload)
- 其他常用业务组件
## Customize configuration
### 5. 系统功能
- 用户登录/注册/重置密码
- 仪表盘
- 系统设置
- 用户管理
- 角色管理
- 菜单管理
- 区域管理
See [Vite Configuration Reference](https://vite.dev/config/).
## 安装和运行
## Project Setup
### 环境要求
- Node.js >= 20.19.0 || >= 22.12.0
```sh
yarn
### 安装依赖
```bash
npm install
```
### Compile and Hot-Reload for Development
### 开发模式
```sh
yarn dev
```bash
npm run dev
```
### Compile and Minify for Production
项目将在 `http://localhost:5173` 启动。
```sh
yarn build
### 构建生产版本
```bash
npm run build
```
### Lint with [ESLint](https://eslint.org/)
构建后的文件将输出到 `dist` 目录。
```sh
yarn lint
### 预览生产版本
```bash
npm run preview
```
### 代码检查和格式化
```bash
# ESLint 检查并修复
npm run lint
# Prettier 格式化代码
npm run format
```
## 配置说明
项目的主要配置文件位于 `src/config/index.js`,包含以下配置项:
```javascript
{
APP_NAME: 'vueadmin', // 应用名称
DASHBOARD_URL: '/dashboard', // 仪表盘URL
API_URL: 'https://www.tensent.cn/admin/', // API接口地址
TIMEOUT: 50000, // 请求超时时间
TOKEN_NAME: 'authorization', // Token名称
TOKEN_PREFIX: 'Bearer ', // Token前缀
LANG: 'zh-cn', // 默认语言
// 更多配置...
}
```
## 开发指南
### 路由配置
路由配置分为两部分:
1. 系统基础路由 (`src/router/systemRoutes.js`)
2. 动态生成的业务路由 (由后端菜单数据转换)
### 组件开发
自定义组件位于 `src/components/` 目录,每个组件有独立的文件夹,包含组件文件和相关资源。
### API 调用
API 接口定义位于 `src/api/` 目录,使用 Axios 封装,支持拦截器、请求缓存等功能。
### 状态管理
使用 Pinia 进行状态管理store 定义位于 `src/stores/` 目录,支持模块化管理。
## 国际化
国际化配置位于 `src/i18n/` 目录,支持中英文切换,可通过以下方式扩展其他语言:
1.`src/i18n/locales/` 目录下添加新的语言文件
2.`src/i18n/index.js` 中注册新语言
## 权限管理
系统采用基于角色的权限控制 (RBAC),权限信息由后端提供,前端根据权限信息动态生成路由和菜单。
## 浏览器支持
- Chrome (最新版本)
- Firefox (最新版本)
- Safari (最新版本)
- Edge (最新版本)
## 注意事项
1. 确保 Node.js 版本符合要求
2. 开发环境下 API 请求可能需要配置代理
3. 生产环境需要配置正确的 API 地址
4. 首次运行需要先安装依赖
## License
MIT License
## 更新日志
### v1.6.6
- 升级 Vue 3 到 3.5.26
- 升级 Vite 到 7.3.0
- 升级 Ant Design Vue 到 4.2.6
- 优化路由管理和权限控制
- 修复已知 bug
### 贡献
欢迎提交 Issue 和 Pull Request 来帮助改进这个项目!

View File

@@ -66,21 +66,3 @@ onMounted(() => {
<router-view />
</a-config-provider>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
min-height: 100vh;
}
</style>

View File

@@ -1,34 +1,34 @@
import request from '../utils/request'
import request from '@/utils/request'
/**
* 用户登录
* @returns {Promise} 菜单数据
*/
export function userLogin(params) {
return request({
url: '/auth/login',
method: 'post',
data: params
})
}
export function userLogout() {
return request({
url: '/auth/logout',
method: 'get'
})
}
export function getUserInfo() {
return request({
url: '/auth/user',
method: 'get'
})
}
export function getMyMenu(){
return request({
url: `auth/menu/my`,
method: 'get'
})
export default {
login: {
url: `auth/login`,
name: '用户登录',
post: async function (params) {
return await request.post(this.url, params)
},
},
logout: {
url: `auth/logout`,
name: '用户登出',
get: async function () {
return await request.get(this.url)
},
},
user: {
url: `auth/user`,
name: '获取用户信息',
get: async function () {
return await request.get(this.url)
},
},
menu: {
my: {
url: `auth/menu/my`,
name: '获取我的菜单',
get: async function () {
return await request.get(this.url)
},
},
},
}

View File

@@ -18,8 +18,8 @@ export default {
info: {
url: `system/index/info`,
name: '系统信息',
get: function (data) {
return request.get(this.url, data)
get: function (params) {
return request.get(this.url, { params })
},
},
setting: {
@@ -27,14 +27,14 @@ export default {
url: `system/setting/index`,
name: '获取配置信息',
get: function (params) {
return request.get(this.url, params)
return request.get(this.url, { params })
},
},
fields: {
url: `system/setting/fields`,
name: '获取配置字段',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
add: {
@@ -64,7 +64,7 @@ export default {
url: `system/dict/category`,
name: '获取字典树',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
editcate: {
@@ -92,14 +92,14 @@ export default {
url: `system/dict/lists`,
name: '字典明细',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
get: {
url: `system/dict/detail`,
name: '获取字典数据',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
edit: {
@@ -127,14 +127,14 @@ export default {
url: `system/dict/detail`,
name: '字典明细',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
alldic: {
url: `system/dict/all`,
name: '全部字典',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
},
@@ -175,7 +175,7 @@ export default {
url: `system/client/index`,
name: '客户端列表',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
add: {
@@ -204,7 +204,7 @@ export default {
url: `system/menu/index`,
name: '客户端菜单列表',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
add: {
@@ -235,14 +235,14 @@ export default {
url: `system/log/index`,
name: '日志列表',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
my: {
url: `system/log/my`,
name: '我的日志',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
delete: {
@@ -258,7 +258,7 @@ export default {
url: `system/tasks/index`,
name: '任务列表',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
delete: {
@@ -274,7 +274,7 @@ export default {
url: `system/crontab/index`,
name: '定时任务列表',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
add: {
@@ -302,7 +302,7 @@ export default {
url: `system/crontab/log`,
name: '定时任务日志',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
reload: {
@@ -318,7 +318,7 @@ export default {
url: `system/modules/index`,
name: '模块列表',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
update: {
@@ -334,7 +334,7 @@ export default {
url: `system/sms/count`,
name: '短信发送统计',
get: async function (params) {
return await request.get(this.url, params)
return await request.get(this.url, { params })
},
},
},

29
src/assets/style/app.scss Normal file
View File

@@ -0,0 +1,29 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
min-height: 100vh;
}
.pages{
flex: 1;
display: flex;
flex-direction: column;
background-color: #ffffff;
padding: 10px;
border-radius: 10px;
.search-box{
padding: 10px;
background-color: #f5f5f5;
border-radius: 10px;
}
}

View File

@@ -256,18 +256,22 @@ const tableContent = useTemplateRef('tableContent')
let scroll = ref({
scrollToFirstRowOnChange: true,
x: 'max-content',
y: 100,
y: true,
})
onMounted(() => {
let tableHeight = 100
updateTableHeight()
})
const updateTableHeight = () => {
let tableHeight = 0
if (props.pagination !== false) {
tableHeight = tableContent.value.clientHeight - 100
tableHeight = tableContent.value.clientHeight - 105
} else {
tableHeight = tableContent.value.clientHeight - 65
}
scroll.value.y = tableHeight
})
}
// 根据表格宽度优化横向滚动配置
watch(

View File

@@ -344,6 +344,8 @@ onMounted(() => {
overflow-y: auto;
flex: 1;
height: calc(100vh - 106px);
display: flex;
flex-direction: column;
}
/* 默认布局 - 双栏菜单 */

View File

@@ -2,6 +2,7 @@ import { createApp } from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import '@/assets/style/app.scss'
import App from './App.vue'
import router from './router'
import pinia from './stores'

View File

@@ -0,0 +1,155 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button>
<el-button type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button>
</div>
<div class="right-panel">
<div class="right-panel-search">
<el-input v-model="search.keyword" placeholder="部门名称" clearable></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</div>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="apiObj" row-key="id" :params="search" @selection-change="selectionChange" hidePagination>
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column label="#" type="index" width="50"></el-table-column>
<el-table-column label="部门名称" prop="title"></el-table-column>
<el-table-column label="部门标识" prop="name" width="150"></el-table-column>
<el-table-column label="排序" prop="sort" width="150"></el-table-column>
<el-table-column label="操作" fixed="right" align="right" width="220">
<template #default="scope">
<el-button-group>
<el-button type="success" @click="table_show(scope.row, scope.$index)">查看</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="primary" @click="table_edit(scope.row, scope.$index)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</el-table-column>
</scTable>
</el-main>
</el-container>
<save-dialog v-if="dialog.save" ref="saveDialog" @success="handleSaveSuccess" @closed="dialog.save=false"></save-dialog>
</template>
<script>
import saveDialog from './save'
export default {
name: 'auth.department',
components: {
saveDialog
},
data() {
return {
dialog: {
save: false,
permission: false
},
apiObj: this.$API.auth.department.list,
selection: [],
search: {
is_tree: 1,
keyword: null
}
}
},
methods: {
//添加
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open()
})
},
//编辑
table_edit(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('edit').setData(row)
})
},
//查看
table_show(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('show').setData(row)
})
},
//删除
async table_del(row){
var reqData = {id: row.id}
var res = await this.$API.auth.department.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
},
//批量删除
async batch_del(){
this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
type: 'warning'
}).then(async () => {
const loading = this.$loading();
let ids = this.selection.map(item => item.id)
var reqData = {ids: ids}
var res = await this.$API.auth.department.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
loading.close();
}else{
this.$message.error(res.message)
}
}).catch(() => {
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
},
//搜索
upsearch(){
this.$refs.table.upData(this.search)
},
//根据ID获取树结构
filterTree(id){
var target = null;
function filter(tree){
tree.forEach(item => {
if(item.id == id){
target = item
}
if(item.children){
filter(item.children)
}
})
}
filter(this.$refs.table.tableData)
return target
},
//本地更新数据
handleSaveSuccess(data, mode){
if(mode=='add'){
this.$refs.table.refresh()
}else if(mode=='edit'){
this.$refs.table.refresh()
}
}
}
}
</script>

View File

@@ -0,0 +1,103 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close :close-on-click-modal="false" @closed="$emit('closed')">
<el-form :model="form" :rules="rules" :disabled="mode=='show'" ref="dialogForm" label-width="100px" label-position="left">
<el-form-item label="上级部门" prop="parent_id">
<el-cascader v-model="form.parent_id" :options="groups" :props="groupsProps" :show-all-levels="false" clearable style="width: 100%;"></el-cascader>
</el-form-item>
<el-form-item label="部门名称" prop="title">
<el-input v-model="form.title" clearable></el-input>
</el-form-item>
<el-form-item label="部门别名" prop="name">
<el-input v-model="form.name" clearable></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right" :min="1" style="width: 100%;"></el-input-number>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button v-if="mode!='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data() {
return {
mode: "add",
titleMap: {
add: '新增',
edit: '编辑',
show: '查看'
},
visible: false,
isSaveing: false,
//表单数据
form: {id:"",title: "",name: "",sort: 1,parent_id: ""},
//验证规则
rules: {
sort: [{required: true, message: '请输入排序', trigger: 'change'}],
title: [{required: true, message: '请输入角色名称'}],
name: [{required: true, message: '请输入角色别名'}]
},
//所需数据选项
groups: [],
groupsProps: {
value: "id",
label: "title",
emitPath: false,
checkStrictly: true
}
}
},
mounted() {
this.getGroup()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this
},
//加载树数据
async getGroup(){
var res = await this.$API.auth.department.list.get({is_tree: 1});
this.groups = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
var res = {}
if(this.mode == 'add'){
res = await this.$API.auth.department.add.post(this.form);
}else{
res = await this.$API.auth.department.edit.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
}
})
},
//表单注入数据
setData(data){
this.form.id = data.id
this.form.title = data.title
this.form.name = data.name
this.form.sort = data.sort
this.form.parent_id = data.parent_id
}
}
}
</script>

View File

@@ -0,0 +1,170 @@
<template>
<el-container>
<el-aside width="300px" v-loading="menuloading">
<el-container>
<el-header>
<el-input placeholder="输入关键字进行过滤" v-model="menuFilterText" clearable></el-input>
</el-header>
<el-main class="nopadding">
<el-tree ref="menu" class="menu" node-key="id" :data="menuList" :props="menuProps" draggable highlight-current :expand-on-click-node="false" check-strictly show-checkbox :filter-node-method="menuFilterNode" @node-click="menuClick" @node-drop="nodeDrop">
<template #default="{node, data}">
<span class="custom-tree-node el-tree-node__label">
<span class="label">
{{ node.label }}
</span>
<span class="do">
<el-icon @click.stop="add(node, data)"><el-icon-plus /></el-icon>
</span>
</span>
</template>
</el-tree>
</el-main>
<el-footer style="height:51px;">
<el-button type="primary" icon="el-icon-plus" @click="add()"></el-button>
<el-button type="danger" plain icon="el-icon-delete" @click="delMenu"></el-button>
</el-footer>
</el-container>
</el-aside>
<el-container>
<el-main class="nopadding" style="padding:20px;" ref="main">
<save ref="save" :menu="menuList"></save>
</el-main>
</el-container>
</el-container>
</template>
<script>
let newMenuIndex = 1;
import save from './save'
export default {
name: "auth.permission",
components: {
save
},
data(){
return {
menuloading: false,
menuList: [],
menuProps: {
label: (data)=>{
return data.title
}
},
menuFilterText: ""
}
},
watch: {
menuFilterText(val){
this.$refs.menu.filter(val);
}
},
mounted() {
this.getMenu();
},
methods: {
//加载树数据
async getMenu(){
this.menuloading = true
var res = await this.$API.auth.menu.list.get({is_tree: 1});
if(res.code == 1){
this.menuloading = false
this.menuList = res.data;
}
},
//树点击
menuClick(data, node){
var pid = node.level==1?undefined:node.parent.data.id;
this.$refs.save.setData(data, pid)
this.$refs.main.$el.scrollTop = 0
},
//树过滤
menuFilterNode(value, data){
if (!value) return true;
var targetText = data.title;
return targetText.indexOf(value) !== -1;
},
//树拖拽
nodeDrop(draggingNode, dropNode, dropType){
if(dropType == 'before'){
}else if(dropType == 'after'){
}else if(dropType == 'inner'){
}
},
//增加
async add(node, data){
var newMenuName = "新菜单" + newMenuIndex++;
var newMenuData = {
parent_id: data ? data.id : 0,
name: newMenuName,
path: "",
component: "",
title: newMenuName,
type: "menu",
sort: 0
}
this.menuloading = true
var res = await this.$API.auth.menu.add.post(newMenuData)
this.menuloading = false
newMenuData.id = res.data.id
this.$refs.menu.append(newMenuData, node)
this.$refs.menu.setCurrentKey(newMenuData.id)
var pid = node ? node.data.id : ""
this.$refs.save.setData(newMenuData, pid)
},
//删除菜单
async delMenu(){
var CheckedNodes = this.$refs.menu.getCheckedNodes()
if(CheckedNodes.length == 0){
this.$message.warning("请选择需要删除的项")
return false;
}
var confirm = await this.$confirm('确认删除已选择的菜单吗?','提示', {
type: 'warning',
confirmButtonText: '删除',
confirmButtonClass: 'el-button--danger'
}).catch(() => {})
if(confirm != 'confirm'){
return false
}
this.menuloading = true
var reqData = {
ids: CheckedNodes.map(item => item.id)
}
var res = await this.$API.auth.menu.delete.post(reqData)
this.menuloading = false
if(res.code == 1){
CheckedNodes.forEach(item => {
var node = this.$refs.menu.getNode(item)
if(node.isCurrent){
this.$refs.save.setData({})
}
this.$refs.menu.remove(item)
})
}else{
this.$message.warning(res.msg)
}
}
}
}
</script>
<style scoped>
.custom-tree-node {display: flex;flex: 1;align-items: center;justify-content: space-between;font-size: 14px;padding-right: 24px;height:100%;}
.custom-tree-node .label {display: flex;align-items: center;;height: 100%;}
.custom-tree-node .label .el-tag {margin-left: 5px;}
.custom-tree-node .do {display: none;}
.custom-tree-node .do i {margin-left:5px;color: #999;}
.custom-tree-node .do i:hover {color: #333;}
.custom-tree-node:hover .do {display: inline-block;}
</style>

View File

@@ -0,0 +1,209 @@
<template>
<el-row :gutter="40">
<el-col v-if="!form.id">
<el-empty description="请选择左侧菜单后操作" :image-size="100"></el-empty>
</el-col>
<template v-else>
<el-col :lg="12">
<h2>{{form.title || "新增菜单"}}</h2>
<el-form :model="form" :rules="rules" ref="dialogForm" label-width="80px" label-position="left">
<el-form-item label="显示名称" prop="title">
<el-input v-model="form.title" clearable placeholder="菜单显示名字"></el-input>
</el-form-item>
<el-form-item label="上级菜单" prop="parentId">
<el-cascader v-model="form.parentId" :options="menuOptions" :props="menuProps" :show-all-levels="false" placeholder="顶级菜单" clearable disabled></el-cascader>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio-button value="menu">菜单</el-radio-button>
<el-radio-button value="iframe">Iframe</el-radio-button>
<el-radio-button value="link">外链</el-radio-button>
<el-radio-button value="button">按钮</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="别名" prop="name">
<el-input v-model="form.name" clearable placeholder="菜单别名"></el-input>
<div class="el-form-item-msg">系统唯一且与内置组件名一致否则导致缓存失效如类型为Iframe的菜单别名将代替源地址显示在地址栏</div>
</el-form-item>
<el-form-item label="菜单图标" prop="icon">
<sc-icon-select v-model="form.icon" clearable></sc-icon-select>
</el-form-item>
<el-form-item label="路由地址" prop="path">
<el-input v-model="form.path" clearable placeholder=""></el-input>
</el-form-item>
<el-form-item label="重定向" prop="redirect">
<el-input v-model="form.redirect" clearable placeholder=""></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input type="number" v-model="form.sort" class="mx-4" :min="0" :max="100" controls-position="right" clearable />
</el-form-item>
<el-form-item label="菜单高亮" prop="active">
<el-input v-model="form.active" clearable placeholder=""></el-input>
<div class="el-form-item-msg">子节点或详情页需要高亮的上级菜单路由地址</div>
</el-form-item>
<el-form-item label="视图" prop="component">
<el-input v-model="form.component" clearable placeholder="">
<template #prepend>pages/</template>
</el-input>
<div class="el-form-item-msg">如父节点链接或Iframe等没有视图的菜单不需要填写</div>
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-color-picker v-model="form.color" :predefine="predefineColors"></el-color-picker>
</el-form-item>
<el-form-item label="是否隐藏" prop="hidden">
<el-checkbox v-model="form.hidden">隐藏菜单</el-checkbox>
<el-checkbox v-model="form.hiddenBreadcrumb">隐藏面包屑</el-checkbox>
<div class="el-form-item-msg">菜单不显示在导航中但用户依然可以访问例如详情页</div>
</el-form-item>
<el-form-item label="是否固定" prop="affix">
<el-switch v-model="form.affix" />
<div class="el-form-item-msg">是否固定类似首页控制台在标签中是没有关闭按钮的</div>
</el-form-item>
<el-form-item label="是否全屏" prop="fullpage">
<el-switch v-model="form.fullpage" />
<div class="el-form-item-msg">是否全屏</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="save" :loading="loading"> </el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :lg="12" class="apilist">
<h2>接口权限</h2>
<sc-form-table v-model="form.apiList" :addTemplate="apiListAddTemplate" placeholder="暂无匹配接口权限">
<el-table-column prop="code" label="标识" width="150">
<template #default="scope">
<el-input v-model="scope.row.code" placeholder="请输入内容"></el-input>
</template>
</el-table-column>
<el-table-column prop="url" label="Api url">
<template #default="scope">
<el-input v-model="scope.row.url" placeholder="请输入内容"></el-input>
</template>
</el-table-column>
</sc-form-table>
</el-col>
</template>
</el-row>
</template>
<script>
import scIconSelect from '@/components/scIconSelect'
export default {
components: {
scIconSelect
},
props: {
menu: { type: Object, default: () => {} },
},
data(){
return {
form: {
id: "",
parentId: "",
name: "",
path: "",
component: "",
redirect: "",
sort: 0,
title: "",
icon: "",
active: "",
color: "",
type: "menu",
affix: false,
apiList: []
},
menuOptions: [],
menuProps: {
value: 'id',
label: 'title',
checkStrictly: true
},
predefineColors: [
'#ff4500',
'#ff8c00',
'#ffd700',
'#67C23A',
'#00ced1',
'#409EFF',
'#c71585'
],
rules: [],
apiListAddTemplate: {
code: "",
url: ""
},
loading: false
}
},
watch: {
menu: {
handler(){
this.menuOptions = this.treeToMap(this.menu)
},
deep: true
}
},
mounted() {
},
methods: {
//简单化菜单
treeToMap(tree){
const map = []
tree.forEach(item => {
var obj = {
id: item.id,
parentId: item.parentId,
title: item.title,
children: item.children&&item.children.length>0 ? this.treeToMap(item.children) : null
}
map.push(obj)
})
return map
},
//保存
async save(){
this.loading = true
let res = {};
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
if(this.form.id){
res = await this.$API.auth.menu.edit.post(this.form)
}else{
res = await this.$API.auth.menu.add.post(this.form)
}
this.loading = false
if(res.code == 1){
this.$message.success("保存成功")
// this.$TOOL.data.set("MENU", res.data)
}else{
this.$message.error(res.message)
}
},
//表单注入数据
setData(data, pid){
this.form = data
this.form.hidden = this.form.hidden == 1 ? true : false;
this.form.hiddenBreadcrumb = this.form.hiddenBreadcrumb == 1 ? true : false;
this.form.affix = this.form.affix == 1 ? true : false;
this.form.fullpage = this.form.fullpage == 1 ? true : false;
this.form.apiList = data.apiList || []
this.form.sort = data.sort + ""
this.form.parent_id = data.parent_id || pid
}
}
}
</script>
<style scoped>
h2 {font-size: 17px;color: #3c4a54;padding:0 0 30px 0;}
.apilist {border-left: 1px solid #eee;}
[data-theme="dark"] h2 {color: #fff;}
[data-theme="dark"] .apilist {border-color: #434343;}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button>
<el-button type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button>
<el-button type="primary" plain :disabled="selection.length!=1" @click="permission">权限设置</el-button>
</div>
<div class="right-panel">
<div class="right-panel-search">
<el-input v-model="search.keyword" placeholder="角色名称" clearable></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</div>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="apiObj" row-key="id" @selection-change="selectionChange" :params="search">
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column label="ID" prop="id" width="50"></el-table-column>
<el-table-column label="角色名称" prop="title"></el-table-column>
<el-table-column label="别名" prop="name" width="150"></el-table-column>
<el-table-column label="状态" prop="status" width="150">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'danger'">{{ scope.row.status == 1 ? '正常' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" align="right" width="180">
<template #default="scope">
<el-button-group>
<el-button type="success" @click="table_show(scope.row, scope.$index)">查看</el-button>
<el-button type="primary" @click="table_edit(scope.row, scope.$index)">编辑</el-button>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</el-table-column>
</scTable>
</el-main>
</el-container>
<save-dialog v-if="dialog.save" ref="saveDialog" @success="handleSaveSuccess" @closed="dialog.save=false"></save-dialog>
<permission-dialog v-if="dialog.permission" ref="permissionDialog" @closed="dialog.permission=false" @success="permissionSuccess"></permission-dialog>
</template>
<script>
import saveDialog from './save'
import permissionDialog from './permission'
export default {
name: 'auth.role',
components: {
saveDialog,
permissionDialog
},
data() {
return {
dialog: {
save: false,
permission: false
},
apiObj: this.$API.auth.role.list,
selection: [],
search: {keyword: null}
}
},
methods: {
//添加
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open()
})
},
//编辑
table_edit(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('edit').setData(row)
})
},
//查看
table_show(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('show').setData(row)
})
},
//权限设置
permission(){
if(this.selection.length != 1){
this.$message.error("请选择要设置的角色")
return
}
this.dialog.permission = true
this.$nextTick(() => {
this.$refs.permissionDialog.open().setData(this.selection[0])
})
},
//删除
async table_del(row){
var reqData = {id: row.id}
var res = await this.$API.auth.role.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
}else{
this.$message.error(res.message)
}
},
//批量删除
async batch_del(){
this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
type: 'warning'
}).then(async () => {
var ids = this.selection.map(item => item.id)
var reqData = {ids: ids}
var res = await this.$API.auth.role.delete.post(reqData);
if(res.code == 1){
const loading = this.$loading();
this.$refs.table.refresh()
loading.close();
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
}).catch(() => {
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
},
//搜索
upsearch(){
this.$refs.table.upData(this.search)
},
//根据ID获取树结构
filterTree(id){
var target = null;
function filter(tree){
tree.forEach(item => {
if(item.id == id){
target = item
}
if(item.children){
filter(item.children)
}
})
}
filter(this.$refs.table.tableData)
return target
},
//本地更新数据
handleSaveSuccess(data, mode){
if(mode=='add'){
this.$refs.table.refresh()
}else if(mode=='edit'){
this.$refs.table.refresh()
}
},
permissionSuccess(){
this.$refs.table.refresh()
}
}
}
</script>

View File

@@ -0,0 +1,129 @@
<template>
<el-dialog title="角色权限设置" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
<el-tabs tab-position="top">
<el-tab-pane label="菜单权限">
<div class="treeMain">
<el-tree ref="menu" node-key="id" :data="menu.list" :default-checked-keys="menu.checked" :props="menu.props" check-strictly show-checkbox></el-tree>
</div>
</el-tab-pane>
<el-tab-pane label="数据权限">
<el-form label-width="100px" label-position="left">
<el-form-item label="数据权限">
<sc-select v-model="form.data_range" dic="data_auth" style="width: 80%;"></sc-select>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="控制台">
<el-form label-width="100px" label-position="left">
<el-form-item label="控制台视图">
<el-select v-model="form.dashboard" placeholder="请选择">
<el-option v-for="item in dashboardOptions" :key="item.value" :label="item.label" :value="item.value">
<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: #8492a6; font-size: 12px">{{ item.views }}</span>
</el-option>
</el-select>
<div class="el-form-item-msg">用于控制角色登录后控制台的视图</div>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data() {
return {
visible: false,
isSaveing: false,
menu: {
list: [],
checked: [],
props: {
label: (data)=>{
return data.title
}
}
},
group: {
list: [],
checked: [],
props: {}
},
type: {
list: [],
checked: [],
props: {}
},
form: {
role_id: 0,
auth: [],
data_range: "",
dashboard: "work",
},
dashboardOptions: [
{
value: '0',
label: '数据统计',
views: 'stats'
},
{
value: '1',
label: '工作台',
views: 'work'
},
]
}
},
mounted() {
this.getMenu();
},
methods: {
open(){
this.visible = true;
return this;
},
async submit(){
let authSelect = this.$refs.menu.getCheckedNodes();
this.form.permissions = [];
authSelect.map(item => {
this.form.permissions.push(item.id);
})
this.isSaveing = true;
var res = await this.$API.auth.role.auth.post(this.form);
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
},
async getMenu(){
var res = await this.$API.auth.menu.list.get({is_tree: 1});
this.menu.list = res.data;
},
//表单注入数据
setData(data){
this.form.id = data.id;
this.form.data_range = data.data_range;
this.form.dashboard = data.dashboard;
// this.form.mobile_module = data.mobile_module;
data.permissions.map(item => {
this.menu.checked.push(item.id);
})
}
}
}
</script>
<style scoped>
.treeMain {height:280px;overflow: auto;border: 1px solid #dcdfe6;margin-bottom: 10px;}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
<el-form :model="form" :rules="rules" :disabled="mode=='show'" ref="dialogForm" label-width="100px" label-position="left">
<el-form-item label="上级角色" prop="parent_id" v-if="false">
<el-cascader v-model="form.parent_id" :options="groups" :props="groupsProps" :show-all-levels="false" clearable style="width: 100%;"></el-cascader>
</el-form-item>
<el-form-item label="角色名称" prop="title">
<el-input v-model="form.title" clearable></el-input>
</el-form-item>
<el-form-item label="角色别名" prop="name">
<el-input v-model="form.name" clearable></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right" :min="1" style="width: 100%;"></el-input-number>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button v-if="mode!='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data() {
return {
mode: "add",
titleMap: {
add: '新增',
edit: '编辑',
show: '查看'
},
visible: false,
isSaveing: false,
//表单数据
form: {
id:"",
title: "",
name: "",
sort: 1,
parent_id: 0
},
//验证规则
rules: {
sort: [
{required: true, message: '请输入排序', trigger: 'change'}
],
label: [
{required: true, message: '请输入角色名称'}
],
alias: [
{required: true, message: '请输入角色别名'}
]
},
//所需数据选项
groups: [],
groupsProps: {
value: "id",
label: "title",
emitPath: false,
checkStrictly: true
}
}
},
mounted() {
this.getGroup()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this
},
//加载树数据
async getGroup(){
var res = await this.$API.auth.role.list.get({is_tree: 1});
this.groups = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
var res = {}
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
if(this.mode == 'add'){
res = await this.$API.auth.role.add.post(this.form);
}else{
res = await this.$API.auth.role.edit.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
}
})
},
//表单注入数据
setData(data){
this.form.id = data.id
this.form.title = data.title
this.form.name = data.name
this.form.sort = data.sort
// this.form.parent_id = data.parent_id
//可以和上面一样单个注入,也可以像下面一样直接合并进去
//Object.assign(this.form, data)
}
}
}
</script>
<style>
</style>

View File

@@ -1,180 +1,218 @@
<template>
<div class="system-user">
<a-card title="用户管理">
<template #extra>
<a-button type="primary" @click="handleAdd">
<PlusOutlined />
新增用户
</a-button>
</template>
<el-container>
<el-aside width="200px" v-loading="showGrouploading">
<el-container>
<el-header>
<el-input placeholder="输入关键字进行过滤" v-model="groupFilterText" clearable></el-input>
</el-header>
<el-main class="nopadding">
<el-tree ref="group" class="menu" node-key="id" :data="group" :props="{label: 'title'}" :current-node-key="''" :highlight-current="true" :expand-on-click-node="false" :filter-node-method="groupFilterNode" @node-click="groupClick"></el-tree>
</el-main>
</el-container>
</el-aside>
<el-container>
<el-header>
<div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button>
<el-button type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button>
<el-button type="primary" plain :disabled="selection.length!=1" @click="roleSet">分配角色</el-button>
</div>
<div class="right-panel">
<div class="right-panel-search">
<el-input v-model="search.name" placeholder="登录账号 / 姓名" clearable></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</div>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="list.apiObj" :column="list.column" @selection-change="selectionChange" stripe remoteSort remoteFilter>
<el-table-column type="selection" width="50"></el-table-column>
<template #username="scope">
<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<el-avatar :src="scope.row.avatar" shape="square" :size="50"></el-avatar>
<div style="display: flex; flex-direction: column;">
<span>{{ scope.row.username }}</span>
<span>{{ scope.row.nickname }}</span>
</div>
</div>
</template>
<template #roleName="scope">
<el-tag v-for="item in scope.row?.roles" :key="item.id">{{item.title}}</el-tag>
</template>
<template #department_name="scope">
{{scope.row.department?.title}}
</template>
<template #operation="scope">
<el-button-group>
<el-button type="success" @click="table_show(scope.row, scope.$index)">查看</el-button>
<el-button type="primary" @click="table_edit(scope.row, scope.$index)">编辑</el-button>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</scTable>
</el-main>
</el-container>
</el-container>
<div class="search-form">
<sc-form :form-items="formItems" :initial-values="searchForm" :show-actions="true" submit-text="查询"
reset-text="重置" @finish="handleSearch" @reset="handleReset" layout="inline" />
</div>
<save-dialog v-if="dialog.save" ref="saveDialog" @success="handleSuccess" @closed="dialog.save=false"></save-dialog>
<role-dialog v-if="dialog.role" ref="roleDialog" @success="handleSuccess" @closed="dialog.role=false"></role-dialog>
<sc-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination"
:show-action-column="true" :action-column="{
title: '操作',
key: 'action',
width: 150,
}">
<template #status="{ record }">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</sc-table>
</a-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import ScTable from '@/components/scTable/index.vue'
import ScForm from '@/components/scForm/index.vue'
<script>
import saveDialog from './save'
import roleDialog from './role'
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
export default {
name: 'auth.user',
components: {
saveDialog,
roleDialog
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
data() {
return {
dialog: {
save: false
},
showGrouploading: false,
groupFilterText: '',
group: [],
list: {
apiObj: this.$API.auth.users.list,
column: [
{prop: 'uid', label: 'UID', width: '80'},
{prop: 'username', label: '登录账号', width: '220'},
{prop: 'mobile', label: '手机号码', width: '120'},
{prop: 'email', label: '邮箱', width: '160'},
{prop: 'department_name', label: '所属部门', width: '120'},
{prop: 'roleName', label: '所属角色', width:'120'},
{prop: 'created_at', label: '加入时间', width:'180'},
{prop: 'last_login_at', label: '上次登录时间', width:'180'},
{prop: 'operation', label: '操作', width:'160', fixed: 'right'}
]
},
selection: [],
search: {
name: null
}
}
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
watch: {
groupFilterText(val) {
this.$refs.group.filter(val);
}
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
mounted() {
this.getGroup()
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
]
methods: {
//添加
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open()
})
},
//编辑
table_edit(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('edit').setData(row)
})
},
//查看
table_show(row){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('show').setData(row)
})
},
//删除
async table_del(row, index){
var reqData = {id: row.uid}
var res = await this.$API.auth.users.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
}else{
this.$message.error(res.message)
}
},
//批量删除
async batch_del(){
this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?`, '提示', {
type: 'warning'
}).then(async () => {
var ids = this.selection.map(item => item.uid)
var reqData = {ids: ids}
var res = await this.$API.auth.users.delete.post(reqData);
if(res.code == 1){
this.$refs.table.refresh()
this.$message.success("删除成功")
}else{
this.$message.error(res.message)
}
}).catch(() => {
// 搜索表单配置
const formItems = [
{
field: 'username',
label: '用户名',
type: 'input',
placeholder: '请输入用户名',
allowClear: true,
},
{
field: 'status',
label: '状态',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '全部', value: '' },
{ label: '正常', value: 1 },
{ label: '禁用', value: 0 },
],
allowClear: true,
style: 'width: 120px',
},
]
const searchForm = ref({
username: '',
status: '',
})
const dataSource = ref([])
const loading = ref(false)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
})
// 模拟数据
const mockData = [
{
id: 1,
username: 'admin',
nickname: '管理员',
status: 1,
createTime: '2024-01-01 10:00:00',
},
{
id: 2,
username: 'user',
nickname: '普通用户',
status: 1,
createTime: '2024-01-02 10:00:00',
},
]
const loadData = () => {
loading.value = true
// 模拟接口请求
setTimeout(() => {
dataSource.value = mockData
pagination.value.total = mockData.length
loading.value = false
}, 500)
}
const handleSearch = () => {
loadData()
}
const handleReset = () => {
searchForm.value = {
username: '',
status: '',
})
},
//权限设置
roleSet(){
if(this.selection.length != 1){
this.$message.error("请选择一条数据")
return false;
}
this.dialog.role = true
this.$nextTick(() => {
this.$refs.roleDialog.open().setData(this.selection[0])
})
},
insertData(){
this.dialog.insert = true
this.$nextTick(() => {
this.$refs.insertDialog.open()
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
},
//加载树数据
async getGroup(){
this.showGrouploading = true;
var res = await this.$API.auth.department.list.get({is_tree: 1});
this.showGrouploading = false;
var allNode ={id: '', title: '所有'}
res.data.unshift(allNode);
this.group = res.data;
},
//树过滤
groupFilterNode(value, data){
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
//树点击事件
groupClick(data){
var params = {
department_id: data.id
}
this.$refs.table.reload(params)
},
//搜索
upsearch(){
this.$refs.table.upData(this.search)
},
//本地更新数据
handleSuccess(){
this.$refs.table.refresh()
}
}
loadData()
}
const handleAdd = () => {
message.info('新增用户功能开发中')
}
const handleEdit = (record) => {
message.info('编辑用户功能开发中')
}
const handleDelete = (record) => {
message.info('删除用户功能开发中')
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.system-user {
.search-form {
margin-bottom: 16px;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
}
<style>
</style>

View File

@@ -0,0 +1,78 @@
<template>
<el-dialog title="角色设置" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
<el-tabs tab-position="top">
<el-tab-pane label="角色选择">
<div class="treeMain">
<el-tree ref="role" node-key="id" :data="role.list" :default-checked-keys="role.checked" :props="role.props" check-strictly default-expand-all show-checkbox></el-tree>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
data(){
return {
visible: false,
isSaveing: false,
role: {
list: [],
checked: [],
props: {
label: (data)=>{
return data.title
}
}
},
form: {uid: '',roles: []}
}
},
mounted(){
this.getRole();
},
methods:{
open(){
this.visible = true;
return this;
},
async submit(){
this.isSaveing = true;
let roles = this.$refs.role.getCheckedNodes();
this.form.roles = [];
roles.map(item => {
this.form.roles.push(item.id);
})
this.isSaveing = true;
var res = await this.$API.auth.users.uprole.post(this.form);
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$message.error(res.message)
}
},
async getRole(){
let res = await this.$API.auth.role.list.get({is_tree: 1});
this.role.list = res.data;
},
setData(data){
data.roles.map(item => {
this.role.checked.push(item.id)
})
this.form.uid = data.uid;
}
}
}
</script>
<style scoped>
.treeMain {height:280px;overflow: auto;border: 1px solid #dcdfe6;margin-bottom: 10px;}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close :close-on-click-modal="false" @closed="$emit('closed')">
<el-form :model="form" :rules="rules" :disabled="mode=='show'" ref="dialogForm" label-width="100px" label-position="left">
<el-form-item label="头像" prop="avatar">
<sc-upload v-model="form.avatar" :cropper="true" :aspectRatio="1" title="上传头像"></sc-upload>
</el-form-item>
<el-form-item label="登录账号" prop="username">
<el-input v-model="form.username" placeholder="用于登录系统" clearable></el-input>
</el-form-item>
<el-form-item label="姓名" prop="nickname">
<el-input v-model="form.nickname" placeholder="请输入完整的真实姓名" clearable></el-input>
</el-form-item>
<template v-if="mode=='add'">
<el-form-item label="登录密码" prop="password">
<el-input type="password" v-model="form.password" clearable show-password></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="password2">
<el-input type="password" v-model="form.password2" clearable show-password></el-input>
</el-form-item>
</template>
<el-form-item label="所属部门" prop="group">
<el-tree-select v-model="form.department_id" :data="department" :props="departmentProps" placeholder="请选择部门" />
</el-form-item>
<el-form-item label="所属角色" prop="roles">
<el-tree-select v-model="form.roles" :data="groups" :props="groupsProps" multiple placeholder="请选择角色" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button v-if="mode!='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data() {
return {
mode: "add",
titleMap: {
add: '新增用户',
edit: '编辑用户',
show: '查看'
},
visible: false,
isSaveing: false,
//表单数据
form: { username: "", avatar: "", department_id: 0, roles: []},
//验证规则
rules: {
// avatar:[
// {required: true, message: '请上传头像'}
// ],
username: [{required: true, message: '请输入登录账号'}],
nickname: [{required: true, message: '请输入真实姓名'}],
password: [
{required: true, message: '请输入登录密码'},
{validator: (rule, value, callback) => {
if (this.form.password2 !== '') {
this.$refs.dialogForm.validateField('password2');
}
callback();
}}
],
password2: [
{required: true, message: '请再次输入密码'},
{validator: (rule, value, callback) => {
if (value !== this.form.password) {
callback(new Error('两次输入密码不一致!'));
}else{
callback();
}
}}
]
},
//所需数据选项
department: [],
departmentProps: {
value: "id",
label: "title",
multiple: false,
checkStrictly: true
},
groups: [],
groupsProps: {
value: "id",
label: "title",
multiple: false,
checkStrictly: true
}
}
},
mounted() {
this.getGroup()
this.getDepartment()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this
},
//加载树数据
async getGroup(){
var res = await this.$API.auth.role.list.get({is_tree: 1});
this.groups = res.data;
},
async getDepartment(){
var res = await this.$API.auth.department.list.get({is_tree: 1});
this.department = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
var res = {};
if(this.mode == 'add'){
res = await this.$API.auth.users.add.post(this.form);
}else{
res = await this.$API.auth.users.edit.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
}else{
return false;
}
})
},
//表单注入数据
setData(data){
this.form.uid = data.uid
this.form.username = data.username
this.form.avatar = data.avatar
this.form.nickname = data.nickname
this.form.department_id = data.department_id
data.roles.map(item => {
this.form.roles.push(item.id)
})
}
}
}
</script>
<style>
</style>

View File

@@ -51,7 +51,7 @@ import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import { userLogin, getUserInfo, getMyMenu } from '@/api/auth'
import authApi from '@/api/auth'
// 定义组件名称(多词命名)
defineOptions({
@@ -72,14 +72,14 @@ const handleLogin = async () => {
loading.value = true;
try {
let res = await userLogin({ username: formState.username, password: formState.password })
let res = await authApi.login.post({ username: formState.username, password: formState.password })
if (res.code == 1) {
userStore.setToken(res.data.access_token)
let userInfo = await getUserInfo()
let userInfo = await authApi.user.get()
if (userInfo.code == 1) {
userStore.setUserInfo(userInfo.data)
}
let authData = await getMyMenu()
let authData = await authApi.menu.my.get()
if (authData.code == 1){
userStore.setMenu(authData.data.menu)
userStore.setPermissions(authData.data.permissions)

View File

@@ -1,5 +1,5 @@
<template>
<div class="system-area">
<div class="pages system-area">
<sc-table ref="tableRef" :columns="columns" :data-source="dataSource" :loading="loading"
:pagination="pagination" @refresh="loadData" @change="handleTableChange" :row-selection="rowSelection"
:show-action="true" :actions="actions" :show-index="true" :show-striped="true">
@@ -160,7 +160,6 @@ const loadData = async () => {
const res = await systemApi.area.list.get(params)
if (res.code === 1) {
dataSource.value = res.data.list || res.data || []
pagination.total = res.data.total || 0
}
} catch (error) {
message.error(t('common.fetchDataFailed'))
@@ -274,15 +273,3 @@ onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.system-area {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #ffffff;
padding: 10px;
border-radius: 10px;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button>
</div>
<div class="right-panel">
<div class="right-panel-search">
<el-input v-model="search.title" placeholder="名称" clearable></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</div>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="list.apiObj" :column="list.column" row-key="id" @selection-change="selectionChange" :params="search">
<el-table-column type="selection" />
<template #operation="scope">
<el-button-group>
<el-button type="primary" @click="edit(scope.row, scope.$index)">编辑</el-button>
<el-button type="primary" @click="table_show(scope.row, scope.$index)">查看</el-button>
<el-button type="primary" @click="client_menu(scope.row, scope.$index)">菜单</el-button>
<el-popconfirm title="确定删除吗?" @confirm="table_delete(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</scTable>
</el-main>
</el-container>
<save ref="saveBox" v-if="dialog.save" @success="upsearch" @closed="dialog.save=false" />
<menus ref="menuBox" v-if="dialog.menu" @closed="dialog.menu=false" />
</template>
<script>
import save from './save.vue'
import menus from './menu.vue'
export default {
name: 'system.client',
components: { save, menus },
data(){
return {
dialog: {search: false, menu: false, save: false},
list: {
apiObj: this.$API.system.client.list,
column: [
{prop: 'id', label: 'ID', width: 80},
{prop: 'title', label: '名称'},
{prop: 'app_id', label: '客户端ID', width: 180},
{prop: 'secret', label: '客户端secret', width: 260},
{prop: 'created_at', label: '添加时间', width: 160},
{prop: 'updated_at', label: '更新时间', width: 160},
{prop: 'operation', label: '操作', width: 220, fixed: 'right'}
],
},
searchFields: [
{title: '标题', key: 'title', type: 'string'},
],
actions: {
add: {title: '', icon: 'a-icon-plus-outlined', type: 'primary'},
},
selection: [],
search: {},
}
},
mounted(){
},
methods:{
upsearch(){
this.$refs.table.reload(this.search);
},
moreUpsearch(search){
this.search = search;
this.upsearch();
},
moreSearch(){
this.dialog.search = true
this.$nextTick(() => {
this.$refs.searchBox.open().setData(this.search)
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
console.log(selection)
},
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('add').setData({})
})
},
edit(item){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('edit').setData(item)
})
},
table_show(item){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('show').setData(item)
})
},
client_menu(item){
this.dialog.menu = true
this.$nextTick(() => {
this.$refs.menuBox.open().setData(item)
})
},
async table_delete(item){
let res = await this.$API.system.client.delete.post({id: item.id});
if(res.code == 1){
//这里选择刷新整个表格 OR 插入/编辑现有表格数据
this.upsearch()
this.$message.success("删除成功")
}else{
this.$message.error(res.message)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,136 @@
<template>
<el-drawer :title="detail.title + '菜单'" v-model="visible" size="80%" destroy-on-close :close-on-click-modal="false" @closed="$emit('closed')">
<el-header>
<div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="add"></el-button>
</div>
<div class="right-panel">
<div class="right-panel-search">
<el-input v-model="search.title" placeholder="名称" clearable></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</div>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="list.apiObj" :column="list.column" row-key="id" @selection-change="selectionChange" :params="search" hidePagination>
<el-table-column type="selection" />
<template #client="scope">
{{ scope.row.client?.title }}
</template>
<template #operation="scope">
<el-button-group>
<el-button type="primary" @click="edit(scope.row, scope.$index)">编辑</el-button>
<el-button type="primary" @click="table_show(scope.row, scope.$index)">查看</el-button>
<el-popconfirm title="确定删除吗?" @confirm="table_delete(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</scTable>
</el-main>
</el-drawer>
<save ref="saveBox" v-if="dialog.save" @success="upsearch" @closed="dialog.save=false" />
</template>
<script>
import save from './menuform.vue'
export default {
emits: ['success', 'closed'],
components: { save },
data(){
return {
detail: {},
visible: false,
isSaveing: false,
dialog: {search: false, menu: false, save: false},
list: {
apiObj: this.$API.system.client.menu.list,
column: [
{prop: 'id', label: 'ID', width: 80},
{prop: 'title', label: '名称'},
{prop: 'client', label: '所属客户端', width: 180},
{prop: 'url', label: '链接', width: 260},
{prop: 'sort', label: '排序', width: 80},
{prop: 'created_at', label: '添加时间', width: 160},
{prop: 'updated_at', label: '更新时间', width: 160},
{prop: 'operation', label: '操作', width: 160, fixed: 'right'}
],
},
searchFields: [
{title: '标题', key: 'title', type: 'string'},
],
actions: {
add: {title: '', icon: 'a-icon-plus-outlined', type: 'primary'},
},
selection: [],
search: {is_tree: 1}
}
},
mounted(){
},
methods:{
//显示
open(){
this.visible = true;
return this;
},
//表单注入数据
setData(data){
this.loading = true
this.detail = data
},
upsearch(){
this.$refs.table.reload(this.search);
},
moreUpsearch(search){
this.search = search;
this.upsearch();
},
moreSearch(){
this.dialog.search = true
this.$nextTick(() => {
this.$refs.searchBox.open().setData(this.search)
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
console.log(selection)
},
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('add').setData()
})
},
edit(item){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('edit').setData(item)
})
},
table_show(item){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('show').setData(item)
})
},
async table_delete(item){
let res = await this.$API.system.client.delete.post({id: item.id});
if(res.code == 1){
//这里选择刷新整个表格 OR 插入/编辑现有表格数据
this.upsearch()
this.$message.success("删除成功")
}else{
this.$message.error(res.message)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,76 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" size="50%" destroy-on-close @closed="$emit('closed')">
<el-main>
<scForm v-model="form" :config="config" :apiObj="apiObj" @onSuccess="onSuccess"></scForm>
</el-main>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data(){
return {
mode: 'add',
titleMap: { add: '添加', edit: '编辑', show: '查看' },
form: {status: 1},
config: {
labelWidth: '120px',
formItems: [
{ name: 'title', label: '名称', type: 'string', options: {} },
{ name: 'parent_id', label: '上级菜单', type: 'scSelectTree', options: {apiObj: this.$API.system.client.menu.list, props: {label: 'title', value: 'id'}} },
{ name: 'url', label: '链接', type: 'string' },
{ name: 'position', label: '所属位置', type: 'scSelect', options: {dic: 'client_menu_position'} },
{ name: 'icon', label: '图标', type: 'scIconSelect' },
{ name: 'sort', label: '排序', type: 'number', options: {} },
{ name: 'is_show', label: '是否显示', type: 'boolean', options: {} },
{ name: 'is_blank', label: '是否新窗口', type: 'boolean', options: {} },
{ name: 'status', label: '状态', type: 'boolean', options: {} }
]
},
rules: {
title: [{ required: true, message: '请输入模型标题' }]
},
apiObj: this.$API.system.client.menu.add,
visible: false,
isSaveing: false
}
},
mounted(){
},
methods:{
open(mode){
this.mode = mode
this.visible = true
this.isSaveing = false
return this;
},
setData(data){
if(this.mode == 'edit'){
this.form.id = data.id
this.apiObj = this.$API.system.client.menu.edit
}else{
this.apiObj = this.$API.system.client.menu.add
}
this.isSaveing = false
this.config.formItems.map(item => {
this.form[item.name] = data[item.name]
})
},
onSuccess(res){
if(res.code == 1){
this.$emit('success', res)
this.visible = false
this.isSaveing = false
this.$message.success("操作成功!")
}else{
this.$message.error(res.message)
this.isSaveing = false
}
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,69 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" size="50%" destroy-on-close @closed="$emit('closed')">
<el-main>
<scForm v-model="form" :config="config" :apiObj="apiObj" @onSuccess="onSuccess"></scForm>
</el-main>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data(){
return {
mode: 'add',
titleMap: { add: '添加', edit: '编辑', show: '查看' },
form: {status: 1},
config: {
labelWidth: '120px',
formItems: [
{ name: 'title', label: '客户端名称', type: 'string', options: {} },
{ name: 'app_id', label: '客户端APPID', type: 'string', options: {}, disabled: true },
{ name: 'secret', label: '客户端密匙', type: 'string', options: {}, disabled: true },
{ name: 'status', label: '状态', type: 'boolean', options: {} }
]
},
rules: {
title: [{ required: true, message: '请输入模型标题' }]
},
apiObj: this.$API.system.client.add,
visible: false,
isSaveing: false
}
},
mounted(){
},
methods:{
open(mode){
this.mode = mode
this.visible = true
this.isSaveing = false
return this;
},
setData(data){
if(this.mode == 'edit'){
this.form.id = data.id
this.apiObj = this.$API.system.client.edit
}else{
this.apiObj = this.$API.system.client.add
}
this.isSaveing = false
this.config.formItems.map(item => {
this.form[item.name] = data[item.name] ? data[item.name] : ''
})
},
onSuccess(res){
if(res.code == 1){
this.$emit('success', res)
this.visible = false
this.$message.success("操作成功!")
}else{
this.$message.error(res.message)
}
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,154 @@
<template>
<el-main>
<el-row :gutter="15">
<el-col :xl="6" :lg="6" :md="8" :sm="12" :xs="24" v-for="item in list" :key="item.id">
<el-card class="task task-item" shadow="hover">
<h2>{{item.title}}</h2>
<ul>
<li>
<h4>任务类型</h4>
<p>{{crontabType[item.type]}}</p>
</li>
<li>
<h4>执行类</h4>
<p>{{item.command}}</p>
</li>
<li>
<h4>定时规则</h4>
<p>{{item.expression}}</p>
</li>
</ul>
<div class="bottom">
<div class="state">
<el-tag v-if="item.status==1">运行中</el-tag>
<el-tag v-if="item.status==0" type="info">停用</el-tag>
</div>
<div class="handler">
<el-popconfirm :title="item.status==1 ? '确定立即关闭吗?' : '确定立即执行吗?'" @confirm="run(item)">
<template #reference>
<el-button type="primary" :icon="item.status==1 ? 'el-icon-switch-button' : 'el-icon-caret-right'" circle></el-button>
</template>
</el-popconfirm>
<el-dropdown trigger="click">
<el-button type="primary" icon="el-icon-more" circle plain></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="edit(item)">编辑</el-dropdown-item>
<el-dropdown-item @click="logs(item)">日志</el-dropdown-item>
<el-dropdown-item @click="del(item)" divided>删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-card>
</el-col>
<el-col :xl="6" :lg="6" :md="8" :sm="12" :xs="24">
<el-card class="task task-add" shadow="none" @click="add">
<el-icon><el-icon-plus /></el-icon>
<p>添加计划任务</p>
</el-card>
</el-col>
</el-row>
</el-main>
<save-dialog v-if="dialog.save" ref="saveDialog" @success="handleSuccess" @closed="dialog.save=false"></save-dialog>
<logs v-if="dialog.logs" ref="logsDialog" @closed="dialog.logs=false"></logs>
</template>
<script>
import saveDialog from './save'
import logs from './logs'
export default {
name: 'system.crontab',
components: {
saveDialog,
logs
},
provide() {
return {
list: this.list
}
},
data() {
return {
dialog: {
save: false,
logs: false
},
crontabType: {1: '执行命令', 2: '执行类class', 3: '执行地址', 4: '执行shell'},
list: []
}
},
mounted() {
this.getCrontabList()
},
methods: {
async getCrontabList(){
let res = await this.$API.system.crontab.list.get()
if(res.code===1){
this.list = res.data
}
},
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open()
})
},
edit(task){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveDialog.open('edit').setData(task)
})
},
del(task){
this.$confirm(`确认删除 ${task.title} 计划任务吗?`,'提示', {
type: 'warning',
confirmButtonText: '删除',
confirmButtonClass: 'el-button--danger'
}).then(async () => {
let res = await this.$API.system.crontab.delete.post({id: task.id})
if(res.code===1){
this.$message.success('操作成功')
this.getCrontabList()
}
}).catch(() => {
//取消
})
},
logs(row){
this.dialog.logs = true
this.$nextTick(() => {
this.$refs.logsDialog.open().setData(row)
})
},
async run(task){
let res = await this.$API.system.crontab.reload.post({id: task.id, status: task.status == 1 ? 0 : 1})
if(res.code===1){
this.$message.success('已成功执行计划任务')
this.getCrontabList()
}
},
//本地更新数据
handleSuccess(){
this.getCrontabList()
}
}
}
</script>
<style scoped>
.task {height: 240px;}
.task-item h2 {font-size: 15px;color: #3c4a54;padding-bottom:15px;}
.task-item li {list-style-type:none;margin-bottom: 10px;}
.task-item li h4 {font-size: 12px;font-weight: normal;color: #999;}
.task-item li p {margin-top: 5px;}
.task-item .bottom {border-top: 1px solid #EBEEF5;text-align: right;padding-top:10px;display: flex;justify-content: space-between;align-items: center;}
.task-add {display: flex;flex-direction: column;align-items: center;justify-content: center;text-align: center;cursor: pointer;color: #999;}
.task-add:hover {color: #409EFF;}
.task-add i {font-size: 30px;}
.task-add p {font-size: 12px;margin-top: 20px;}
</style>

View File

@@ -0,0 +1,79 @@
<!--
* @Descripttion: 系统计划任务配置
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月7日09:28:32
* @LastEditors:
* @LastEditTime:
-->
<template>
<el-drawer title="计划任务日志" v-model="Visible" :size="780" direction="rtl" destroy-on-close>
<el-container>
<el-main style="padding:0 20px;">
<scTable ref="table" :apiObj="list.apiObj" :column="list.column" :params="search" stripe>
<el-table-column type="selection" width="50"></el-table-column>
<template #return_code="scope">
<span v-if="scope.row.return_code==0" style="color: #67C23A;"><el-icon><el-icon-success-filled /></el-icon></span>
<span v-else style="color: #F56C6C;"><el-icon><el-icon-circle-close-filled /></el-icon></span>
</template>
<template #logs="scope">
<el-button @click="show(scope.row)" type="text">日志</el-button>
</template>
<template #create_time="scope">
<div v-time="scope.row.create_time"></div>
</template>
</scTable>
</el-main>
</el-container>
<el-drawer title="日志" v-model="logsVisible" :size="500" direction="rtl" destroy-on-close>
<el-main style="padding:0 20px 20px 20px;">
<pre style="font-size: 12px;color: #999;padding:20px;background: #333;font-family: consolas;line-height: 1.5;overflow: auto;">{{logDetail}}</pre>
</el-main>
</el-drawer>
</el-drawer>
</template>
<script>
export default {
data() {
return {
Visible: false,
logsVisible: false,
crontab: {},
logDetail: '',
list: {
apiObj: this.$API.system.crontab.log,
column: [
{ label: '执行时间', prop: 'running_time', width: 100 },
{ label: '执行结果', prop: 'return_code', width: 100, align: 'center' },
{ label: '参数', prop: 'parameter', width: 100, align: 'center' },
{ label: '时间', prop: 'create_time', width: 200 },
{ label: '执行日志', prop: 'logs', align: 'center' }
]
},
search: {crontab_id: 0}
}
},
mounted() {
},
methods: {
open() {
this.Visible = true
return this
},
show(item){
this.logDetail = item.exception
this.logsVisible = true;
},
setData(row){
this.crontab = row
this.search.crontab_id = row.id
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,113 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="400" destroy-on-close @closed="$emit('closed')">
<el-form :model="form" :rules="rules" ref="dialogForm" label-width="100px" label-position="left">
<el-form-item label="描述" prop="title">
<el-input v-model="form.title" placeholder="计划任务标题" clearable></el-input>
</el-form-item>
<el-form-item label="任务分类" prop="type">
<el-select v-model="form.type" placeholder="计划任务执行类名称" clearable>
<el-option v-for="item in [{value: 1, label: '执行命令'}, {value: 2, label: '执行类class'}, {value: 3, label: '执行地址'}, {value: 4, label: '执行shell'}]" :key="item.value" :label="item.label" :value="item.value" ></el-option>
</el-select>
</el-form-item>
<el-form-item label="执行类" prop="command">
<el-input v-model="form.command" placeholder="计划任务执行类名称" clearable></el-input>
</el-form-item>
<el-form-item label="定时规则" prop="expression">
<sc-cron v-model="form.expression" placeholder="请输入Cron定时规则" clearable :shortcuts="shortcuts"></sc-cron>
</el-form-item>
<el-form-item label="是否启用" prop="status">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="备注" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
import scCron from '@/components/scCron';
export default {
components: {
scCron
},
emits: ['success', 'closed'],
data() {
return {
mode: "add",
titleMap: {
add: '新增计划任务',
edit: '编辑计划任务'
},
form: {
id:"",
title: "",
type: 1,
command: "",
expression: "",
status: 1,
remark: ""
},
rules: {
title:[{required: true, message: '请填写标题'}],
command:[{required: true, message: '请填写执行类'}],
expression:[{required: true, message: '请填写定时规则'}]
},
visible: false,
isSaveing: false,
shortcuts: [
{
text: "每天8点和12点 (自定义追加)",
value: "0 0 8,12 * * ?"
}
]
}
},
mounted() {
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
let res = await this.$API.system.crontab[this.mode].post(this.form)
if(res.code == 1){
this.isSaveing = false;
this.visible = false;
this.$message.success("操作成功")
this.$emit('success', this.form, this.mode)
}else{
this.$message.error(res.message)
this.isSaveing = false;
}
}
})
},
//表单注入数据
setData(data){
this.form.id = data.id ?? 0
this.form.title = data.title ?? ""
this.form.type = data.type ?? 1
this.form.command = data.command ?? ""
this.form.expression = data.expression ?? ""
this.form.status = data.status ?? 1
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,107 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="330" destroy-on-close @closed="$emit('closed')">
<el-form :model="form" :rules="rules" ref="dialogForm" label-width="80px" label-position="left">
<el-form-item label="编码" prop="name">
<el-input v-model="form.name" clearable placeholder="字典编码"></el-input>
</el-form-item>
<el-form-item label="字典名称" prop="title">
<el-input v-model="form.title" clearable placeholder="字典显示名称"></el-input>
</el-form-item>
<el-form-item label="父路径" prop="parent_id">
<el-cascader v-model="form.parent_id" :options="dic" :props="dicProps" :show-all-levels="false" clearable></el-cascader>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data() {
return {
mode: "add",
titleMap: {
add: '新增字典分类',
edit: '编辑字典分类'
},
visible: false,
isSaveing: false,
form: {
id:"",
title: "",
name: "",
parent_id: ""
},
rules: {
name: [
{required: true, message: '请输入编码'}
],
title: [
{required: true, message: '请输入字典名称'}
]
},
dic: [],
dicProps: {
value: "id",
label: "name",
emitPath: false,
checkStrictly: true
}
}
},
mounted() {
this.getDic()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this;
},
//获取字典列表
async getDic(){
var res = await this.$API.system.dictionary.category.get({is_tree: 1});
this.dic = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
var res = {};
this.form.parent_id = this.form.parent_id ? this.form.parent_id : 0;
if(this.mode == 'add'){
res = await this.$API.system.dictionary.addcate.post(this.form);
}else{
res = await this.$API.system.dictionary.editcate.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
}
})
},
//表单注入数据
setData(data){
this.form.id = data.id
this.form.title = data.title
this.form.name = data.name
this.form.parent_id = data.parent_id
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,321 @@
<template>
<el-container>
<el-aside width="300px" v-loading="showDicloading">
<el-container>
<el-header>
<el-input placeholder="输入关键字进行过滤" v-model="dicFilterText" clearable></el-input>
</el-header>
<el-main class="nopadding">
<el-tree ref="dic" class="menu" node-key="id" :data="dicList" :props="dicProps" :highlight-current="true" :expand-on-click-node="false" :filter-node-method="dicFilterNode" @node-click="dicClick">
<template #default="{node, data}">
<span class="custom-tree-node">
<span class="label">{{ node.label }}</span>
<span class="code">{{ data.name }}</span>
<span class="do">
<el-icon :size="26" @click.stop="dicEdit(data)"><el-icon-edit /></el-icon>
<el-icon :size="26" @click.stop="dicDel(node, data)"><el-icon-delete /></el-icon>
</span>
</span>
</template>
</el-tree>
</el-main>
<el-footer style="height:51px;">
<el-button type="primary" icon="el-icon-plus" style="width: 100%;" @click="addDic">字典分类</el-button>
</el-footer>
</el-container>
</el-aside>
<el-container class="is-vertical">
<el-header>
<div class="left-panel">
<el-button type="primary" icon="el-icon-plus" @click="addInfo"></el-button>
<el-button type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="listApi" row-key="id" :params="listApiParams" @selection-change="selectionChange" stripe :paginationLayout="'prev, pager, next'">
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column label="" width="60">
<template #default>
<el-tag class="move" style="cursor: move;"><el-icon-d-caret style="width: 1em; height: 1em;"/></el-tag>
</template>
</el-table-column>
<el-table-column label="名称" prop="title"></el-table-column>
<el-table-column label="键值" prop="values" width="150"></el-table-column>
<el-table-column label="是否有效" prop="status" width="100">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status==1"></el-tag>
<el-tag type="info" v-if="scope.row.status==0"></el-tag>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" width="150"></el-table-column>
<el-table-column label="操作" fixed="right" align="right" width="140">
<template #default="scope">
<el-button-group>
<el-button type="primary" @click="table_edit(scope.row, scope.$index)">编辑</el-button>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</el-table-column>
</scTable>
</el-main>
</el-container>
</el-container>
<dic-dialog v-if="dialog.dic" ref="dicDialog" @success="handleDicSuccess" @closed="dialog.dic=false"></dic-dialog>
<list-dialog v-if="dialog.list" ref="listDialog" @success="handleListSuccess" @closed="dialog.list=false"></list-dialog>
</template>
<script>
import dicDialog from './dic'
import listDialog from './list'
import Sortable from 'sortablejs'
export default {
name: 'system.dic',
components: {
dicDialog,
listDialog
},
data() {
return {
dialog: {
dic: false,
info: false
},
showDicloading: true,
dicList: [],
dicFilterText: '',
dicProps: {
label: 'title'
},
listApi: this.$API.system.dictionary.list,
listApiParams: {},
selection: []
}
},
watch: {
dicFilterText(val) {
this.$refs.dic.filter(val);
}
},
mounted() {
this.getDic()
// this.rowDrop()
},
methods: {
//加载树数据
async getDic(){
var res = await this.$API.system.dictionary.category.get({is_tree: 1});
this.showDicloading = false;
this.dicList = res.data;
//获取第一个节点,设置选中 & 加载明细列表
var firstNode = this.dicList[0];
if(firstNode){
this.$nextTick(() => {
this.$refs.dic.setCurrentKey(firstNode.id)
})
this.listApiParams = {
group_id: firstNode.id
}
this.$refs.table.reload(this.listApiParams);
}
},
//树过滤
dicFilterNode(value, data){
if (!value) return true;
var targetText = data.title + data.name;
return targetText.indexOf(value) !== -1;
},
//树增加
addDic(){
this.dialog.dic = true
this.$nextTick(() => {
this.$refs.dicDialog.open()
})
},
//编辑树
dicEdit(data){
this.dialog.dic = true
this.$nextTick(() => {
var editNode = this.$refs.dic.getNode(data.id);
var editNodeParentId = editNode.level==1?undefined:editNode.parent.data.id
data.parent_id = editNodeParentId
this.$refs.dicDialog.open('edit').setData(data)
})
},
//树点击事件
dicClick(data){
this.$refs.table.reload({
group_id: data.id
})
},
//删除树
dicDel(node, data){
this.$confirm(`确定删除 ${data.name} 项吗?`, '提示', {
type: 'warning'
}).then(() => {
this.showDicloading = true;
this.$API.system.dictionary.delCate.post({id: data.id});
//删除节点是否为高亮当前 是的话 设置第一个节点高亮
var dicCurrentKey = this.$refs.dic.getCurrentKey();
this.$refs.dic.remove(data.id)
if(dicCurrentKey == data.id){
var firstNode = this.dicList[0];
if(firstNode){
this.$refs.dic.setCurrentKey(firstNode.id);
this.$refs.table.upData({
code: firstNode.code
})
}else{
this.listApi = null;
this.$refs.table.tableData = []
}
}
this.showDicloading = false;
this.$message.success("操作成功")
}).catch(() => {
})
},
//行拖拽
rowDrop(){
const _this = this
const tbody = this.$refs.table.$el.querySelector('.el-table__body-wrapper tbody')
Sortable.create(tbody, {
handle: ".move",
animation: 300,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.$refs.table.tableData
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
_this.$message.success("排序成功")
}
})
},
//添加明细
addInfo(){
this.dialog.list = true
this.$nextTick(() => {
var node = this.$refs.dic.getCurrentNode();
const data = {
dic_type: node.code
}
this.$refs.listDialog.open().setData(data)
})
},
//编辑明细
table_edit(row){
this.dialog.list = true
this.$nextTick(() => {
this.$refs.listDialog.open('edit').setData(row)
})
},
//删除明细
async table_del(row, index){
var reqData = {id: row.id}
var res = await this.$API.system.dictionary.delete.post(reqData);
if(res.code == 1){
this.$refs.table.tableData.splice(index, 1);
this.$message.success("删除成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
},
//批量删除
async batch_del(){
this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?`, '提示', {
type: 'warning'
}).then(() => {
const loading = this.$loading();
this.selection.forEach(item => {
this.$refs.table.tableData.forEach((itemI, indexI) => {
if (item.id === itemI.id) {
this.$refs.table.tableData.splice(indexI, 1)
}
})
})
loading.close();
this.$message.success("操作成功")
}).catch(() => {
})
},
//提交明细
saveList(){
this.$refs.listDialog.submit(async (formData) => {
this.isListSaveing = true;
var res = await this.$API.system.dictionary.post.post(formData);
this.isListSaveing = false;
if(res.code == 1){
//这里选择刷新整个表格 OR 插入/编辑现有表格数据
this.listDialogVisible = false;
this.$message.success("操作成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
},
//表格内开关事件
changeSwitch(val, row){
this.$API.system.dictionary.edit.post({id: row.id, status: val});
},
//本地更新数据
handleDicSuccess(data, mode){
if(mode=='add'){
if(this.dicList.length > 0){
this.$refs.table.upData({
code: data.code
})
}else{
this.listApiParams = {
code: data.code
}
this.listApi = this.$API.system.dictionary.list;
}
this.$refs.dic.append(data, data.parent_id)
this.$refs.dic.setCurrentKey(data.id)
}else if(mode=='edit'){
var editNode = this.$refs.dic.getNode(data.id);
//判断是否移动?
var editNodeParentId = editNode.level==1 ? undefined : editNode.parent.data.id
console.log(editNodeParentId)
if(editNodeParentId != data.parent_id){
var obj = editNode.data;
this.$refs.dic.remove(data.id)
this.$refs.dic.append(obj, data.parent_id)
}
Object.assign(editNode.data, data)
this.$refs.table.refresh()
}
},
//本地更新数据
handleListSuccess(){
this.$refs.table.refresh()
}
}
}
</script>
<style scoped>
.custom-tree-node {display: flex;flex: 1;align-items: center;justify-content: space-between;font-size: 14px;padding-right: 24px;height:100%;}
.custom-tree-node .code {font-size: 12px;color: #999;}
.custom-tree-node .do {display: none;}
.custom-tree-node .do i {margin-left:5px;color: #999;padding:5px;}
.custom-tree-node .do i:hover {color: #333;}
.custom-tree-node:hover .code {display: none;}
.custom-tree-node:hover .do {display: inline-block;}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<el-dialog :title="titleMap[mode]" v-model="visible" :width="400" destroy-on-close @closed="$emit('closed')">
<el-form :model="form" :rules="rules" ref="dialogForm" label-width="100px" label-position="left">
<el-form-item label="所属字典" prop="group_id">
<el-cascader v-model="form.group_id" :options="dic" :props="dicProps" :show-all-levels="false" clearable></el-cascader>
</el-form-item>
<el-form-item label="项名称" prop="title">
<el-input v-model="form.title" clearable></el-input>
</el-form-item>
<el-form-item label="键值" prop="values">
<el-input v-model="form.values" clearable></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="form.sort" clearable></el-input>
</el-form-item>
<el-form-item label="是否有效" prop="status">
<el-switch v-model="form.status" :active-value="'1'" :inactive-value="'0'"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false" > </el-button>
<el-button type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
export default {
emits: ['success', 'closed'],
data() {
return {
mode: "add",
titleMap: {
add: '新增项',
edit: '编辑项'
},
visible: false,
isSaveing: false,
form: {id: "", group_id: "", title: "", values: "", sort: 0, status: "1"},
rules: {
group_id: [{required: true, message: '请选择所属字典'}],
title: [{required: true, message: '请输入项名称'}],
values: [{required: true, message: '请输入键值'}]
},
dic: [],
dicProps: {
value: "id",
label: "title",
emitPath: false,
checkStrictly: true
}
}
},
mounted() {
if(this.params){
this.form.dic = this.params.code
}
this.getDic()
},
methods: {
//显示
open(mode='add'){
this.mode = mode;
this.visible = true;
return this;
},
//获取字典列表
async getDic(){
var res = await this.$API.system.dictionary.category.get({is_tree: 1});
this.dic = res.data;
},
//表单提交方法
submit(){
this.$refs.dialogForm.validate(async (valid) => {
if (valid) {
this.isSaveing = true;
var res;
if(this.mode == 'add'){
res = await this.$API.system.dictionary.add.post(this.form);
}else{
res = await this.$API.system.dictionary.edit.post(this.form);
}
this.isSaveing = false;
if(res.code == 1){
this.$emit('success', this.form, this.mode)
this.visible = false;
this.$message.success("操作成功")
}else{
this.$alert(res.message, "提示", {type: 'error'})
}
}
})
},
//表单注入数据
setData(data){
this.form.id = data.id
this.form.title = data.title
this.form.values = data.values
this.form.sort = data.sort || 0
this.form.status = data.status+"" || "1"
this.form.group_id = parseInt(data.group_id)
return this;
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div class="pages log-page">
<div class="left-box">
<a-card>
<a-tree show-line showIcon switcherIcon default-expand-all :tree-data="treeData "@select="onSelect" />
</a-card>
</div>
<div class="right-box">
<div class="right-warp">
<scTable ref="tableRef" :columns="columns" :data-source="dataList.data" :loading="loading"></scTable>
</div>
</div>
</div>
</template>
<script setup>
import scTable from '@/components/scTable/index.vue'
import system from '@/api/system'
import { onMounted, ref } from 'vue';
const treeData = ref([
{title: '请求分类', key: 'request', children: [
{title: 'GET请求', key: 'get'},
{title: 'POST请求', key: 'post'},
{title: 'PUT请求', key: 'put'},
{title: 'DELETE请求', key: 'delete'}
]},
{title: '响应状态', key: 'response', children: []}
])
const onSelect = (selectedKeys, info) => {
console.log(selectedKeys, info)
}
let loading = ref(true)
let search = ref({page: 1, limit: 30})
let dataList = ref({})
const columns = ref([
{dataIndex: 'id', title: 'ID', width: 100},
{dataIndex: 'title', title: '请求名称'},
{dataIndex: 'method', title: '请求类型', width: 100},
{dataIndex: 'url', title: '请求地址'},
{dataIndex: 'created_at', title: '请求时间', width: 180},
])
const loadData = async () => {
let res = await system.log.list.get(search.value)
if (res.code == 1) {
dataList.value = res.data
loading = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.log-page {
flex-direction: row;
gap: 10px;
.left-box {
width: 240px;
height: 100%;
}
.right-box {
flex: 1;
display: flex;
flex-direction: column;
.right-warp{
flex: 1;
}
}
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<el-main style="padding:0 20px;">
<el-descriptions :column="1" border>
<el-descriptions-item label="操作人">{{data.user?.nickname}}</el-descriptions-item>
<el-descriptions-item label="客户端IP">{{data.client_ip}}</el-descriptions-item>
<el-descriptions-item label="请求接口">{{data.url}}</el-descriptions-item>
<el-descriptions-item label="请求方法">{{data.method}}</el-descriptions-item>
<el-descriptions-item label="状态代码">{{data.status}}</el-descriptions-item>
<el-descriptions-item label="日志名">{{data.title}}</el-descriptions-item>
<el-descriptions-item label="日志时间">{{data.created_at}}</el-descriptions-item>
</el-descriptions>
<el-collapse v-model="activeNames" style="margin-top: 20px;">
<el-collapse-item title="请求参数" name="1">
<div class="code">{{data.data}}</div>
</el-collapse-item>
<el-collapse-item title="详细" name="2">
<div class="code">
{{data.browser}}
</div>
</el-collapse-item>
</el-collapse>
</el-main>
</template>
<script>
export default {
data() {
return {
data: {},
activeNames: ['1'],
typeMap: {
'info': "info",
'warn': "warning",
'error': "error"
}
}
},
methods: {
setData(data){
this.data = data
}
}
}
</script>
<style scoped>
.code {background: #848484;padding:15px;color: #fff;font-size: 12px;border-radius: 4px;}
</style>

View File

@@ -1,193 +0,0 @@
<template>
<div class="system-menu">
<a-card title="菜单管理">
<template #extra>
<a-button type="primary" @click="handleAdd">
<PlusOutlined />
新增菜单
</a-button>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="false" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'icon'">
<component :is="record.icon || 'MenuOutlined'" />
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handleAddChild(record)">新增子菜单</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, MenuOutlined } from '@ant-design/icons-vue'
const columns = [
{
title: '菜单名称',
dataIndex: 'title',
key: 'title'
},
{
title: '图标',
dataIndex: 'icon',
key: 'icon',
width: 80
},
{
title: '路径',
dataIndex: 'path',
key: 'path'
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 80
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '操作',
key: 'action',
width: 220
}
]
const dataSource = ref([])
const loading = ref(false)
// 模拟数据
const mockData = [
{
id: 1,
title: '首页',
path: '/home',
icon: 'HomeOutlined',
type: 1,
sort: 1,
status: 1,
children: []
},
{
id: 2,
title: '系统管理',
path: '/system',
icon: 'SettingOutlined',
type: 1,
sort: 2,
status: 1,
children: [
{
id: 21,
title: '用户管理',
path: '/system/user',
icon: 'UserOutlined',
type: 2,
sort: 1,
status: 1
},
{
id: 22,
title: '角色管理',
path: '/system/role',
icon: 'TeamOutlined',
type: 2,
sort: 2,
status: 1
},
{
id: 23,
title: '菜单管理',
path: '/system/menu',
icon: 'MenuOutlined',
type: 2,
sort: 3,
status: 1
}
]
}
]
const loadData = () => {
loading.value = true
// 模拟接口请求
setTimeout(() => {
dataSource.value = mockData
loading.value = false
}, 500)
}
const getTypeColor = (type) => {
const colorMap = {
1: 'blue',
2: 'green',
3: 'orange'
}
return colorMap[type] || 'default'
}
const getTypeText = (type) => {
const textMap = {
1: '目录',
2: '菜单',
3: '按钮'
}
return textMap[type] || '未知'
}
const handleAdd = () => {
message.info('新增菜单功能开发中')
}
const handleEdit = (record) => {
message.info('编辑菜单功能开发中')
}
const handleAddChild = (record) => {
message.info(`新增 ${record.title} 的子菜单功能开发中`)
}
const handleDelete = (record) => {
message.info('删除菜单功能开发中')
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.system-menu {
// 样式
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
</div>
<div class="right-panel">
<div class="right-panel-search">
<el-input v-model="search.title" placeholder="名称" clearable></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</div>
</div>
</el-header>
<el-main class="nopadding">
<scTable ref="table" :apiObj="list.apiObj" :column="list.column" row-key="id" @selection-change="selectionChange" :params="search">
<el-table-column type="selection" />
<template #status="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'error'">{{ scope.row.status == 1 ? '启用' : '禁用' }}</el-tag>
</template>
<template #operation="scope">
<el-button type="primary" @click="update(scope.row, scope.$index)">更新</el-button>
</template>
</scTable>
</el-main>
</el-container>
</template>
<script>
export default {
name: 'system.client',
data(){
return {
dialog: {search: false, menu: false, save: false},
list: {
apiObj: this.$API.system.modules.list,
column: [
{prop: 'id', label: 'ID', width: 80},
{prop: 'title', label: '模块名称'},
{prop: 'name', label: '模块标识', width: 180},
{prop: 'status', label: '状态', width: 260},
{prop: 'created_at', label: '添加时间', width: 160},
{prop: 'updated_at', label: '更新时间', width: 160},
{prop: 'operation', label: '操作', width: 80, fixed: 'right'}
],
},
searchFields: [
{title: '标题', key: 'title', type: 'string'},
],
actions: {
add: {title: '', icon: 'a-icon-plus-outlined', type: 'primary'},
},
selection: [],
search: {},
}
},
mounted(){
},
methods:{
upsearch(){
this.$refs.table.reload(this.search);
},
moreUpsearch(search){
this.search = search;
this.upsearch();
},
moreSearch(){
this.dialog.search = true
this.$nextTick(() => {
this.$refs.searchBox.open().setData(this.search)
})
},
//表格选择后回调事件
selectionChange(selection){
this.selection = selection;
console.log(selection)
},
add(){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('add').setData({})
})
},
edit(item){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('edit').setData(item)
})
},
table_show(item){
this.dialog.save = true
this.$nextTick(() => {
this.$refs.saveBox.open('show').setData(item)
})
},
client_menu(item){
this.dialog.menu = true
this.$nextTick(() => {
this.$refs.menuBox.open().setData(item)
})
},
async update(item){
let res = await this.$API.system.modules.update.post({name: item.name});
if(res.code == 1){
//这里选择刷新整个表格 OR 插入/编辑现有表格数据
this.upsearch()
this.$message.success("更新成功")
}else{
this.$message.error(res.message)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,141 +0,0 @@
<template>
<div class="system-role">
<a-card title="角色管理">
<template #extra>
<a-button type="primary" @click="handleAdd">
<PlusOutlined />
新增角色
</a-button>
</template>
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handlePermission(record)">权限</a-button>
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '角色名称',
dataIndex: 'roleName',
key: 'roleName'
},
{
title: '角色标识',
dataIndex: 'roleCode',
key: 'roleCode'
},
{
title: '描述',
dataIndex: 'description',
key: 'description'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime'
},
{
title: '操作',
key: 'action',
width: 180
}
]
const dataSource = ref([])
const loading = ref(false)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`
})
// 模拟数据
const mockData = [
{
id: 1,
roleName: '管理员',
roleCode: 'admin',
description: '系统管理员',
status: 1,
createTime: '2024-01-01 10:00:00'
},
{
id: 2,
roleName: '普通用户',
roleCode: 'user',
description: '普通用户角色',
status: 1,
createTime: '2024-01-02 10:00:00'
}
]
const loadData = () => {
loading.value = true
// 模拟接口请求
setTimeout(() => {
dataSource.value = mockData
pagination.value.total = mockData.length
loading.value = false
}, 500)
}
const handleAdd = () => {
message.info('新增角色功能开发中')
}
const handleEdit = (record) => {
message.info('编辑角色功能开发中')
}
const handlePermission = (record) => {
message.info('权限配置功能开发中')
}
const handleDelete = (record) => {
message.info('删除角色功能开发中')
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.system-role {
// 样式
}
</style>

View File

@@ -25,14 +25,16 @@
<a-form-item v-if="!isEdit" :name="['type']" :label="$t('common.configType')"
:rules="[{ required: true, message: $t('common.pleaseSelect') + $t('common.configType') }]">
<a-select v-model:value="formData.type" :placeholder="$t('common.pleaseSelect')">
<a-select-option value="text">{{ $t('common.typeText') }}</a-select-option>
<a-select-option value="textarea">{{ $t('common.typeTextarea') }}</a-select-option>
<a-select-option value="number">{{ $t('common.typeNumber') }}</a-select-option>
<a-select-option value="switch">{{ $t('common.typeSwitch') }}</a-select-option>
<a-select-option value="select">{{ $t('common.typeSelect') }}</a-select-option>
<a-select-option value="multiselect">{{ $t('common.typeMultiselect') }}</a-select-option>
<a-select-option value="datetime">{{ $t('common.typeDatetime') }}</a-select-option>
<a-select-option value="color">{{ $t('common.typeColor') }}</a-select-option>
<a-select-option value="string">文本</a-select-option>
<a-select-option value="textarea">文本域</a-select-option>
<a-select-option value="number">数字</a-select-option>
<a-select-option value="switch">开关</a-select-option>
<a-select-option value="select">下拉选择</a-select-option>
<a-select-option value="radio">单选</a-select-option>
<a-select-option value="multiselect">多选</a-select-option>
<a-select-option value="datetime">日期时间</a-select-option>
<a-select-option value="color">颜色</a-select-option>
<a-select-option value="image">图片</a-select-option>
</a-select>
</a-form-item>
@@ -40,8 +42,8 @@
<a-input v-model:value="formData.value" :placeholder="$t('common.pleaseEnter')" />
</a-form-item>
<a-form-item :name="['tip']" :label="$t('common.configTip')">
<a-textarea v-model:value="formData.tip" :placeholder="$t('common.pleaseEnter')" :rows="3" />
<a-form-item :name="['remark']" :label="$t('common.configTip')">
<a-textarea v-model:value="formData.remark" :placeholder="$t('common.pleaseEnter')" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
@@ -89,9 +91,9 @@ const formData = reactive({
category: '',
name: '',
title: '',
type: 'text',
type: 'string',
value: '',
tip: '',
remark: '',
})
// 监听弹窗显示和初始数据变化
@@ -100,9 +102,9 @@ watch(() => props.initialData, (newVal) => {
formData.category = newVal.category || ''
formData.name = newVal.name || ''
formData.title = newVal.title || ''
formData.type = newVal.type || 'text'
formData.type = newVal.type || 'string'
formData.value = newVal.value || ''
formData.tip = newVal.tip || ''
formData.remark = newVal.remark || newVal.tip || ''
}
}, { immediate: true })
@@ -129,9 +131,9 @@ const handleClose = () => {
formData.category = ''
formData.name = ''
formData.title = ''
formData.type = 'text'
formData.type = 'string'
formData.value = ''
formData.tip = ''
formData.remark = ''
emit('update:visible', false)
}
</script>

View File

@@ -1,102 +1,102 @@
<template>
<div class="system-setting">
<a-card :bordered="false">
<template #title>
<div class="page-title">
<SettingOutlined />
<span>{{ $t('common.systemSettings') }}</span>
</div>
<div class="pages system-setting">
<!-- Tab 页签 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<template #rightExtra>
<a-button type="primary" @click="handleAddConfig">
<PlusOutlined />
{{ $t('common.addConfig') }}
</a-button>
</template>
<!-- Tab 页签 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<template #rightExtra>
<a-button type="primary" @click="handleAddConfig">
<PlusOutlined />
{{ $t('common.addConfig') }}
</a-button>
</template>
<a-tab-pane v-for="category in categories" :key="category.name" :tab="category.title">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }" class="setting-form">
<a-form-item v-for="field in fields.filter(f => f.category === category.name)" :key="field.name"
:label="field.title">
<div class="form-item-content">
<div class="form-input-wrapper">
<!-- 文本输入 -->
<a-input v-if="field.type === 'text'" v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" />
<!-- 文本域 -->
<a-textarea v-else-if="field.type === 'textarea'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" :rows="4" />
<!-- 数字输入 -->
<a-input-number v-else-if="field.type === 'number'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')"
style="width: 100%" />
<!-- 开关 -->
<a-switch v-else-if="field.type === 'switch'"
v-model:checked="formData[field.name]" />
<!-- 下拉选择 -->
<a-select v-else-if="field.type === 'select'" v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')"
style="width: 100%">
<a-select-option v-for="option in field.options" :key="option.value"
:value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<!-- 多选 -->
<a-select v-else-if="field.type === 'multiselect'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')" mode="multiple"
style="width: 100%">
<a-select-option v-for="option in field.options" :key="option.value"
:value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<!-- 日期时间 -->
<a-date-picker v-else-if="field.type === 'datetime'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')"
style="width: 100%" show-time format="YYYY-MM-DD HH:mm:ss" />
<!-- 颜色选择器 -->
<a-input v-else-if="field.type === 'color'" v-model:value="formData[field.name]"
type="color" style="width: 100px" />
<!-- 默认文本输入 -->
<a-input v-else v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" />
</div>
<div class="form-actions">
<EditOutlined class="action-icon edit-icon" :title="$t('common.edit')"
@click="handleEditField(field)" />
<a-tab-pane v-for="category in categories" :key="category.name" :tab="category.title">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }" class="setting-form">
<a-form-item v-for="field in fields.filter(f => f.category === category.name)" :key="field.name"
:label="field.title">
<div class="form-item-content">
<div class="form-input-wrapper">
<!-- 文本输入 -->
<a-input v-if="field.type === 'text'" v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" />
<!-- 文本域 -->
<a-textarea v-else-if="field.type === 'textarea'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" :rows="4" />
<!-- 数字输入 -->
<a-input-number v-else-if="field.type === 'number'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')"
style="width: 100%" />
<!-- 开关 -->
<a-switch v-else-if="field.type === 'switch'"
v-model:checked="formData[field.name]" />
<!-- 下拉选择 -->
<a-select v-else-if="field.type === 'select'" v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')"
style="width: 100%">
<a-select-option v-for="option in field.options" :key="option.value"
:value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<!-- 多选 -->
<a-select v-else-if="field.type === 'multiselect'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')" mode="multiple"
style="width: 100%">
<a-select-option v-for="option in field.options" :key="option.value"
:value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<!-- 日期时间 -->
<a-date-picker v-else-if="field.type === 'datetime'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')"
style="width: 100%" show-time format="YYYY-MM-DD HH:mm:ss" />
<!-- 颜色选择器 -->
<a-input v-else-if="field.type === 'color'" v-model:value="formData[field.name]"
type="color" style="width: 100px" />
<!-- 图片上传 -->
<div v-else-if="field.type === 'image'" class="image-uploader">
<img v-if="formData[field.name]" :src="formData[field.name]" class="image-preview" />
<a-button v-else type="dashed" @click="handleUpload(field)">
<UploadOutlined />
{{ $t('common.uploadImage') }}
</a-button>
</div>
<!-- 默认文本输入 -->
<a-input v-else v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" />
</div>
<div v-if="field.tip" class="field-tip">{{ field.tip }}</div>
</a-form-item>
</a-form>
<div class="form-actions">
<EditOutlined class="action-icon edit-icon" :title="$t('common.edit')"
@click="handleEditField(field)" />
</div>
</div>
<div v-if="field.tip" class="field-tip">{{ field.tip }}</div>
</a-form-item>
</a-form>
<!-- 空状态 -->
<a-empty v-if="fields.filter(f => f.category === category.name).length === 0"
:description="$t('common.noConfig')" />
</a-tab-pane>
</a-tabs>
<!-- 空状态 -->
<a-empty v-if="fields.filter(f => f.category === category.name).length === 0"
:description="$t('common.noConfig')" />
</a-tab-pane>
</a-tabs>
<!-- 底部保存按钮 -->
<div class="save-actions">
<a-space>
<a-button @click="handleReset">
{{ $t('common.reset') }}
</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">
<SaveOutlined />
{{ $t('common.save') }}
</a-button>
</a-space>
</div>
</a-card>
<!-- 底部保存按钮 -->
<div class="save-actions">
<a-space>
<a-button @click="handleReset">
{{ $t('common.reset') }}
</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">
<SaveOutlined />
{{ $t('common.save') }}
</a-button>
</a-space>
</div>
<!-- 配置弹窗 -->
<ConfigModal v-model:visible="modalVisible" :is-edit="isEditMode" :categories="categories"
@@ -107,7 +107,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { SettingOutlined, PlusOutlined, EditOutlined, SaveOutlined } from '@ant-design/icons-vue'
import { SettingOutlined, PlusOutlined, EditOutlined, SaveOutlined, UploadOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
import systemApi from '@/api/system'
import ConfigModal from './components/ConfigModal.vue'
@@ -146,9 +146,43 @@ const currentEditData = ref({})
const fetchFields = async () => {
try {
const res = await systemApi.setting.fields.get()
if (res.code === 200) {
fields.value = res.data.fields || []
categories.value = res.data.categories || categories.value
if (res.code === 1) {
// 根据返回的数据提取配置项
const configData = res.data || []
// 根据 group 字段提取分类
const groupMap = new Map()
configData.forEach(item => {
if (item.group && !groupMap.has(item.group)) {
// 将 group 名称转换为中文标题
const groupTitles = {
'base': '基础设置',
'upload': '上传设置',
'email': '邮件设置',
'sms': '短信设置',
'security': '安全设置',
}
groupMap.set(item.group, {
name: item.group,
title: groupTitles[item.group] || item.group
})
}
})
categories.value = Array.from(groupMap.values())
// 将配置项转换为前端需要的格式
fields.value = configData.map(item => ({
id: item.id,
name: item.name,
title: item.title || item.label,
value: item.values,
type: mapFieldType(item.type),
category: item.group,
placeholder: item.options?.placeholder || item.remark,
tip: item.remark,
options: mapFieldOptions(item.type, item.options?.options || []),
sort: item.sort
}))
// 初始化表单数据
fields.value.forEach(field => {
@@ -166,6 +200,39 @@ const fetchFields = async () => {
}
}
// 映射字段类型
const mapFieldType = (backendType) => {
const typeMap = {
'string': 'text',
'text': 'text',
'textarea': 'textarea',
'number': 'number',
'boolean': 'switch',
'switch': 'switch',
'select': 'select',
'radio': 'select',
'multiselect': 'multiselect',
'checkbox': 'multiselect',
'datetime': 'datetime',
'date': 'datetime',
'color': 'color',
'image': 'image',
'file': 'file',
}
return typeMap[backendType] || 'text'
}
// 映射字段选项
const mapFieldOptions = (type, options) => {
if (options && options.length > 0) {
return options.map(opt => ({
label: opt.label || opt.name || opt,
value: opt.value || opt.key || opt
}))
}
return []
}
// 切换 Tab
const handleTabChange = (key) => {
activeTab.value = key
@@ -189,12 +256,16 @@ const handleAddConfig = () => {
const handleEditField = (field) => {
isEditMode.value = true
currentEditData.value = {
id: field.id,
category: field.category,
name: field.name,
title: field.title,
type: field.type,
value: formData[field.name],
tip: field.tip || '',
remark: field.tip || '',
placeholder: field.placeholder,
options: field.options || []
}
modalVisible.value = true
}
@@ -206,10 +277,11 @@ const handleModalConfirm = async (values) => {
if (isEditMode.value) {
// 编辑模式
res = await systemApi.setting.edit.post({
id: currentEditData.value.id,
...values,
name: currentEditData.value.name,
})
if (res.code === 200) {
if (res.code === 1) {
message.success(t('common.editSuccess'))
modalVisible.value = false
@@ -221,13 +293,14 @@ const handleModalConfirm = async (values) => {
if (fieldIndex > -1) {
fields.value[fieldIndex].title = values.title
fields.value[fieldIndex].value = values.value
fields.value[fieldIndex].tip = values.tip
fields.value[fieldIndex].tip = values.tip || values.remark
fields.value[fieldIndex].placeholder = values.placeholder
}
}
} else {
// 添加模式
res = await systemApi.setting.add.post(values)
if (res.code === 200) {
if (res.code === 1) {
message.success(t('common.addSuccess'))
modalVisible.value = false
// 重新获取配置字段
@@ -248,7 +321,7 @@ const handleSave = async () => {
try {
saving.value = true
const res = await systemApi.setting.save.post(formData)
if (res.code === 200) {
if (res.code === 1) {
message.success(t('common.saveSuccess'))
}
} catch (error) {
@@ -270,6 +343,12 @@ const handleReset = () => {
message.info(t('common.resetSuccess'))
}
// 处理图片上传
const handleUpload = (field) => {
message.info('图片上传功能待实现')
// TODO: 实现图片上传逻辑
}
onMounted(() => {
fetchFields()
})
@@ -317,6 +396,16 @@ onMounted(() => {
color: #8c8c8c;
line-height: 1.4;
}
.image-uploader {
.image-preview {
max-width: 200px;
max-height: 200px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 4px;
}
}
}
.save-actions {