feat: 从冻结 GoAdmin 源码重建 Sense 骨架 (#61)

This commit is contained in:
QiuSW
2026-08-14 11:11:44 +08:00
parent 27f180cec0
commit 96740834d6
702 changed files with 74611 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="字典名称" prop="dictType">
<el-select v-model="queryParams.dictType" size="small">
<el-option
v-for="item in typeOptions"
:key="item.dictId"
:label="item.dictName"
:value="item.dictType"
/>
</el-select>
</el-form-item>
<el-form-item label="字典标签" prop="dictLabel">
<el-input
v-model="queryParams.dictLabel"
placeholder="请输入字典标签"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="数据状态" clearable size="small">
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysDictData:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-permisaction="['admin:sysDictData:edit']" type="primary" size="small" :icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button v-permisaction="['admin:sysDictData:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<el-table v-loading="loading" :data="dataList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="字典编码" width="80" align="center" prop="dictCode" />
<el-table-column label="字典标签" align="center" prop="dictLabel" />
<el-table-column label="字典键值" align="center" prop="dictValue" />
<el-table-column label="字典排序" align="center" prop="dictSort" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="parseInt(scope.row.status) === 2 ? 'success' : 'danger'" size="small" disable-transitions>
{{ statusFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button v-permisaction="['admin:sysDictData:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysDictData:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="字典类型">
<el-input v-model="form.dictType" :disabled="true" />
</el-form-item>
<el-form-item label="数据标签" prop="dictLabel">
<el-input v-model="form.dictLabel" placeholder="请输入数据标签" :disabled="isEdit" />
</el-form-item>
<el-form-item label="数据键值" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入数据键值" :disabled="isEdit" />
</el-form-item>
<el-form-item label="显示排序" prop="dictSort">
<el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listData, getData, delData, addData, updateData, exportData } from '@/api/admin/dict/data'
import { listType, getType } from '@/api/admin/dict/type'
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
export default {
name: 'SysDictDataManage',
setup() {
return { Search, Refresh, Plus, Edit, Delete }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 字典表格数据
dataList: [],
// 默认字典类型
defaultDictType: '',
// 弹出层标题
title: '',
isEdit: false,
// 是否显示弹出层
open: false,
// 状态数据字典
statusOptions: [],
// 类型数据字典
typeOptions: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
dictName: undefined,
dictType: undefined,
status: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
dictLabel: [
{ required: true, message: '数据标签不能为空', trigger: 'blur' }
],
dictValue: [
{ required: true, message: '数据键值不能为空', trigger: 'blur' }
],
dictSort: [
{ required: true, message: '数据顺序不能为空', trigger: 'blur' }
]
}
}
},
created() {
const dictId = this.$route.params && this.$route.params.dictId
this.getType(dictId)
this.getTypeList()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询字典类型详细 */
getType(dictId) {
getType(dictId).then(response => {
this.queryParams.dictType = response.data.dictType
this.defaultDictType = response.data.dictType
this.getList()
})
},
/** 查询字典类型列表 */
getTypeList() {
listType({ pageSize: 1000 }).then(response => {
this.typeOptions = response.data.list
})
},
/** 查询字典数据列表 */
getList() {
this.loading = true
listData(this.queryParams).then(response => {
this.dataList = response.data.list
this.total = response.data.count
this.loading = false
})
},
// 数据状态字典翻译
statusFormat(row, column) {
return this.selectDictLabel(this.statusOptions, row.status)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
dictCode: undefined,
dictLabel: undefined,
dictValue: undefined,
dictSort: 0,
status: '2',
remark: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.queryParams.dictType = this.defaultDictType
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加字典数据'
this.isEdit = false
this.form.dictType = this.queryParams.dictType
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.dictCode)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const dictCode = row.dictCode || this.ids
getData(dictCode).then(response => {
this.form = response.data
this.form.status = String(this.form.status)
this.open = true
this.title = '修改字典数据'
this.isEdit = true
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
this.form.status = parseInt(this.form.status)
if (this.form.dictCode !== undefined) {
updateData(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addData(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const dictCodes = (row.dictCode && [row.dictCode]) || this.ids
this.$confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delData({ 'ids': dictCodes })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams
this.$confirm('是否确认导出所有数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return exportData(queryParams)
}).then(response => {
this.download(response.msg)
}).catch(function() {})
}
}
}
</script>
+322
View File
@@ -0,0 +1,322 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="字典名称" prop="dictName">
<el-input
v-model="queryParams.dictName"
placeholder="请输入字典名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="字典类型" prop="dictType">
<el-input
v-model="queryParams.dictType"
placeholder="请输入字典类型"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="字典状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysDictType:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-permisaction="['admin:sysDictType:edit']" type="primary" size="small" :icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button v-permisaction="['admin:sysDictType:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button v-permisaction="['admin:sysDictType:export']" size="small" :icon="Download" @click="handleExport">导出</el-button>
</div>
<el-table v-loading="loading" :data="typeList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="字典编号" width="80" align="center" prop="id" />
<el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
<el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
<template #default="scope">
<router-link :to="{name:'SysDictDataManage', params: {dictId:scope.row.id}}" class="link-type">
<span>{{ scope.row.dictType }}</span>
</router-link>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="parseInt(scope.row.status) === 2 ? 'success' : 'danger'" size="small" disable-transitions>
{{ statusFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button v-permisaction="['admin:sysDictType:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysDictType:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="字典名称" prop="dictName">
<el-input v-model="form.dictName" placeholder="请输入字典名称" :disabled="isEdit" />
</el-form-item>
<el-form-item label="字典类型" prop="dictType">
<el-input v-model="form.dictType" placeholder="请输入字典类型" :disabled="isEdit" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listType, getType, delType, addType, updateType } from '@/api/admin/dict/type'
import { formatJson } from '@/utils'
import { Search, Refresh, Plus, Edit, Delete, Download } from '@element-plus/icons-vue'
export default {
name: 'SysDictTypeManage',
setup() {
return { Search, Refresh, Plus, Edit, Delete, Download }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 字典表格数据
typeList: [],
// 弹出层标题
title: '',
isEdit: false,
// 是否显示弹出层
open: false,
// 状态数据字典
statusOptions: [],
// 日期范围
dateRange: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
dictName: undefined,
dictType: undefined,
status: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
dictName: [
{ required: true, message: '字典名称不能为空', trigger: 'blur' }
],
dictType: [
{ required: true, message: '字典类型不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.getList()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询字典类型列表 */
getList() {
this.loading = true
listType(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.typeList = response.data.list
this.total = response.data.count
this.loading = false
})
},
// 字典状态字典翻译
statusFormat(row, column) {
return this.selectDictLabel(this.statusOptions, parseInt(row.status))
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
id: undefined,
dictName: undefined,
dictType: undefined,
status: '2',
remark: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加字典类型'
this.isEdit = false
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const dictId = row.id || this.ids
getType(dictId).then(response => {
this.form = response.data
this.form.status = String(this.form.status)
this.open = true
this.title = '修改字典类型'
this.isEdit = true
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
this.form.status = parseInt(this.form.status)
if (this.form.id !== undefined) {
updateType(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addType(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const dictIds = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delType({ 'ids': dictIds })
}).then(() => {
this.getList()
this.msgSuccess('删除成功')
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
// const queryParams = this.queryParams
this.$confirm('是否确认导出所有类型数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['字典编号', '字典名称', '字典类型', '状态', '备注']
const filterVal = ['id', 'dictName', 'dictType', 'status', 'remark']
const list = this.typeList
const data = formatJson(filterVal, list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '字典管理',
autoWidth: true, // Optional
bookType: 'xlsx' // Optional
})
this.downloadLoading = false
})
})
}
}
}
</script>
+426
View File
@@ -0,0 +1,426 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="地址" prop="path">
<el-input
v-model="queryParams.path"
placeholder="请输入地址"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="Method" prop="action">
<el-select
v-model="queryParams.action"
placeholder="请选择Method"
clearable
size="small"
>
<el-option value="GET">GET</el-option>
<el-option value="POST">POST</el-option>
<el-option value="PUT">PUT</el-option>
<el-option value="DELETE">DELETE</el-option>
</el-select>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择类型"
clearable
size="small"
>
<el-option value="SYS">SYS</el-option>
<el-option value="BUS">BUS</el-option>
<el-option value="暂无">暂无</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="sysapiList"
border
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column
label="标题"
header-align="center"
align="left"
prop="title"
fixed="left"
sortable="custom"
width="260px"
:show-overflow-tooltip="true"
>
<template #default="scope">
<span v-if="scope.row.type=='SYS' && scope.row.title!=''"><el-tag type="success">{{ '['+scope.row.type +'] '+ scope.row.title }}</el-tag></span>
<span v-if="scope.row.type!='SYS' && scope.row.title!=''"><el-tag type="">{{ '['+scope.row.type +'] '+scope.row.title }}</el-tag></span>
<span v-if="scope.row.title==''"><el-tag type="danger">暂无</el-tag></span>
</template>
</el-table-column>
<el-table-column
label="Request Info"
header-align="center"
align="left"
prop="path"
sortable="custom"
:show-overflow-tooltip="true"
>
<!-- <template v-slot="scope">
<span>{{ "["+scope.row.action +"] "+ scope.row.path }}</span>
</template> -->
<template #default="scope">
<el-popover trigger="hover" placement="top">
<p><span v-if="scope.row.type=='SYS' && scope.row.title!=''"><el-tag type="success">{{ '['+scope.row.type +'] '+ scope.row.title }}</el-tag></span>
<span v-if="scope.row.type!='SYS' && scope.row.title!=''"><el-tag type="">{{ '['+scope.row.type +'] '+scope.row.title }}</el-tag></span>
<span v-if="scope.row.title==''"><el-tag type="danger">暂无</el-tag></span>
</p>
<p>Handle: {{ scope.row.handle }}</p>
<p>Method:
<el-tag v-if="scope.row.action=='GET'">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='POST'" type="success">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='PUT'" type="warning">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='DELETE'" type="danger">{{ scope.row.action }}</el-tag>
</p>
<p>接口类型: {{ scope.row.type }}</p>
<template #reference><div class="name-wrapper">
<el-tag v-if="scope.row.action=='GET'">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='POST'" type="success">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='PUT'" type="warning">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='DELETE'" type="danger">{{ scope.row.action }}</el-tag>
{{ scope.row.path }}
</div></template>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
width="155px"
sortable="custom"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="80px"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button v-permisaction="['admin:sysApi:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改对话框 -->
<el-drawer
ref="drawer"
v-model="open"
:title="title"
:before-close="cancel"
direction="rtl"
custom-class="demo-drawer"
>
<div class="demo-drawer__content">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="Handle" prop="handle">
<el-input
v-model="form.handle"
placeholder="handle"
/>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="form.title"
placeholder="标题"
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择类型"
clearable
size="small"
@keyup.enter="handleQuery"
>
<el-option value="SYS">SYS</el-option>
<el-option value="BUS">BUS</el-option>
</el-select>
</el-form-item>
<el-form-item label="Method" prop="action">
<el-select
v-model="form.action"
placeholder="请选择方式"
clearable
size="small"
@keyup.enter="handleQuery"
>
<el-option value="GET">GET</el-option>
<el-option value="POST">POST</el-option>
<el-option value="PUT">PUT</el-option>
<el-option value="DELETE">DELETE</el-option>
</el-select>
</el-form-item>
<el-form-item label="地址" prop="path">
<el-input
v-model="form.path"
:disabled="isEdit"
placeholder="地址"
/>
</el-form-item>
</el-form>
<div class="demo-drawer__footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</div>
</el-drawer>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { addSysApi, delSysApi, getSysApi, listSysApi, updateSysApi } from '@/api/admin/sys-api'
import { Search, Refresh, Edit } from '@element-plus/icons-vue'
export default {
name: 'SysApiManage',
components: {
},
setup() {
return { Search, Refresh, Edit }
},
data() {
return {
dialog: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
isEdit: false,
// 类型数据字典
typeOptions: [],
sysapiList: [],
dateRange: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
name: undefined,
title: undefined,
path: undefined,
type: undefined,
action: undefined,
parentId: undefined
},
// 表单参数
form: {
},
// 表单校验
rules: {
title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
path: [{ required: true, message: '地址不能为空', trigger: 'blur' }],
action: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
parentId: [{ required: true, message: '按钮id不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getList()
},
methods: {
handleClose(done) {
if (this.loading) {
return
}
done()
},
/** 查询参数列表 */
getList() {
this.loading = true
listSysApi(this.queryParams).then(response => {
this.sysapiList = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
id: undefined,
name: undefined,
title: undefined,
path: undefined,
paths: undefined,
action: undefined,
parentId: undefined,
sort: undefined
}
this.resetForm('form')
},
parentIdFormat(row) {
return this.selectItemsLabel(this.parentIdOptions, row.parentId)
},
// 文件
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加接口管理'
this.isEdit = false
},
/** 排序回调函数 */
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (this.order !== '' && this.order !== prop + 'Order') {
this.queryParams[this.order] = undefined
}
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
this.order = prop + 'Order'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
this.order = prop + 'Order'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id =
row.id || this.ids
getSysApi(id).then(response => {
this.form = response.data
this.open = true
this.title = '修改接口管理'
this.isEdit = true
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.id !== undefined) {
updateSysApi(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addSysApi(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
var Ids = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delSysApi({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
}
}
</script>
@@ -0,0 +1,432 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" label-width="48px">
<el-form-item label="名称" prop="configName">
<el-input
v-model="queryParams.configName"
placeholder="请输入参数名称"
clearable
size="small"
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="键名" prop="configKey">
<el-input
v-model="queryParams.configKey"
placeholder="请输入参数键名"
clearable
size="small"
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="内置" prop="configType">
<el-select v-model="queryParams.configType" placeholder="系统内置" clearable size="small">
<el-option
v-for="dict in typeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="handleQuery">搜索</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysConfig:add']"
type="primary"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysConfig:edit']"
type="success"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysConfig:remove']"
type="danger"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysConfig:export']"
type="warning"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="configList"
border
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column
label="编码"
sortable="custom"
width="75"
prop="id"
/>
<el-table-column
label="名称"
sortable="custom"
prop="configName"
:show-overflow-tooltip="true"
/>
<el-table-column
label="键名"
sortable="custom"
prop="configKey"
>
<template #default="scope">
<el-popover trigger="hover" placement="top">
<p>键值: {{ scope.row.configValue }}</p>
<p>UI参数: <el-tag v-if="scope.row.isFrontend=='2'">否</el-tag>
<el-tag v-if="scope.row.isFrontend=='1'" type="success">是</el-tag>
</p>
<template #reference><div class="name-wrapper">
{{ scope.row.configKey }}
</div></template>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="内置"
sortable="custom"
prop="configType"
:formatter="typeFormat"
width="80"
/>
<el-table-column
label="备注"
prop="remark"
:show-overflow-tooltip="true"
/>
<el-table-column
label="创建时间"
sortable="custom"
prop="createdAt"
width="160"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
class-name="small-padding fixed-width"
width="120"
>
<template #default="scope">
<el-button
v-permisaction="['admin:sysConfig:edit']"
size="mini"
type="text"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-permisaction="['admin:sysConfig:remove']"
size="mini"
type="text"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="参数名称" prop="configName">
<el-input v-model="form.configName" placeholder="请输入参数名称" :disabled="isEdit" />
</el-form-item>
<el-form-item label="参数键名" prop="configKey">
<el-input v-model="form.configKey" placeholder="请输入参数键名" :disabled="isEdit" />
</el-form-item>
<el-form-item label="参数键值" prop="configValue">
<el-input v-model="form.configValue" placeholder="请输入参数键值" />
</el-form-item>
<el-form-item label="系统内置" prop="configType">
<el-radio-group v-model="form.configType">
<el-radio
v-for="dict in typeOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="前台显示" prop="isFrontend">
<el-select v-model="form.isFrontend" placeholder="是否前台显示" clearable size="small">
<el-option label="是" value="1" />
<el-option label="否" value="2" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listConfig, getConfig, delConfig, addConfig, updateConfig } from '@/api/admin/sys-config'
import { formatJson } from '@/utils'
export default {
name: 'SysConfigManage',
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 参数表格数据
configList: [],
// 排序字段
order: 'createdAtOrder',
// 弹出层标题
title: '',
isEdit: false,
// 是否显示弹出层
open: false,
// 类型数据字典
typeOptions: [],
// 日期范围
dateRange: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
configName: undefined,
configKey: undefined,
configType: undefined,
createdAtOrder: 'desc'
},
// 表单参数
form: {},
// 表单校验
rules: {
configName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
configKey: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
configValue: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
isFrontend: [{ required: true, message: '是否前台显示不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getList()
this.getDicts('sys_yes_no').then(response => {
this.typeOptions = response.data
})
},
methods: {
/** 查询参数列表 */
getList() {
this.loading = true
listConfig(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.configList = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
// 参数系统内置字典翻译
typeFormat(row, column) {
return this.selectDictLabel(this.typeOptions, row.configType)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
id: undefined,
configName: undefined,
configKey: undefined,
configValue: undefined,
configType: 'Y',
isFrontend: '1',
remark: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.queryParams['createdAtOrderOrder'] = 'desc'
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加参数'
this.isEdit = false
},
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (this.order !== '' && this.order !== prop + 'Order') {
this.queryParams[this.order] = undefined
}
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
this.order = prop + 'Order'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
this.order = prop + 'Order'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const ID = row.id || this.ids
getConfig(ID).then(response => {
this.form = response.data
this.open = true
this.title = '修改参数'
this.isEdit = true
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.id !== undefined) {
updateConfig(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addConfig(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const Ids = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除参数编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delConfig({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
// const queryParams = this.queryParams
this.$confirm('是否确认导出所有参数数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['参数主键', '参数名称', '参数键名', '参数键值', '备注', '创建时间']
const filterVal = ['configId', 'configName', 'configKey', 'configValue', 'remark', 'createdAt']
const list = this.configList
const data = formatJson(filterVal, list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '参数设置',
autoWidth: true, // Optional
bookType: 'xlsx' // Optional
})
this.downloadLoading = false
})
})
}
}
}
</script>
+604
View File
@@ -0,0 +1,604 @@
<template>
<BasicLayout>
<template #wrapper>
<div class="sys-config-set">
<div class="config-wrapper">
<!-- ── 左侧导航 ── -->
<div class="config-sidebar">
<div class="sidebar-header">
<div class="sidebar-icon-box">
<el-icon :size="18" color="#1677ff"><Setting /></el-icon>
</div>
<div>
<p class="sidebar-header__title">系统配置</p>
<p class="sidebar-header__sub">基础信息与外观设置</p>
</div>
</div>
<div class="sidebar-divider" />
<div
v-for="(item, idx) in navItems"
:key="idx"
class="nav-item"
:class="{ 'is-active': activeSection === idx }"
@click="activeSection = idx"
>
<div class="nav-item__icon-wrap">
<el-icon :size="15"><component :is="item.icon" /></el-icon>
</div>
<div class="nav-item__text">
<span class="nav-item__label">{{ item.label }}</span>
<span class="nav-item__sub">{{ item.sub }}</span>
</div>
</div>
</div>
<!-- ── 右侧主内容 ── -->
<div class="config-main">
<!-- 基础信息 -->
<transition name="fade-slide" mode="out-in">
<div v-if="activeSection === 0" key="basic" class="config-section">
<div class="section-head">
<div class="section-head__icon-wrap">
<el-icon :size="16" color="#1677ff"><OfficeBuilding /></el-icon>
</div>
<div>
<p class="section-head__title">系统基础信息</p>
<p class="section-head__desc">配置系统名称、Logo 及用户默认密码</p>
</div>
</div>
<div class="section-body">
<!-- Logo -->
<div class="logo-zone">
<div class="logo-preview">
<img v-if="form.sys_app_logo" :src="form.sys_app_logo" alt="logo" class="logo-img">
<div v-else class="logo-empty">
<el-icon :size="24" color="#c0c4cc"><Picture /></el-icon>
<span>暂无 Logo</span>
</div>
</div>
<div class="logo-meta">
<p class="logo-meta__name">系统 Logo</p>
<p class="logo-meta__hint">推荐 200×200,支持 JPG / PNG,大小不超过 2MB</p>
<el-upload
:headers="headers"
:file-list="sys_app_logofileList"
:action="sys_app_logoAction"
:before-upload="sys_app_logoBeforeUpload"
:show-file-list="false"
:on-success="uploadSuccess"
>
<el-button size="small" plain type="primary">
<el-icon style="margin-right:4px"><Upload /></el-icon>上传图片
</el-button>
</el-upload>
</div>
</div>
<el-divider style="margin: 16px 0" />
<el-form ref="form" :model="form" :rules="rules" label-position="top">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="系统名称" prop="sys_app_name">
<el-input v-model="form.sys_app_name" placeholder="请输入系统名称" clearable>
<template #prefix><el-icon><OfficeBuilding /></el-icon></template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户初始密码" prop="sys_user_initPassword">
<el-input v-model="form.sys_user_initPassword" placeholder="请输入初始密码" clearable show-password>
<template #prefix><el-icon><Lock /></el-icon></template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
<!-- 外观设置 -->
<div v-else-if="activeSection === 1" key="theme" class="config-section">
<div class="section-head">
<div class="section-head__icon-wrap">
<el-icon :size="16" color="#1677ff"><Brush /></el-icon>
</div>
<div>
<p class="section-head__title">外观设置</p>
<p class="section-head__desc">调整皮肤样式与侧栏主题风格</p>
</div>
</div>
<div class="section-body">
<el-form label-position="top">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="皮肤样式">
<el-select v-model="form.sys_index_skinName" placeholder="请选择皮肤样式" style="width:100%">
<el-option v-for="(item, i) in sys_index_skinNameOptions" :key="i" :label="item.label" :value="item.value" :disabled="item.disabled" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="侧栏主题">
<el-select v-model="form.sys_index_sideTheme" placeholder="请选择侧栏主题" style="width:100%">
<el-option v-for="(item, i) in sys_index_sideThemeOptions" :key="i" :label="item.label" :value="item.value" :disabled="item.disabled" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<p class="theme-label">点击快速切换主题</p>
<div class="theme-row">
<div
class="theme-card"
:class="{ 'is-active': form.sys_index_sideTheme === 'theme-dark' }"
@click="form.sys_index_sideTheme = 'theme-dark'"
>
<div class="tc-preview tc-preview--dark">
<div class="tc-sidebar" />
<div class="tc-body">
<div class="tc-topbar" />
<div class="tc-content" />
</div>
</div>
<div class="tc-label">
<span>深色主题</span>
<el-icon v-if="form.sys_index_sideTheme === 'theme-dark'" color="#1677ff"><Check /></el-icon>
</div>
</div>
<div
class="theme-card"
:class="{ 'is-active': form.sys_index_sideTheme === 'theme-light' }"
@click="form.sys_index_sideTheme = 'theme-light'"
>
<div class="tc-preview tc-preview--light">
<div class="tc-sidebar" />
<div class="tc-body">
<div class="tc-topbar" />
<div class="tc-content" />
</div>
</div>
<div class="tc-label">
<span>浅色主题</span>
<el-icon v-if="form.sys_index_sideTheme === 'theme-light'" color="#1677ff"><Check /></el-icon>
</div>
</div>
</div>
</div>
</div>
</transition>
<!-- 操作栏 -->
<div class="config-footer">
<el-button @click="resetForm">重置</el-button>
<el-button type="primary" :loading="saving" @click="submitForm">保存设置</el-button>
</div>
</div>
</div>
</div>
</template>
</BasicLayout>
</template>
<script>
import { getSetConfig, updateSetConfig } from '@/api/admin/sys-config'
import { getToken } from '@/utils/auth'
import { Setting, OfficeBuilding, Lock, Picture, Upload, Brush, Check } from '@element-plus/icons-vue'
export default {
name: 'SysConfigSet',
setup() {
return { Setting, OfficeBuilding, Lock, Picture, Upload, Brush, Check }
},
data() {
return {
activeSection: 0,
saving: false,
loading: false,
configList: [],
headers: { 'Authorization': 'Bearer ' + getToken() },
form: {
sys_app_name: undefined,
sys_app_logo: null,
sys_user_initPassword: undefined,
sys_index_skinName: undefined,
sys_index_sideTheme: undefined
},
rules: {
sys_app_name: [{ required: true, message: '请输入系统名称', trigger: 'blur' }],
sys_user_initPassword: [{ required: true, message: '请输入初始密码', trigger: 'blur' }]
},
sys_app_logoAction: process.env.VUE_APP_BASE_API + '/api/v1/public/uploadFile',
sys_app_logofileList: [],
sys_index_skinNameOptions: [{ label: '蓝色', value: 'skin-blue' }],
sys_index_sideThemeOptions: [
{ label: '深色主题', value: 'theme-dark' },
{ label: '浅色主题', value: 'theme-light' }
]
}
},
computed: {
navItems() {
return [
{ label: '基础信息', sub: '名称、Logo、密码', icon: this.OfficeBuilding },
{ label: '外观设置', sub: '皮肤与主题风格', icon: this.Brush }
]
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
getSetConfig().then(response => {
this.form = response.data
this.loading = false
})
},
submitForm() {
this.$refs['form'].validate(valid => {
if (!valid) return
this.saving = true
const list = Object.keys(this.form).map(key => ({ configKey: key, configValue: this.form[key] }))
updateSetConfig(list).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.getList()
const { sys_app_name, sys_app_logo } = this.form
this.$store.commit('system/SET_INFO', { sys_app_logo, sys_app_name })
} else {
this.msgError(response.msg)
}
}).finally(() => { this.saving = false })
})
},
resetForm() {
this.$refs['form'].resetFields()
},
sys_app_logoBeforeUpload(file) {
const ok = file.size / 1024 / 1024 < 2
if (!ok) this.$message.error('文件大小超过 2MB')
return ok
},
uploadSuccess(response) {
this.form.sys_app_logo = process.env.VUE_APP_BASE_API + response.data.full_path
}
}
}
</script>
<style lang="scss" scoped>
// ── 页面容器 ──────────────────────────────────────────────
.sys-config-set {
padding: 0;
}
.config-wrapper {
display: flex;
gap: 16px;
align-items: flex-start;
}
// ══════════════════════════════
// 左侧导航卡
// ══════════════════════════════
.config-sidebar {
width: 188px;
flex-shrink: 0;
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
overflow: hidden;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 14px 12px;
}
.sidebar-icon-box {
width: 34px;
height: 34px;
border-radius: 8px;
background: #e6f4ff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.sidebar-header__title {
margin: 0 0 2px;
font-size: 14px;
font-weight: 600;
color: #000000d9;
line-height: 1;
}
.sidebar-header__sub {
margin: 0;
font-size: 11px;
color: #00000073;
line-height: 1;
}
.sidebar-divider {
height: 1px;
background: #f0f0f0;
margin: 0;
}
// 导航项
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 14px;
cursor: pointer;
border-left: 2px solid transparent;
transition: all 0.15s;
&:hover:not(.is-active) { background: #fafafa; }
&.is-active {
background: #e6f4ff;
border-left-color: #1677ff;
.nav-item__icon-wrap { color: #1677ff; background: rgba(22,119,255,0.1); }
.nav-item__label { color: #1677ff; font-weight: 600; }
}
&:not(:last-child) { border-bottom: 1px solid #f5f5f5; }
}
.nav-item__icon-wrap {
width: 28px;
height: 28px;
border-radius: 6px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #00000073;
flex-shrink: 0;
transition: all 0.15s;
}
.nav-item__text {
display: flex;
flex-direction: column;
gap: 1px;
}
.nav-item__label {
font-size: 13px;
color: #000000d9;
font-weight: 500;
line-height: 1.3;
}
.nav-item__sub {
font-size: 11px;
color: #00000045;
line-height: 1;
}
// ══════════════════════════════
// 右侧主内容
// ══════════════════════════════
.config-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
// 内容分区卡片
.config-section {
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
overflow: hidden;
}
// 分区头部
.section-head {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.section-head__icon-wrap {
width: 30px;
height: 30px;
border-radius: 6px;
background: #e6f4ff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.section-head__title {
margin: 0 0 2px;
font-size: 14px;
font-weight: 600;
color: #000000d9;
line-height: 1;
}
.section-head__desc {
margin: 0;
font-size: 12px;
color: #00000073;
line-height: 1;
}
// 分区内容体
.section-body {
padding: 16px;
}
// ── Logo 区域 ──────────────────────────────────────────
.logo-zone {
display: flex;
align-items: center;
gap: 16px;
padding: 14px;
background: #fafafa;
border-radius: 6px;
border: 1px dashed #d9d9d9;
}
.logo-preview {
width: 72px;
height: 72px;
border-radius: 8px;
border: 1px solid #e8e8e8;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
}
.logo-img {
width: 100%;
height: 100%;
object-fit: contain;
}
.logo-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: #c0c4cc;
font-size: 11px;
}
.logo-meta { flex: 1; }
.logo-meta__name {
margin: 0 0 3px;
font-size: 13px;
font-weight: 500;
color: #000000d9;
}
.logo-meta__hint {
margin: 0 0 10px;
font-size: 12px;
color: #00000073;
line-height: 1.5;
}
// ── 主题选择 ─────────────────────────────────────────────
.theme-label {
margin: 12px 0 10px;
font-size: 12px;
color: #00000073;
}
.theme-row {
display: flex;
gap: 12px;
}
.theme-card {
width: 160px;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
&:hover { border-color: #4096ff; box-shadow: 0 2px 8px rgba(22,119,255,0.12); }
&.is-active {
border-color: #1677ff;
box-shadow: 0 2px 8px rgba(22,119,255,0.2);
}
}
.tc-preview {
display: flex;
height: 80px;
}
.tc-preview--dark {
background: #1e2a3b;
.tc-sidebar { background: #001529; }
.tc-topbar { background: #162131; height: 16px; }
.tc-content { flex: 1; background: #1e2a3b; }
}
.tc-preview--light {
background: #f5f5f5;
.tc-sidebar { background: #fff; border-right: 1px solid #f0f0f0; }
.tc-topbar { background: #fff; height: 16px; border-bottom: 1px solid #f0f0f0; }
.tc-content { flex: 1; background: #f5f5f5; }
}
.tc-sidebar {
width: 36px;
height: 100%;
flex-shrink: 0;
}
.tc-body {
flex: 1;
display: flex;
flex-direction: column;
}
.tc-label {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
font-size: 12px;
color: #000000d9;
background: #fff;
border-top: 1px solid #f0f0f0;
}
// ── 底部操作栏 ────────────────────────────────────────────
.config-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
// ── 过渡动画 ─────────────────────────────────────────────
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: opacity 0.15s, transform 0.15s;
}
.fade-slide-enter-from { opacity: 0; transform: translateY(6px); }
.fade-slide-leave-to { opacity: 0; transform: translateY(-6px); }
</style>
+377
View File
@@ -0,0 +1,377 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<!-- 搜索区 -->
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="部门名称" prop="deptName">
<el-input
v-model="queryParams.deptName"
placeholder="请输入部门名称"
clearable
size="small"
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="部门状态"
clearable
size="small"
style="width: 140px"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysDept:add']"
type="primary"
size="small"
:icon="Plus"
@click="handleAdd()"
>新增</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table
v-loading="loading"
:data="deptList"
row-key="deptId"
default-expand-all
border
stripe
highlight-current-row
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="deptName" label="部门名称" min-width="160" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope">
<el-tag
:type="scope.row.status === 1 ? 'danger' : 'success'"
size="small"
disable-transitions
>{{ statusFormat(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
v-permisaction="['admin:sysDept:edit']"
size="small"
type="primary"
link
:icon="Edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-divider direction="vertical" />
<el-button
v-permisaction="['admin:sysDept:add']"
size="small"
type="primary"
link
:icon="Plus"
@click="handleAdd(scope.row)"
>新增</el-button>
<template v-if="scope.row.p_id != 0">
<el-divider direction="vertical" />
<el-button
v-permisaction="['admin:sysDept:remove']"
size="small"
type="danger"
link
:icon="Delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 添加或修改部门对话框 -->
<el-dialog v-model="open" :title="title" width="600px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="24">
<el-form-item label="上级部门" prop="parentId">
<treeselect
v-model="form.parentId"
:options="deptOptions"
:normalizer="normalizer"
:show-count="true"
placeholder="选择上级部门"
:is-disabled="isEdit"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门名称" prop="deptName">
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.sort" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="leader">
<el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { getDeptList, getDept, delDept, addDept, updateDept } from '@/api/admin/sys-dept'
import Treeselect from 'vue3-treeselect'
import 'vue3-treeselect/dist/vue3-treeselect.css'
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
export default {
name: 'SysDeptManage',
components: { Treeselect },
setup() {
return { Search, Refresh, Plus, Edit, Delete }
},
data() {
return {
loading: true,
deptList: [],
deptOptions: [],
title: '',
isEdit: false,
open: false,
statusOptions: [],
queryParams: {
deptName: undefined,
status: undefined
},
form: {},
rules: {
parentId: [
{ required: true, message: '上级部门不能为空', trigger: 'blur' }
],
deptName: [
{ required: true, message: '部门名称不能为空', trigger: 'blur' }
],
sort: [
{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }
],
leader: [
{ required: true, message: '负责人不能为空', trigger: 'blur' }
],
email: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
],
phone: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
]
}
}
},
created() {
this.getList()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
},
methods: {
getList() {
this.loading = true
getDeptList(this.queryParams).then(response => {
this.deptList = response.data
this.loading = false
})
},
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.deptId,
label: node.deptName,
children: node.children
}
},
getTreeselect(e) {
getDeptList().then(response => {
this.deptOptions = []
if (e === 'update') {
const dept = { deptId: 0, deptName: '主类目', children: [], isDisabled: true }
dept.children = response.data
this.deptOptions.push(dept)
} else {
const dept = { deptId: 0, deptName: '主类目', children: [] }
dept.children = response.data
this.deptOptions.push(dept)
}
})
},
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, parseInt(row.status))
},
cancel() {
this.open = false
this.reset()
},
reset() {
this.form = {
deptId: undefined,
parentId: undefined,
deptName: undefined,
sort: 10,
leader: undefined,
phone: undefined,
email: undefined,
status: '2'
}
},
handleQuery() {
this.getList()
},
handleResetQuery() {
this.queryParams = { deptName: undefined, status: undefined }
this.getList()
},
handleAdd(row) {
this.reset()
this.getTreeselect('add')
if (row !== undefined) {
this.form.parentId = row.deptId
}
this.open = true
this.title = '添加部门'
this.isEdit = false
},
handleUpdate(row) {
this.reset()
this.getTreeselect('update')
getDept(row.deptId).then(response => {
this.form = response.data
this.form.status = String(this.form.status)
this.form.sort = String(this.form.sort)
this.open = true
this.title = '修改部门'
this.isEdit = true
})
},
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
this.form.status = parseInt(this.form.status)
this.form.sort = parseInt(this.form.sort)
if (this.form.deptId !== undefined) {
updateDept(this.form, this.form.deptId).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addDept(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
handleDelete(row) {
const Ids = (row.deptId && [row.deptId]) || this.ids
this.$confirm(
'是否确认删除名称为"' + row.deptName + '"的数据项?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(function() {
return delDept({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
}
}
</script>
@@ -0,0 +1,287 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="用户名" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="系统登录日志状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="ip地址" prop="ipaddr">
<el-input
v-model="queryParams.ipaddr"
placeholder="请输入ip地址"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysLoginLog:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<el-table v-loading="loading" :data="sysloginlogList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column
label="用户名"
align="center"
prop="username"
:show-overflow-tooltip="true"
/>
<el-table-column
label="类型"
align="center"
prop="msg"
:show-overflow-tooltip="true"
/>
<el-table-column
label="状态"
align="center"
prop="status"
width="100"
>
<template #default="scope">
<el-tag :type="scope.row.status === '2' || scope.row.status === 2 ? 'success' : 'danger'" size="small" disable-transitions>
{{ statusFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="ip地址"
align="center"
prop="ipaddr"
>
<template #default="scope">
<el-popover trigger="hover" placement="top">
<p>IP: {{ scope.row.ipaddr }}</p>
<p>归属地: {{ scope.row.loginLocation }}</p>
<p>浏览器: {{ scope.row.browser }}</p>
<p>系统: {{ scope.row.os }}</p>
<p>固件: {{ scope.row.platform }}</p>
<template #reference><div class="name-wrapper">
{{ scope.row.ipaddr }}
</div></template>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="登录时间"
align="center"
prop="loginTime"
width="180"
>
<template #default="scope">
<span>{{ parseTime(scope.row.loginTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button v-permisaction="['admin:sysLoginLog:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { delSysLoginlog, getSysLoginlog, listSysLoginlog } from '@/api/admin/sys-login-log'
import { Search, Refresh, Delete } from '@element-plus/icons-vue'
export default {
name: 'SysLoginLogManage',
components: {
},
setup() {
return { Search, Refresh, Delete }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
isEdit: false,
fileOpen: false,
fileIndex: undefined,
// 类型数据字典
typeOptions: [],
sysloginlogList: [],
statusOptions: [],
// 关系表类型
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
username: undefined,
status: undefined,
ipaddr: undefined,
loginLocation: undefined,
createdAtOrder: 'desc'
},
// 表单参数
form: {
},
// 表单校验
rules: {
}
}
},
created() {
this.getList()
this.getDicts('sys_common_status').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询参数列表 */
getList() {
this.loading = true
listSysLoginlog(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.sysloginlogList = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
ID: undefined,
username: undefined,
status: undefined,
ipaddr: undefined,
loginLocation: undefined,
browser: undefined,
os: undefined,
platform: undefined,
loginTime: undefined,
remark: undefined,
msg: undefined
}
this.resetForm('form')
},
getImgList: function() {
this.form[this.fileIndex] = this.$refs['fileChoose'].resultList[0].fullUrl
},
fileClose: function() {
this.fileOpen = false
},
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, row.status)
},
// 关系
// 文件
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加系统登录日志'
this.isEdit = false
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const ID =
row.id || this.ids
getSysLoginlog(ID).then(response => {
this.form = response.data
this.open = true
this.title = '修改系统登录日志'
this.isEdit = true
})
},
/** 删除按钮操作 */
handleDelete(row) {
var Ids = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delSysLoginlog({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
}
}
</script>
+634
View File
@@ -0,0 +1,634 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form :inline="true" class="search-form">
<el-form-item label="菜单名称">
<el-input
v-model="queryParams.title"
placeholder="请输入菜单名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.visible" placeholder="菜单状态" clearable size="small">
<el-option
v-for="dict in visibleOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button v-permisaction="['admin:sysMenu:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="menuList"
border
stripe
row-key="menuId"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
<el-table-column prop="title" label="菜单名称" :show-overflow-tooltip="true" width="180px" />
<el-table-column prop="icon" label="图标" align="center" width="100px">
<template #default="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="60px" />
<el-table-column prop="permission" label="权限标识" :show-overflow-tooltip="true">
<template #default="scope">
<el-popover v-if="scope.row.sysApi.length>0" trigger="hover" placement="top">
<el-table
:data="scope.row.sysApi"
border
style="width: 100%"
>
<el-table-column
prop="title"
label="title"
width="260px"
>
<template #default="scope">
<span v-if="scope.row.type=='SYS' && scope.row.title!=''"><el-tag type="success">{{ '['+scope.row.type +'] '+ scope.row.title }}</el-tag></span>
<span v-if="scope.row.type!='SYS' && scope.row.title!=''"><el-tag type="">{{ '['+scope.row.type +'] '+scope.row.title }}</el-tag></span>
<span v-if="scope.row.title==''"><el-tag type="danger">暂无</el-tag></span>
</template>
</el-table-column>
<el-table-column
prop="path"
label="path"
width="270px"
>
<template #default="scope">
<el-tag v-if="scope.row.action=='GET'">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='POST'" type="success">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='PUT'" type="warning">{{ scope.row.action }}</el-tag>
<el-tag v-if="scope.row.action=='DELETE'" type="danger">{{ scope.row.action }}</el-tag>
{{ scope.row.path }}
</template>
</el-table-column>
</el-table>
<template #reference><div class="name-wrapper">
<span v-if="scope.row.permission==''">-</span>
<span v-else>{{ scope.row.permission }}</span>
</div></template>
</el-popover>
<span v-else>
<span v-if="scope.row.permission==''">-</span>
<span v-else>{{ scope.row.permission }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="path" label="组件路径" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="scope.row.menuType=='A'">{{ scope.row.path }}</span>
<span v-else>{{ scope.row.component }}</span>
</template>
</el-table-column>
<el-table-column prop="visible" label="可见" :formatter="visibleFormat" width="80">
<template #default="scope">
<el-tag
:type="scope.row.visible === '1' ? 'danger' : 'success'"
disable-transitions
>{{ visibleFormat(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-button v-permisaction="['admin:sysMenu:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysMenu:add']" type="primary" link size="small" :icon="Plus" @click="handleAdd(scope.row)">新增</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysMenu:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加或修改菜单对话框 -->
<el-drawer
ref="drawer"
v-model="open"
:title="title"
:before-close="cancel"
direction="rtl"
custom-class="demo-drawer"
size="830px"
>
<div class="demo-drawer__content">
<el-form ref="form" :model="form" :rules="rules" label-position="top" label-width="106px">
<el-row>
<el-col :span="24">
<el-form-item prop="parentId">
<template #label>
<span>
上级菜单
<el-tooltip content="指当前菜单停靠的菜单归属" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<treeselect
v-model="form.parentId"
:options="menuOptions"
:normalizer="normalizer"
:show-count="true"
placeholder="选择上级菜单"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="title">
<template #label>
<span>
菜单标题
<el-tooltip content="菜单位置显示的说明信息" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.title" placeholder="请输入菜单标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sort">
<template #label>
<span>
显示排序
<el-tooltip content="根据序号升序排列" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input-number v-model="form.sort" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="menuType">
<template #label>
<span>
菜单类型
<el-tooltip content="包含目录:以及菜单或者菜单组,菜单:具体对应某一个页面,按钮:功能才做按钮;" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.menuType">
<el-radio label="M">目录</el-radio>
<el-radio label="C">菜单</el-radio>
<el-radio label="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单图标">
<el-popover
placement="bottom-start"
width="460"
trigger="click"
@show="$refs['iconSelect'].reset()"
>
<IconSelect ref="iconSelect" @selected="selected" />
<template #reference>
<el-input v-model="form.icon" placeholder="点击选择图标" readonly>
<template #prefix>
<svg-icon
v-if="form.icon"
:icon-class="form.icon"
class="el-input__icon"
style="height: 32px;width: 16px;"
/>
<i v-else class="ri-search-line el-input__icon" />
</template>
</el-input>
</template>
</el-popover>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == 'M' || form.menuType == 'C'" prop="menuName">
<template #label>
<span>
路由名称
<el-tooltip content="需要和页面name保持一致,对应页面即可选择缓存" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.menuName" placeholder="请输入路由名称" />
</el-form-item>
</el-col>
<el-col v-if="form.menuType == 'M' || form.menuType == 'C'" :span="12">
<el-form-item prop="component">
<template #label>
<span>
组件路径
<el-tooltip content="菜单对应的具体vue页面文件路径views的下级路径/admin/sys-api/index;目录类型:填写Layout,如何有二级目录请参照日志目录填写;" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == 'M' || form.menuType == 'C'">
<template #label>
<span>
是否外链
<el-tooltip content="可以通过iframe打开指定地址" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio label="0">是</el-radio>
<el-radio label="1">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != 'F'" prop="path">
<template #label>
<span>
路由地址
<el-tooltip content="访问此页面自定义的url地址,建议/开头书写,例如 /app-name/menu-name" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType == 'F' || form.menuType == 'C'">
<template #label>
<span>
权限标识
<el-tooltip content="前端权限控制按钮是否显示" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-input v-model="form.permission" placeholder="请权限标识" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.menuType != 'F'">
<template #label>
<span>
菜单状态
<el-tooltip content="需要显示在菜单列表的菜单设置为显示,否则设置为隐藏" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-radio-group v-model="form.visible">
<el-radio
v-for="dict in visibleOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item v-if="form.menuType == 'F' || form.menuType == 'C'">
<template #label>
<span>
api权限
<el-tooltip content="配置在这个才做上需要使用到的接口,否则在设置用户角色时,接口将无权访问。" placement="top">
<i class="ri-question-line" />
</el-tooltip>
</span>
</template>
<el-transfer
v-model="form.apis"
style="text-align: left; display: inline-block"
filterable
:props="{
key: 'id',
label: 'title'
}"
:titles="['未授权', '已授权']"
:button-texts="['收回', '授权 ']"
:format="{
noChecked: '${total}',
hasChecked: '${checked}/${total}'
}"
class="panel"
:data="sysapiList"
@change="handleChange"
>
<template #default="{ option }">
<span>{{ option.title }}</span>
</template>
</el-transfer>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="demo-drawer__footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</div>
</el-drawer>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listMenu, getMenu, delMenu, addMenu, updateMenu } from '@/api/admin/sys-menu'
import { listSysApi } from '@/api/admin/sys-api'
import Treeselect from 'vue3-treeselect'
import 'vue3-treeselect/dist/vue3-treeselect.css'
import IconSelect from '@/components/IconSelect'
import { Search, Plus, Edit, Delete } from '@element-plus/icons-vue'
export default {
name: 'SysMenuManage',
components: { Treeselect, IconSelect },
setup() {
return { Search, Plus, Edit, Delete }
},
data() {
return {
// 遮罩层
loading: true,
// 菜单表格树数据
menuList: [],
sysapiList: [],
// 菜单树选项
menuOptions: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 菜单状态数据字典
visibleOptions: [],
// 查询参数
queryParams: {
title: undefined,
visible: undefined
},
// 表单参数
form: {
apis: [],
sysApi: []
},
// 表单校验
rules: {
title: [{ required: true, message: '菜单标题不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getList()
this.getApiList()
this.getDicts('sys_show_hide').then(response => {
this.visibleOptions = response.data
})
},
methods: {
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys)
const list = this.form.sysApi
this.form.apis = value
if (direction === 'right') {
for (let x = 0; x < movedKeys.length; x++) {
for (let index = 0; index < this.sysapiList.length; index++) {
const element = this.sysapiList[index]
if (element.id === movedKeys[x]) {
list.push(element)
break
}
}
}
this.form.sysApi = list
} else if (direction === 'left') {
const l = []
for (let index = 0; index < movedKeys.length; index++) {
const element = movedKeys[index]
for (let x = 0; x < list.length; x++) {
const e = list[x]
if (element !== e.id) {
l.push()
break
}
}
}
this.form.sysApi = l
}
// this.setApis(this.form.SysApi)
console.log(this.form.sysApi)
},
getApiList() {
this.loading = true
listSysApi({ 'pageSize': 10000, 'type': 'BUS' }).then(response => {
this.sysapiList = response.data.list
this.loading = false
}
)
},
handleClose(done) {
// if (this.loading) {
// return
// }
// this.$confirm('需要提交表单吗?')
// .then(_ => {
// this.loading = true
// this.timer = setTimeout(() => {
// done()
// // 动画关闭需要一定的时间
// setTimeout(() => {
// this.loading = false
// }, 400)
// }, 1000)
// })
// .catch(_ => {})
},
// 选择图标
selected(name) {
this.form.icon = name
},
/** 查询菜单列表 */
getList() {
this.loading = true
listMenu(this.queryParams).then(response => {
this.menuList = response.data
this.loading = false
})
},
/** 转换菜单数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.menuId,
label: node.title,
children: node.children
}
},
/** 查询菜单下拉树结构 */
getTreeselect() {
listMenu().then(response => {
this.menuOptions = []
const menu = { menuId: 0, title: '主类目', children: [] }
menu.children = response.data
this.menuOptions.push(menu)
})
},
// 菜单显示状态字典翻译
visibleFormat(row) {
if (row.menuType === 'F') {
return '-- --'
}
return this.selectDictLabel(this.visibleOptions, row.visible)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
menuId: undefined,
parentId: 0,
menuName: undefined,
icon: undefined,
menuType: 'M',
apis: [],
sort: 0,
action: this.form.menuType === 'A' ? this.form.action : '',
isFrame: '1',
visible: '0'
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.getList()
},
/** 新增按钮操作 */
handleAdd(row) {
this.reset()
this.getTreeselect()
if (row != null) {
this.form.parentId = row.menuId
}
this.open = true
this.title = '添加菜单'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
this.getTreeselect()
getMenu(row.menuId).then(response => {
this.form = response.data
this.open = true
this.title = '修改菜单'
})
},
setApis(apiArray) {
var l = []
for (var index = 0; index < apiArray.length; index++) {
const element = apiArray[index]
l.push(element.id)
}
this.form.apis = l
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.menuId !== undefined) {
updateMenu(this.form, this.form.menuId).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addMenu(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除名称为"' + row.title + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
var Ids = (row.menuId && [row.menuId]) || this.ids
return delMenu({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
}
}
</script>
<style lang="css">
.panel .el-transfer__buttons{
width: 150px;
}
.panel .el-transfer__buttons .el-button + .el-button{
margin-left:0;
}
.panel .el-transfer-panel{
width: 300px;
}
.el-col {
padding: 0 5px;
}
.el-drawer__header{
margin-bottom: 0;
}
</style>
@@ -0,0 +1,314 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="操作状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
size="small"
type="datetimerange"
:picker-options="pickerOptions"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
align="right"
value-format="yyyy-MM-dd HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysOperLog:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button v-permisaction="['admin:sysOperLog:export']" size="small" :icon="Download" @click="handleExport">导出</el-button>
</div>
<el-table v-loading="loading" :data="list" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" width="70" prop="id" />
<el-table-column
label="Request info"
prop="operUrl"
>
<template #default="scope">
<el-popover trigger="hover" placement="top">
<p>Request:
<el-tag v-if="scope.row.requestMethod=='GET'">{{ scope.row.requestMethod }}</el-tag>
<el-tag v-if="scope.row.requestMethod=='POST'" type="success">{{ scope.row.requestMethod }}</el-tag>
<el-tag v-if="scope.row.requestMethod=='PUT'" type="warning">{{ scope.row.requestMethod }}</el-tag>
<el-tag v-if="scope.row.requestMethod=='DELETE'" type="danger">{{ scope.row.requestMethod }}</el-tag>
{{ scope.row.operUrl }}
</p>
<p>Host: {{ scope.row.operIp }}</p>
<p>Location: {{ scope.row.operLocation }}</p>
<p>耗时: {{ scope.row.latencyTime }}</p>
<template #reference><div class="name-wrapper">
<el-tag v-if="scope.row.requestMethod=='GET'">{{ scope.row.requestMethod }}</el-tag>
<el-tag v-if="scope.row.requestMethod=='POST'" type="success">{{ scope.row.requestMethod }}</el-tag>
<el-tag v-if="scope.row.requestMethod=='PUT'" type="warning">{{ scope.row.requestMethod }}</el-tag>
<el-tag v-if="scope.row.requestMethod=='DELETE'" type="danger">{{ scope.row.requestMethod }}</el-tag>
{{ scope.row.operUrl }}
</div></template>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="操作人员"
prop="operName"
width="160"
:show-overflow-tooltip="true"
/>
<el-table-column
label="状态"
prop="status"
width="80"
:show-overflow-tooltip="true"
>
<template #default="scope">
<el-tag v-if="scope.row.status=='2'" type="success">{{ statusFormat(scope.row,scope.row.status) }}</el-tag>
<el-tag v-if="scope.row.status=='1'" type="danger">{{ statusFormat(scope.row,scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作日期" prop="operTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.operTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="80"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button v-permisaction="['admin:sysOperLog:query']" type="primary" link size="small" @click="handleView(scope.row,scope.index)">详细</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 操作日志详细 -->
<el-dialog v-model="open" title="操作日志详细" width="700px" :close-on-click-modal="false">
<el-form ref="form" :model="form" label-width="100px" size="mini">
<el-row>
<el-col :span="24">
<el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="登录信息:"
>{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="耗时:">{{ form.latencyTime }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="操作状态:">
<div v-if="form.status === '2'">正常</div>
<div v-else-if="form.status === '1'">关闭</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item v-if="form.status === 1" label="异常信息:">{{ form.errorMsg }}</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer><div class="dialog-footer">
<el-button @click="open = false">关 闭</el-button>
</div></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listSysOperlog, delSysOperlog, cleanOperlog } from '@/api/admin/sys-opera-log'
import { formatJson } from '@/utils'
import { Search, Refresh, Delete, Download } from '@element-plus/icons-vue'
export default {
name: 'SysOperLogManage',
setup() {
return { Search, Refresh, Delete, Download }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 表格数据
list: [],
// 是否显示弹出层
open: false,
// 类型数据字典
statusOptions: [],
// 日期范围
dateRange: [],
// 表单参数
form: {},
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
title: undefined,
operName: undefined,
businessType: undefined,
status: undefined,
createdAtOrder: 'desc'
}
}
},
created() {
this.getList()
this.getDicts('sys_common_status').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询登录日志 */
getList() {
this.loading = true
listSysOperlog(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.list = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
// 操作日志状态字典翻译
statusFormat(row, column) {
return this.selectDictLabel(this.statusOptions, row.status)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.multiple = !selection.length
},
/** 详细按钮操作 */
handleView(row) {
this.open = true
this.form = row
},
/** 删除按钮操作 */
handleDelete(row) {
const operIds = (row.id && [row.id]) || this.ids
this.$confirm('是否确认删除日志编号为"' + operIds + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delSysOperlog({ 'ids': operIds })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 清空按钮操作 */
handleClean() {
this.$confirm('是否确认清空所有操作日志数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return cleanOperlog()
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
// const queryParams = this.queryParams
this.$confirm('是否确认导出所有操作日志数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['日志编号', '系统模块', '操作类型', '请求方式', '操作人员', '主机', '操作地点', '操作状态', '操作url', '操作日期']
const filterVal = ['ID', 'title', 'businessType', 'method', 'operName', 'operIp', 'operLocation', 'status', 'operUrl', 'operTime']
const list = this.list
const data = formatJson(filterVal, list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '操作日志',
autoWidth: true, // Optional
bookType: 'xlsx' // Optional
})
this.downloadLoading = false
})
})
}
}
}
</script>
+319
View File
@@ -0,0 +1,319 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="岗位编码" prop="postCode">
<el-input
v-model="queryParams.postCode"
placeholder="请输入岗位编码"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="岗位名称" prop="postName">
<el-input
v-model="queryParams.postName"
placeholder="请输入岗位名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="岗位状态" clearable size="small">
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysPost:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-permisaction="['admin:sysPost:edit']" type="primary" size="small" :icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button v-permisaction="['admin:sysPost:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button v-permisaction="['admin:sysPost:export']" size="small" :icon="Download" @click="handleExport">导出</el-button>
</div>
<el-table v-loading="loading" :data="postList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="岗位编号" width="80" align="center" prop="postId" />
<el-table-column label="岗位编码" align="center" prop="postCode" />
<el-table-column label="岗位名称" align="center" prop="postName" />
<el-table-column label="岗位排序" align="center" prop="sort" />
<el-table-column label="状态" align="center" prop="status" :formatter="statusFormat">
<template #default="scope">
<el-tag
:type="scope.row.status === 1 ? 'danger' : 'success'"
disable-transitions
>{{ statusFormat(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button v-permisaction="['admin:sysPost:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysPost:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改岗位对话框 -->
<el-dialog v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="岗位名称" prop="postName">
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
</el-form-item>
<el-form-item label="岗位编码" prop="postCode">
<el-input v-model="form.postCode" placeholder="请输入编码名称" />
</el-form-item>
<el-form-item label="岗位顺序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right" :min="0" />
</el-form-item>
<el-form-item label="岗位状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listPost, getPost, delPost, addPost, updatePost } from '@/api/admin/sys-post'
import { formatJson } from '@/utils'
import { Search, Refresh, Plus, Edit, Delete, Download } from '@element-plus/icons-vue'
export default {
name: 'SysPostManage',
setup() {
return { Search, Refresh, Plus, Edit, Delete, Download }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 岗位表格数据
postList: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 状态数据字典
statusOptions: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
postCode: undefined,
postName: undefined,
status: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
postName: [
{ required: true, message: '岗位名称不能为空', trigger: 'blur' }
],
postCode: [
{ required: true, message: '岗位编码不能为空', trigger: 'blur' }
],
sort: [
{ required: true, message: '岗位顺序不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.getList()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询岗位列表 */
getList() {
this.loading = true
listPost(this.queryParams).then(response => {
this.postList = response.data.list
this.total = response.data.count
this.loading = false
})
},
// 岗位状态字典翻译
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, row.status)
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
postId: undefined,
postCode: undefined,
postName: undefined,
sort: 0,
status: '1',
remark: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.postId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加岗位'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const postId = row.postId || this.ids
getPost(postId).then(response => {
this.form = response.data
this.form.status = String(this.form.status)
this.open = true
this.title = '修改岗位'
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
this.form.status = parseInt(this.form.status)
if (this.form.postId !== undefined) {
updatePost(this.form, this.form.postId).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addPost(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
// const postIds = row.postId || this.ids
const Ids = (row.postId && [row.postId]) || this.ids
this.$confirm('是否确认删除岗位编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delPost({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
// const queryParams = this.queryParams
this.$confirm('是否确认导出所有岗位数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['岗位编号', '岗位编码', '岗位名称', '排序', '创建时间']
const filterVal = ['postId', 'postCode', 'postName', 'sort', 'createdAt']
const list = this.postList
const data = formatJson(filterVal, list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '岗位管理',
autoWidth: true, // Optional
bookType: 'xlsx' // Optional
})
this.downloadLoading = false
})
}).catch(function() {})
}
}
}
</script>
+552
View File
@@ -0,0 +1,552 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="名称" prop="roleName">
<el-input
v-model="queryParams.roleName"
placeholder="请输入角色名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="权限字符" prop="roleKey">
<el-input
v-model="queryParams.roleKey"
placeholder="请输入权限字符"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="角色状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['admin:sysRole:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-permisaction="['admin:sysRole:update']" type="primary" size="small" :icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button v-permisaction="['admin:sysRole:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button v-permisaction="['admin:sysRole:export']" size="small" :icon="Download" @click="handleExport">导出</el-button>
</div>
<el-table
v-loading="loading"
:data="roleList"
border
stripe
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编码" sortable="custom" prop="roleId" width="80" />
<el-table-column label="名称" sortable="custom" prop="roleName" :show-overflow-tooltip="true" />
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
<el-table-column label="排序" sortable="custom" prop="roleSort" width="80" />
<el-table-column label="状态" sortable="custom" width="80">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="2"
inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="创建时间" sortable="custom" prop="createdAt" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="left"
class-name="small-padding fixed-width"
width="220"
>
<template #default="scope">
<el-button v-permisaction="['admin:sysRole:update']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysRole:update']" type="primary" link size="small" @click="handleDataScope(scope.row)">数据权限</el-button>
<template v-if="scope.row.roleKey!=='admin'">
<el-divider direction="vertical" />
<el-button v-permisaction="['admin:sysRole:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改角色配置对话框 -->
<el-dialog v-if="open" v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" :disabled="isEdit" />
</el-form-item>
<el-form-item label="权限字符" prop="roleKey">
<el-input v-model="form.roleKey" placeholder="请输入权限字符" :disabled="isEdit" />
</el-form-item>
<el-form-item label="角色顺序" prop="roleSort">
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<el-tree
ref="menuTree"
:data="menuOptions"
show-checkbox
node-key="id"
:empty-text="menuOptionsAlert"
style="height:171px;overflow-y:auto;overflow-x:hidden;"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
<!-- 分配角色数据权限对话框 -->
<el-dialog v-if="openDataScope" v-model="open" :title="title" width="500px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="角色名称">
<el-input v-model="form.roleName" :disabled="true" />
</el-form-item>
<el-form-item label="权限字符">
<el-input v-model="form.roleKey" :disabled="true" />
</el-form-item>
<el-form-item label="权限范围">
<el-select v-model="form.dataScope">
<el-option
v-for="item in dataScopeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-show="form.dataScope == 2" label="数据权限">
<el-tree
ref="dept"
:data="deptOptions"
show-checkbox
default-expand-all
node-key="id"
empty-text="加载中,请稍后"
:props="defaultProps"
/>
</el-form-item>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitDataScope">确 定</el-button>
<el-button @click="cancelDataScope">取 消</el-button>
</div></template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus } from '@/api/admin/sys-role'
import { roleMenuTreeselect } from '@/api/admin/sys-menu'
import { treeselect as deptTreeselect, roleDeptTreeselect } from '@/api/admin/sys-dept'
import { formatJson } from '@/utils'
import { Search, Refresh, Plus, Edit, Delete, Download } from '@element-plus/icons-vue'
export default {
name: 'RoleManagement',
components: {
},
setup() {
return { Search, Refresh, Plus, Edit, Delete, Download }
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 角色表格数据
roleList: [],
menuIdsChecked: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 是否显示弹出层(数据权限)
openDataScope: false,
isEdit: false,
// 日期范围
dateRange: [],
// 状态数据字典
statusOptions: [],
// 数据范围选项
dataScopeOptions: [
{
value: '1',
label: '全部数据权限'
},
{
value: '2',
label: '自定数据权限'
},
{
value: '3',
label: '本部门数据权限'
},
{
value: '4',
label: '本部门及以下数据权限'
},
{
value: '5',
label: '仅本人数据权限'
}
],
// 菜单列表
menuOptions: [],
menuList: [],
// 部门列表
deptOptions: [],
menuOptionsAlert: '加载中,请稍后',
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
roleName: undefined,
roleKey: undefined,
status: undefined
},
// 表单参数
form: {
sysMenu: []
},
defaultProps: {
children: 'children',
label: 'label'
},
// 表单校验
rules: {
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' }
],
roleKey: [
{ required: true, message: '权限字符不能为空', trigger: 'blur' }
],
roleSort: [
{ required: true, message: '角色顺序不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.getList()
this.getMenuTreeselect()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询角色列表 */
getList() {
this.loading = true
listRole(this.addDateRange(this.queryParams, this.dateRange)).then(
response => {
this.roleList = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
/** 查询菜单树结构 */
getMenuTreeselect() {
roleMenuTreeselect(0).then(response => {
this.menuOptions = response.data.menus
this.menuList = this.menuOptions
})
},
/** 查询部门树结构 */
getDeptTreeselect() {
deptTreeselect().then(response => {
this.deptOptions = response.data.list
})
},
// 所有菜单节点数据
getMenuAllCheckedKeys() {
// 目前被选中的菜单节点
const checkedKeys = this.$refs.menuTree.getHalfCheckedKeys()
console.log('目前被选中的菜单节点', checkedKeys)
// 半选中的菜单节点
const halfCheckedKeys = this.$refs.menuTree.getCheckedKeys()
console.log('半选中的菜单节点', halfCheckedKeys)
// checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return halfCheckedKeys
},
// 所有部门节点数据
getDeptAllCheckedKeys() {
// 目前被选中的部门节点
const checkedKeys = this.$refs.dept.getCheckedKeys()
// 半选中的部门节点
// const halfCheckedKeys = this.$refs.dept.getCheckedKeys()
// checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return checkedKeys
},
/** 根据角色ID查询菜单树结构 */
getRoleMenuTreeselect(row, checkedKeys) {
if (row.roleKey === 'admin') {
this.menuOptionsAlert = '系统超级管理员无需此操作'
this.menuOptions = []
} else {
this.$nextTick(() => {
this.$refs.menuTree.setCheckedKeys(checkedKeys)
})
}
},
/** 根据角色ID查询部门树结构 */
getRoleDeptTreeselect(roleId) {
roleDeptTreeselect(roleId).then(response => {
this.deptOptions = response.data.depts
this.$nextTick(() => {
this.$refs.dept.setCheckedKeys(response.data.checkedKeys)
})
})
},
// 角色状态修改
handleStatusChange(row) {
const text = row.status === '2' ? '启用' : '停用'
this.$confirm('确认要"' + text + '""' + row.roleName + '"角色吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return changeRoleStatus(row.roleId, row.status)
}).then((res) => {
console.log('res', res)
this.msgSuccess(res.msg)
}).catch(function() {
row.status = row.status === '2' ? '1' : '2'
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 取消按钮(数据权限)
cancelDataScope() {
this.openDataScope = false
this.reset()
},
// 表单重置
reset() {
this.menuOptions = this.menuList
if (this.$refs.menuTree !== undefined) {
this.$refs.menuTree.setCheckedKeys([])
}
this.form = {
roleId: undefined,
roleName: undefined,
roleKey: undefined,
roleSort: 0,
status: '2',
menuIds: [],
deptIds: [],
sysMenu: [],
remark: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.roleId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
// this.getMenuTreeselect(0)
this.open = true
this.title = '添加角色'
this.isEdit = false
},
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
/** 修改按钮操作 */
handleUpdate(row) {
this.menuIdsChecked = []
this.reset()
const roleId = row.roleId || this.ids
getRole(roleId).then(response => {
this.form = response.data
this.menuIdsChecked = response.data.menuIds
this.title = '修改角色'
this.isEdit = true
this.open = true
this.getRoleMenuTreeselect(row, response.data.menuIds)
})
},
/** 分配数据权限操作 */
handleDataScope(row) {
this.reset()
getRole(row.roleId).then(response => {
this.form = response.data
this.openDataScope = true
this.title = '分配数据权限'
this.getRoleDeptTreeselect(row.roleId)
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.roleId !== undefined) {
this.form.menuIds = this.getMenuAllCheckedKeys()
updateRole(this.form, this.form.roleId).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
this.form.menuIds = this.getMenuAllCheckedKeys()
addRole(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 提交按钮(数据权限) */
submitDataScope: function() {
if (this.form.roleId !== undefined) {
this.form.deptIds = this.getDeptAllCheckedKeys()
console.log(this.getDeptAllCheckedKeys())
dataScope(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.openDataScope = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
},
/** 删除按钮操作 */
handleDelete(row) {
const roleIds = (row.roleId && [row.roleId]) || this.ids
this.$confirm('是否确认删除角色编号为"' + roleIds + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delRole({ 'ids': roleIds })
}).then((response) => {
this.getList()
this.msgSuccess(response.msg)
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
this.$confirm('是否确认导出所有角色数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['角色编号', '角色名称', '权限字符', '显示顺序', '状态', '创建时间']
const filterVal = ['roleId', 'roleName', 'roleKey', 'roleSort', 'status', 'createdAt']
const list = this.roleList
const data = formatJson(filterVal, list)
excel.export_json_to_excel({
header: tHeader,
data,
filename: '角色管理',
autoWidth: true, // Optional
bookType: 'xlsx' // Optional
})
this.downloadLoading = false
})
})
}
}
}
</script>
+683
View File
@@ -0,0 +1,683 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-row :gutter="20">
<!--部门数据-->
<el-col :span="4" :xs="24">
<div class="head-container">
<el-input
v-model="deptName"
placeholder="请输入部门名称"
clearable
size="small"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container">
<el-tree
ref="tree"
:data="deptOptions"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
default-expand-all
@node-click="handleNodeClick"
/>
</div>
</el-col>
<!--用户数据-->
<el-col :span="20" :xs="24">
<el-form ref="queryForm" :model="queryParams" :inline="true" label-width="68px">
<el-form-item label="用户名称" prop="username">
<el-input
v-model="queryParams.username"
placeholder="请输入用户名称"
clearable
size="small"
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input
v-model="queryParams.phone"
placeholder="请输入手机号码"
clearable
size="small"
style="width: 160px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
size="small"
style="width: 160px"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="handleQuery">搜索</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysUser:add']"
type="primary"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysUser:edit']"
type="success"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-permisaction="['admin:sysUser:remove']"
type="danger"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="userList"
border
@selection-change="handleSelectionChange"
@sort-change="handleSortChang"
>
<el-table-column type="selection" width="45" align="center" />
<el-table-column label="编号" width="75" prop="userId" sortable="custom" />
<el-table-column label="登录名" width="105" prop="username" sortable="custom" :show-overflow-tooltip="true" />
<el-table-column label="昵称" prop="nickName" :show-overflow-tooltip="true" />
<el-table-column label="部门" prop="dept.deptName" :show-overflow-tooltip="true" />
<el-table-column label="手机号" prop="phone" width="108" />
<el-table-column label="状态" width="80" sortable="custom">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="2"
inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
label="创建时间"
prop="createdAt"
sortable="custom"
width="155"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="160"
fix="right"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button
v-permisaction="['admin:sysUser:edit']"
size="mini"
type="text"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-if="scope.row.userId !== 1"
v-permisaction="['admin:sysUser:remove']"
size="mini"
type="text"
@click="handleDelete(scope.row)"
>删除</el-button>
<el-button
v-permisaction="['admin:sysUser:resetPassword']"
size="mini"
type="text"
@click="handleResetPwd(scope.row)"
>重置</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-col>
</el-row>
</el-card>
<!-- 添加或修改参数配置对话框 -->
<el-dialog v-model="open" :title="title" width="600px" :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<treeselect
v-model="form.deptId"
:options="deptOptions"
placeholder="请选择归属部门"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号码" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户名称" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option
v-for="dict in sexOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="form.postId" placeholder="请选择" @change="$forceUpdate()">
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == 1"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色">
<el-select v-model="form.roleId" placeholder="请选择" @change="$forceUpdate()">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == 1"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div></template>
</el-dialog>
<!-- 用户导入对话框 -->
<el-dialog v-model="open" :title="upload.title" width="400px" :close-on-click-modal="false">
<el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<i class="ri-upload-line" />
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
<el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
</div>
<div class="el-upload__tip" style="color:red">提示:仅允许导入"xls"或"xlsx"格式文件!</div>
</template>
</el-upload>
<template #footer><div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div></template>
</el-dialog>
</template>
</BasicLayout>
</template>
<script>
import { listUser, getUser, delUser, addUser, updateUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from '@/api/admin/sys-user'
import { getToken } from '@/utils/auth'
import { listPost } from '@/api/admin/sys-post'
import { listRole } from '@/api/admin/sys-role'
import { treeselect } from '@/api/admin/sys-dept'
import Treeselect from 'vue3-treeselect'
import 'vue3-treeselect/dist/vue3-treeselect.css'
export default {
name: 'SysUserManage',
components: { Treeselect },
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 用户表格数据
userList: null,
// 弹出层标题
title: '',
// 部门树选项
deptOptions: undefined,
// 是否显示弹出层
open: false,
// 部门名称
deptName: undefined,
// 默认密码
initPassword: undefined,
// 日期范围
dateRange: [],
// 状态数据字典
statusOptions: [],
// 性别状态字典
sexOptions: [],
// 岗位选项
postOptions: [],
// 角色选项
roleOptions: [],
// 表单参数
form: {},
defaultProps: {
children: 'children',
label: 'label'
},
// 用户导入参数
upload: {
// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: '',
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: 'Bearer ' + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + '/system/user/importData'
},
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
username: undefined,
phone: undefined,
status: undefined,
deptId: undefined
},
// 表单校验
rules: {
username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
deptId: [{ required: true, message: '归属部门不能为空', trigger: 'blur' }],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }],
email: [
{ required: true, message: '邮箱地址不能为空', trigger: 'blur' },
{ type: 'email', message: "'请输入正确的邮箱地址", trigger: ['blur', 'change'] }
],
phone: [
{ required: true, message: '手机号码不能为空', trigger: 'blur' },
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
}
}
},
watch: {
// 根据名称筛选部门树
deptName(val) {
this.$refs.tree.filter(val)
}
},
created() {
this.getList()
this.getTreeselect()
this.getDicts('sys_normal_disable').then(response => {
this.statusOptions = response.data
})
this.getDicts('sys_user_sex').then(response => {
this.sexOptions = response.data
})
this.getConfigKey('sys_user_initPassword').then(response => {
this.initPassword = response.data.configValue
})
},
methods: {
/** 查询用户列表 */
getList() {
this.loading = true
listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.userList = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
/** 查询部门下拉树结构 */
getTreeselect() {
treeselect().then(response => {
this.deptOptions = response.data
})
},
// 筛选节点
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
// 节点单击事件
handleNodeClick(data) {
this.queryParams.deptId = '/' + data.id + '/'
this.getList()
},
/** 转换菜单数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.id,
label: node.label,
children: node.children
}
},
/** 排序回调函数 */
handleSortChang(column, prop, order) {
prop = column.prop
order = column.order
if (this.order !== '' && this.order !== prop + 'Order') {
this.queryParams[this.order] = undefined
}
if (order === 'descending') {
this.queryParams[prop + 'Order'] = 'desc'
this.order = prop + 'Order'
} else if (order === 'ascending') {
this.queryParams[prop + 'Order'] = 'asc'
this.order = prop + 'Order'
} else {
this.queryParams[prop + 'Order'] = undefined
}
this.getList()
},
// 用户状态修改
handleStatusChange(row) {
const text = row.status === '2' ? '启用' : '停用'
this.$confirm('确认要"' + text + '""' + row.username + '"用户吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return changeUserStatus(row)
}).then(() => {
this.msgSuccess(text + '成功')
}).catch(function() {
row.status = row.status === '2' ? '1' : '2'
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
userId: undefined,
deptId: undefined,
username: undefined,
nickName: undefined,
password: undefined,
phone: undefined,
email: undefined,
sex: undefined,
status: '2',
remark: undefined,
postIds: undefined,
roleIds: undefined
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.page = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.queryParams.deptId = ''
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.userId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.getTreeselect()
listPost({ pageSize: 1000 }).then(response => {
this.postOptions = response.data.list
})
listRole({ pageSize: 1000 }).then(response => {
this.roleOptions = response.data.list
})
this.open = true
this.title = '添加用户'
this.form.password = this.initPassword
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const userId = row.userId || this.ids
getUser(userId).then(response => {
this.form = response.data
this.open = true
this.title = '修改用户'
this.form.password = ''
})
listPost({ pageSize: 1000 }).then(response => {
this.postOptions = response.data.list
})
listRole({ pageSize: 1000 }).then(response => {
this.roleOptions = response.data.list
})
},
/** 重置密码按钮操作 */
handleResetPwd(row) {
this.$prompt('请输入"' + row.username + '"的新密码', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(({ value }) => {
resetUserPwd(row.userId, value).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
} else {
this.msgError(response.msg)
}
})
}).catch(() => {})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.userId !== undefined) {
updateUser(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
addUser(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const Ids = (row.userId && [row.userId]) || this.ids
this.$confirm('是否确认删除用户编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delUser({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 导出按钮操作 */
handleExport() {
const queryParams = this.queryParams
this.$confirm('是否确认导出所有用户数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return exportUser(queryParams)
}).then(response => {
this.download(response.msg)
}).catch(function() {})
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = '用户导入'
this.upload.open = true
},
/** 下载模板操作 */
importTemplate() {
importTemplate().then(response => {
this.download(response.msg)
})
},
// 文件上传中处理
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true
},
// 文件上传成功处理
handleFileSuccess(response, file, fileList) {
this.upload.open = false
this.upload.isUploading = false
this.$refs.upload.clearFiles()
this.$alert(response.msg, '导入结果', { dangerouslyUseHTMLString: true })
this.getList()
},
// 提交上传文件
submitFileForm() {
this.$refs.upload.submit()
}
}
}
</script>
@@ -0,0 +1,41 @@
<template>
<div ref="chartRef" :class="className" :style="{height, width}" />
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'BarChart',
props: {
className: { type: String, default: 'chart' },
width: { type: String, default: '100%' },
height: { type: String, default: '300px' }
},
data() {
return { chart: null }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.chart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }},
grid: { top: 10, left: '2%', right: '2%', bottom: '3%', containLabel: true },
xAxis: [{ type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], axisTick: { alignWithLabel: true }}],
yAxis: [{ type: 'value', axisTick: { show: false }}],
series: [
{ name: 'pageA', type: 'bar', stack: 'vistors', barWidth: '60%', data: [79, 52, 200, 334, 390, 330, 220] },
{ name: 'pageB', type: 'bar', stack: 'vistors', barWidth: '60%', data: [80, 52, 200, 334, 390, 330, 220] },
{ name: 'pageC', type: 'bar', stack: 'vistors', barWidth: '60%', data: [30, 52, 200, 334, 390, 330, 220] }
]
})
}
}
}
</script>
@@ -0,0 +1,62 @@
<template>
<div ref="chartRef" :class="className" :style="{height, width}" />
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'LineChart',
props: {
className: { type: String, default: 'chart' },
width: { type: String, default: '100%' },
height: { type: String, default: '350px' },
autoResize: { type: Boolean, default: true },
chartData: { type: Object, required: true }
},
data() {
return { chart: null }
},
watch: {
chartData: { deep: true, handler(val) { this.setOptions(val) } }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.setOptions(this.chartData)
},
setOptions({ expectedData, actualData } = {}) {
this.chart.setOption({
xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
boundaryGap: false,
axisTick: { show: false }
},
grid: { left: 10, right: 10, bottom: 20, top: 30, containLabel: true },
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, padding: [5, 10] },
yAxis: { axisTick: { show: false }},
legend: { data: ['expected', 'actual'] },
series: [
{
name: 'expected',
itemStyle: { color: '#FF005A', lineStyle: { color: '#FF005A', width: 2 }},
smooth: true, type: 'line', data: expectedData,
animationDuration: 2800, animationEasing: 'cubicInOut'
},
{
name: 'actual', smooth: true, type: 'line',
itemStyle: { color: '#3888fa', lineStyle: { color: '#3888fa', width: 2 }, areaStyle: { color: '#f3f8ff' }},
data: actualData, animationDuration: 2800, animationEasing: 'quadraticOut'
}
]
})
}
}
}
</script>
@@ -0,0 +1,183 @@
<template>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('newVisitis')">
<div class="card-panel-icon-wrapper icon-people">
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
新用户
</div>
<count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('messages')">
<div class="card-panel-icon-wrapper icon-message">
<svg-icon icon-class="message" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
消息
</div>
<count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('purchases')">
<div class="card-panel-icon-wrapper icon-money">
<svg-icon icon-class="money" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
金额
</div>
<count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('shoppings')">
<div class="card-panel-icon-wrapper icon-shopping">
<svg-icon icon-class="shopping" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
销量
</div>
<count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" />
</div>
</div>
</el-col>
</el-row>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
name: 'PanelGroupComponent',
emits: ['handleSetLineChartData'],
components: {
CountTo
},
methods: {
handleSetLineChartData(type) {
this.$emit('handleSetLineChartData', type)
}
}
}
</script>
<style lang="scss" scoped>
.panel-group {
margin-top: 18px;
.card-panel-col {
margin-bottom: 32px;
}
.card-panel {
height: 108px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
border-color: rgba(0, 0, 0, .05);
&:hover {
.card-panel-icon-wrapper {
color: #fff;
}
.icon-people {
background: #40c9c6;
}
.icon-message {
background: #36a3f7;
}
.icon-money {
background: #f4516c;
}
.icon-shopping {
background: #34bfa3
}
}
.icon-people {
color: #40c9c6;
}
.icon-message {
color: #36a3f7;
}
.icon-money {
color: #f4516c;
}
.icon-shopping {
color: #34bfa3
}
.card-panel-icon-wrapper {
float: left;
margin: 14px 0 0 14px;
padding: 16px;
transition: all 0.38s ease-out;
border-radius: 6px;
}
.card-panel-icon {
float: left;
font-size: 48px;
}
.card-panel-description {
float: right;
font-weight: bold;
margin: 26px;
margin-left: 0px;
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
margin-bottom: 12px;
}
.card-panel-num {
font-size: 20px;
}
}
}
}
@media (max-width:550px) {
.card-panel-description {
display: none;
}
.card-panel-icon-wrapper {
float: none !important;
width: 100%;
height: 100%;
margin: 0 !important;
.svg-icon {
display: block;
margin: 14px auto !important;
float: none !important;
}
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div ref="chartRef" :class="className" :style="{height, width}" />
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'PieChart',
props: {
className: { type: String, default: 'chart' },
width: { type: String, default: '100%' },
height: { type: String, default: '300px' }
},
data() {
return { chart: null }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.chart.setOption({
tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' },
legend: { left: 'center', bottom: '10', data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts'] },
series: [{
name: 'WEEKLY WRITE ARTICLES',
type: 'pie',
roseType: 'radius',
radius: [15, 95],
center: ['50%', '38%'],
data: [
{ value: 320, name: 'Industries' },
{ value: 240, name: 'Technology' },
{ value: 149, name: 'Forex' },
{ value: 100, name: 'Gold' },
{ value: 59, name: 'Forecasts' }
],
animationEasing: 'cubicInOut',
animationDuration: 2600
}]
})
}
}
}
</script>
@@ -0,0 +1,51 @@
<template>
<div ref="chartRef" :class="className" :style="{height, width}" />
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'RaddarChart',
props: {
className: { type: String, default: 'chart' },
width: { type: String, default: '100%' },
height: { type: String, default: '300px' }
},
data() {
return { chart: null }
},
mounted() {
this.$nextTick(() => { this.initChart() })
},
beforeUnmount() {
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartRef)
this.chart.setOption({
legend: { bottom: 10, data: ['Allocated Budget', 'Expected Spending', 'Actual Spending'] },
radar: {
indicator: [
{ name: 'Sales', max: 6500 },
{ name: 'Administration', max: 16000 },
{ name: 'Information Technology', max: 30000 },
{ name: 'Customer Support', max: 38000 },
{ name: 'Development', max: 52000 },
{ name: 'Marketing', max: 25000 }
]
},
series: [{
type: 'radar',
data: [
{ value: [5000, 7000, 12000, 11000, 15000, 14000], name: 'Allocated Budget' },
{ value: [4000, 9000, 15000, 15000, 13000, 11000], name: 'Expected Spending' },
{ value: [5500, 11000, 12000, 15000, 12000, 12000], name: 'Actual Spending' }
]
}]
})
}
}
}
</script>
@@ -0,0 +1,82 @@
<template>
<li :class="{ completed: todo.done, editing: editing }" class="todo">
<div class="view">
<input
:checked="todo.done"
class="toggle"
type="checkbox"
@change="toggleTodo( todo)"
>
<label @dblclick="editing = true" v-text="todo.text" />
<button class="destroy" @click="deleteTodo( todo )" />
</div>
<input
v-show="editing"
v-focus="editing"
:value="todo.text"
class="edit"
@keyup.enter="doneEdit"
@keyup.esc="cancelEdit"
@blur="doneEdit"
>
</li>
</template>
<script>
export default {
name: 'TodoItem',
emits: ['deleteTodo', 'editTodo', 'toggleTodo'],
directives: {
focus(el, { value }, { context }) {
if (value) {
context.$nextTick(() => {
el.focus()
})
}
}
},
props: {
todo: {
type: Object,
default: function() {
return {}
}
}
},
data() {
return {
editing: false
}
},
methods: {
deleteTodo(todo) {
this.$emit('deleteTodo', todo)
},
editTodo({ todo, value }) {
this.$emit('editTodo', { todo, value })
},
toggleTodo(todo) {
this.$emit('toggleTodo', todo)
},
doneEdit(e) {
const value = e.target.value.trim()
const { todo } = this
if (!value) {
this.deleteTodo({
todo
})
} else if (this.editing) {
this.editTodo({
todo,
value
})
this.editing = false
}
},
cancelEdit(e) {
e.target.value = this.todo.text
this.editing = false
}
}
}
</script>
@@ -0,0 +1,320 @@
.todoapp {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto ;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
background: #fff;
z-index: 1;
position: relative;
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 18px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 10px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
text-align: center;
border: none;
/* Mobile Safari */
opacity: 0;
position: absolute;
}
.toggle-all+label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all+label:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked+label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none;
/* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle+label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
background-size: 36px;
}
.todo-list li .toggle:checked+label {
background-size: 36px;
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 50px;
display: block;
line-height: 1.0;
font-size: 14px;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
transition: color 0.2s ease-out;
cursor: pointer;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
position: relative;
padding: 10px 15px;
height: 40px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 40px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
position: relative;
z-index: 1;
list-style: none;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
font-size: 12px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
}
@@ -0,0 +1,129 @@
<template>
<section class="todoapp">
<!-- header -->
<header class="header">
<input class="new-todo" autocomplete="off" placeholder="Todo List" @keyup.enter="addTodo">
</header>
<!-- main section -->
<section v-show="todos.length" class="main">
<input id="toggle-all" :checked="allChecked" class="toggle-all" type="checkbox" @change="toggleAll({ done: !allChecked })">
<label for="toggle-all" />
<ul class="todo-list">
<todo
v-for="(todo, index) in filteredTodos"
:key="index"
:todo="todo"
@toggle-todo="toggleTodo"
@edit-todo="editTodo"
@delete-todo="deleteTodo"
/>
</ul>
</section>
<!-- footer -->
<footer v-show="todos.length" class="footer">
<span class="todo-count">
<strong>{{ remaining }}</strong>
{{ pluralizeFilter(remaining, 'item') }} left
</span>
<ul class="filters">
<li v-for="(val, key) in filters" :key="key">
<a :class="{ selected: visibility === key }" @click.prevent="visibility = key">{{ capitalizeFilter(key) }}</a>
</li>
</ul>
<!-- <button class="clear-completed" v-show="todos.length > remaining" @click="clearCompleted">
Clear completed
</button> -->
</footer>
</section>
</template>
<script>
import Todo from './Todo.vue'
const STORAGE_KEY = 'todos'
const filters = {
all: todos => todos,
active: todos => todos.filter(todo => !todo.done),
completed: todos => todos.filter(todo => todo.done)
}
const defalutList = [
{ text: 'star this repository', done: false },
{ text: 'fork this repository', done: false },
{ text: 'follow author', done: false },
{ text: 'vue-element-admin', done: true },
{ text: 'vue', done: true },
{ text: 'element-ui', done: true },
{ text: 'axios', done: true },
{ text: 'webpack', done: true }
]
export default {
components: { Todo },
data() {
return {
visibility: 'all',
filters,
// todos: JSON.parse(window.localStorage.getItem(STORAGE_KEY)) || defalutList
todos: defalutList
}
},
computed: {
allChecked() {
return this.todos.every(todo => todo.done)
},
filteredTodos() {
return filters[this.visibility](this.todos)
},
remaining() {
return this.todos.filter(todo => !todo.done).length
}
},
methods: {
pluralizeFilter(n, w) {
return n === 1 ? w : w + 's'
},
capitalizeFilter(s) {
return s.charAt(0).toUpperCase() + s.slice(1)
},
setLocalStorage() {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos))
},
addTodo(e) {
const text = e.target.value
if (text.trim()) {
this.todos.push({
text,
done: false
})
this.setLocalStorage()
}
e.target.value = ''
},
toggleTodo(val) {
val.done = !val.done
this.setLocalStorage()
},
deleteTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
this.setLocalStorage()
},
editTodo({ todo, value }) {
todo.text = value
this.setLocalStorage()
},
clearCompleted() {
this.todos = this.todos.filter(todo => !todo.done)
this.setLocalStorage()
},
toggleAll({ done }) {
this.todos.forEach(todo => {
todo.done = done
this.setLocalStorage()
})
}
}
}
</script>
<style lang="scss">
@use './index.scss' as *;
</style>
@@ -0,0 +1,55 @@
import { debounce } from '@/utils'
export default {
data() {
return {
$_sidebarElm: null,
$_resizeHandler: null
}
},
mounted() {
this.$_resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
},
beforeUnmount() {
this.$_destroyResizeEvent()
this.$_destroySidebarResizeEvent()
},
// to fixed bug when cached by keep-alive
// https://github.com/PanJiaChen/vue-element-admin/issues/2116
activated() {
this.$_initResizeEvent()
this.$_initSidebarResizeEvent()
},
deactivated() {
this.$_destroyResizeEvent()
this.$_destroySidebarResizeEvent()
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_initResizeEvent() {
window.addEventListener('resize', this.$_resizeHandler)
},
$_destroyResizeEvent() {
window.removeEventListener('resize', this.$_resizeHandler)
},
$_sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.$_resizeHandler()
}
},
$_initSidebarResizeEvent() {
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
$_destroySidebarResizeEvent() {
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
}
}
}
@@ -0,0 +1,154 @@
<template>
<div class="dashboard-editor-container">
<el-row :gutter="12">
<el-col :sm="24" :xs="24" :md="6" :xl="6" :lg="6" :style="{ marginBottom: '12px' }">
<chart-card title="总销售额" total="¥126,560" color="#1677ff">
<template #icon><i class="ri-money-dollar-circle-line" /></template>
<template #action>
<el-tooltip class="item" effect="dark" content="指标说明" placement="top-start">
<i class="ri-error-warning-line" />
</el-tooltip>
</template>
<div>
<trend flag="top" style="margin-right: 16px;" rate="12">
<template #term><span>周同比</span></template>
</trend>
<trend flag="bottom" rate="11">
<template #term><span>日同比</span></template>
</trend>
</div>
<template #footer>日均销售额<span>¥ 234.56</span></template>
</chart-card>
</el-col>
<el-col :sm="24" :xs="24" :md="6" :xl="6" :lg="6" :style="{ marginBottom: '12px' }">
<chart-card title="访问量" :total="8846" color="#52c41a">
<template #icon><i class="ri-group-line" /></template>
<template #action>
<el-tooltip class="item" effect="dark" content="指标说明" placement="top-start">
<i class="ri-error-warning-line" />
</el-tooltip>
</template>
<div>
<mini-area />
</div>
<template #footer>日访问量<span> {{ '1234' }}</span></template>
</chart-card>
</el-col>
<el-col :sm="24" :xs="24" :md="6" :xl="6" :lg="6" :style="{ marginBottom: '12px' }">
<chart-card title="支付笔数" :total="6560" color="#fa8c16">
<template #icon><i class="ri-bank-card-line" /></template>
<template #action>
<el-tooltip class="item" effect="dark" content="指标说明" placement="top-start">
<i class="ri-error-warning-line" />
</el-tooltip>
</template>
<div>
<mini-bar />
</div>
<template #footer>转化率 <span>60%</span></template>
</chart-card>
</el-col>
<el-col :sm="24" :xs="24" :md="6" :xl="6" :lg="6" :style="{ marginBottom: '12px' }">
<chart-card title="运营活动效果" total="78%" color="#722ed1">
<template #icon><i class="ri-bar-chart-box-line" /></template>
<template #action>
<el-tooltip class="item" effect="dark" content="指标说明" placement="top-start">
<i class="ri-error-warning-line" />
</el-tooltip>
</template>
<div>
<mini-progress :target="80" :percentage="78" height="8px" />
</div>
<template #footer>
<trend flag="top" style="margin-right: 16px;" rate="12">
<template #term><span>同周比</span></template>
</trend>
<trend flag="bottom" rate="80">
<template #term><span>日环比</span></template>
</trend>
</template>
</chart-card>
</el-col>
</el-row>
<el-card :bordered="false" :body-style="{ padding: '0' }">
<div class="salesCard">
<el-tabs>
<el-tab-pane label="销售额">
<el-row>
<el-col :xl="16" :lg="12" :md="12" :sm="24" :xs="24">
<bar :list="barData" title="销售额排行" />
</el-col>
<el-col :xl="8" :lg="12" :md="12" :sm="24" :xs="24">
<rank-list title="门店销售排行榜" :list="rankList" />
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="访问量">
<el-row>
<el-col :xl="16" :lg="12" :md="12" :sm="24" :xs="24">
<bar :list="barData2" title="销售额趋势" />
</el-col>
<el-col :xl="8" :lg="12" :md="12" :sm="24" :xs="24">
<rank-list title="门店销售排行榜" :list="rankList" />
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</div>
</el-card>
</div>
</template>
<script>
import ChartCard from '@/components/ChartCard'
import Trend from '@/components/Trend'
import MiniArea from '@/components/MiniArea'
import MiniBar from '@/components/MiniBar'
import MiniProgress from '@/components/MiniProgress'
import RankList from '@/components/RankList/index'
import Bar from '@/components/Bar.vue'
const barData = []
const barData2 = []
for (let i = 0; i < 12; i += 1) {
barData.push({ x: `${i + 1}月`, y: Math.floor(Math.random() * 1000) + 200 })
barData2.push({ x: `${i + 1}月`, y: Math.floor(Math.random() * 1000) + 200 })
}
const rankList = []
for (let i = 0; i < 7; i++) {
rankList.push({ name: '白鹭岛 ' + (i + 1) + ' 号店', total: 1234.56 - i * 100 })
}
export default {
name: 'DashboardAdmin',
components: { ChartCard, Trend, MiniArea, MiniBar, MiniProgress, RankList, Bar },
data() {
return { barData, barData2, rankList }
}
}
</script>
<style lang="scss" scoped>
.dashboard-editor-container {
padding: 12px;
background-color: #f0f2f5;
position: relative;
}
:deep(.el-tabs__item) {
padding-left: 16px !important;
height: 50px;
line-height: 50px;
}
@media (max-width: 1024px) {
.chart-wrapper {
padding: 8px;
}
}
</style>
@@ -0,0 +1,74 @@
<template>
<div class="dashboard-editor-container">
<div class=" clearfix">
<pan-thumb :image="avatar" style="float: left">
Your roles:
<span v-for="item in roles" :key="item" class="pan-info-roles">{{ item }}</span>
</pan-thumb>
<github-corner style="position: absolute; top: 0px; border: 0; right: 0;" />
<div class="info-container">
<span class="display_name">{{ name }}</span>
<span style="font-size:20px;padding-top:20px;display:inline-block;">Editor's Dashboard</span>
</div>
</div>
<div>
<img :src="emptyGif" class="emptyGif">
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import PanThumb from '@/components/PanThumb'
import GithubCorner from '@/components/GithubCorner'
export default {
name: 'DashboardEditor',
components: { PanThumb, GithubCorner },
data() {
return {
emptyGif: 'https://wpimg.wallstcn.com/0e03b7da-db9e-4819-ba10-9016ddfdaed3'
}
},
computed: {
...mapGetters([
'name',
'avatar',
'roles'
])
}
}
</script>
<style lang="scss" scoped>
.emptyGif {
display: block;
width: 45%;
margin: 0 auto;
}
.dashboard-editor-container {
background-color: #e3e3e3;
min-height: 100vh;
padding: 50px 60px 0px;
.pan-info-roles {
font-size: 12px;
font-weight: 700;
color: #333;
display: block;
}
.info-container {
position: relative;
margin-left: 190px;
height: 150px;
line-height: 200px;
.display_name {
font-size: 48px;
line-height: 48px;
color: #212121;
position: absolute;
top: 25px;
}
}
}
</style>
+31
View File
@@ -0,0 +1,31 @@
<template>
<div class="dashboard-container">
<component :is="currentRole" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import adminDashboard from './admin'
import editorDashboard from './editor'
export default {
name: 'DashboardPage',
components: { adminDashboard, editorDashboard },
data() {
return {
currentRole: 'adminDashboard'
}
},
computed: {
...mapGetters([
'roles'
])
},
created() {
// if (!this.roles.includes('admin')) {
// this.currentRole = 'editorDashboard'
// }
}
}
</script>
@@ -0,0 +1,109 @@
<template>
<el-form ref="basicInfoForm" :model="formData" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item prop="tableName">
<template #label>数据表名称
<el-tooltip content="数据库表名称,针对gorm对应的table()使用,⚠️这里必须是蛇形结构" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.tableName" placeholder="请输入表名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="tableComment">
<template #label>菜单名称
<el-tooltip content="同步的数据库表名称,生成配置数据时,用作菜单名称" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.tableComment" placeholder="请输入菜单名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="className">
<template #label>结构体模型名称
<el-tooltip content="结构体模型名称,代码中的struct名称定义使用" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.className" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者名称" prop="functionAuthor">
<el-input v-model="formData.functionAuthor" placeholder="请输入作者名称" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item prop="isLogicalDelete">
<template #label>是否逻辑删除
<el-tooltip content="目前只支持逻辑删除" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-radio-group v-model="formData.isLogicalDelete">
<el-radio label="1">是</el-radio>
<el-radio label="0">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="formData.isLogicalDelete == '1'" label="逻辑删除字段" prop="logicalDeleteColumn">
<el-input v-model="formData.logicalDeleteColumn" placeholder="请输入" />
</el-form-item>
</el-col> -->
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
export default {
name: 'BasicInfoFormComponent',
props: {
info: {
type: Object,
default: null
}
},
emits: ['update:info'],
data() {
return {
formData: { ...this.info },
rules: {
tableName: [
{ required: true, message: '请输入表名称', trigger: 'blur' },
{ pattern: /^[a-z\._]*$/g, trigger: 'blur', message: '只允许小写字母,例如 sys_demo 格式' }
],
tableComment: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' }
],
className: [
{ required: true, message: '请输入模型名称', trigger: 'blur' },
{ pattern: /^[A-Z][A-z0-9]*$/g, trigger: 'blur', message: '必须以大写字母开头,例如 SysDemo 格式' }
],
functionAuthor: [
{ required: true, message: '请输入作者', trigger: 'blur' },
{ pattern: /^[A-Za-z]+$/, trigger: 'blur', message: '校验规则: 只允许输入字母 a-z 或大写 A-Z' }
]
}
}
},
watch: {
info: {
handler(val) {
this.formData = { ...val }
},
deep: true
},
formData: {
handler(val) {
this.$emit('update:info', val)
},
deep: true
}
}
}
</script>
@@ -0,0 +1,348 @@
<template>
<el-card>
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basic">
<basic-info-form ref="basicInfo" :info="info" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="cloum">
<el-alert
title="⚠️表字段中的id、create_by、update_by、created_at、updated_at、deleted_at的字段在此列表中已经隐藏"
type="warning"
show-icon
/>
<el-table :data="columns" :max-height="tableHeight" style="width: 100%">
<el-table-column fixed label="序号" type="index" width="50" />
<el-table-column
fixed
label="字段列名"
prop="columnName"
width="150"
:show-overflow-tooltip="true"
/>
<el-table-column fixed label="字段描述" width="150">
<template #default="scope">
<el-input v-model="scope.row.columnComment" />
</template>
</el-table-column>
<el-table-column
label="物理类型"
prop="columnType"
width="120"
:show-overflow-tooltip="true"
/>
<el-table-column label="go类型" width="120">
<template #default="scope">
<el-select v-model="scope.row.goType">
<el-option label="int64" value="int64" />
<el-option label="string" value="string" />
<!-- <el-option label="int" value="int" />
<el-option label="bool" value="bool" /> -->
</el-select>
</template>
</el-table-column>
<el-table-column label="go属性" width="150">
<template #default="scope">
<el-input v-model="scope.row.goField" />
</template>
</el-table-column>
<el-table-column label="json属性" width="150">
<template #default="scope">
<el-input v-model="scope.row.jsonField" />
</template>
</el-table-column>
<el-table-column label="编辑" width="50">
<template #default="scope">
<el-checkbox v-model="scope.row.isInsert" true-label="1" false-label="0" />
</template>
</el-table-column>
<!-- <el-table-column label="编辑" width="70" :render-header="renderHeadeUpdate" :cell-style="{'text-align':'center'}">
<template v-slot="scope">
<el-checkbox v-model="scope.row.isEdit" true-label="1" false-label="0" />
</template>
</el-table-column> -->
<el-table-column label="列表" width="70" :render-header="renderHeadeList" :cell-style="{'text-align':'center'}">
<template #default="scope">
<el-checkbox v-model="scope.row.isList" true-label="1" false-label="0" />
</template>
</el-table-column>
<el-table-column label="查询" width="70" :render-header="renderHeadeSearch" :cell-style="{'text-align':'center'}">
<template #default="scope">
<el-checkbox v-model="scope.row.isQuery" true-label="1" false-label="0" />
</template>
</el-table-column>
<el-table-column label="查询方式" width="120">
<template #default="scope">
<el-select v-model="scope.row.queryType">
<el-option label="=" value="EQ" />
<el-option label="!=" value="NE" />
<el-option label=">" value="GT" />
<el-option label=">=" value="GTE" />
<el-option label="<" value="LT" />
<el-option label="<=" value="LTE" />
<el-option label="LIKE" value="LIKE" />
<!-- <el-option label="BETWEEN" value="BETWEEN" /> -->
</el-select>
</template>
</el-table-column>
<el-table-column label="必填" width="50">
<template #default="scope">
<el-checkbox v-model="scope.row.isRequired" true-label="1" />
</template>
</el-table-column>
<el-table-column label="显示类型" width="140">
<template #default="scope">
<el-select v-model="scope.row.htmlType">
<el-option label="文本框" value="input" />
<el-option label="下拉框" value="select" />
<el-option label="单选框" value="radio" />
<!-- <el-option label="文件选择" value="file" /> -->
<!-- <el-option label="复选框" value="checkbox" />
<el-option label="日期控件" value="datetime" />-->
<el-option label="文本域" value="textarea" />
</el-select>
</template>
</el-table-column>
<el-table-column label="字典类型" width="160">
<template #default="scope">
<el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
<el-option
v-for="dict in dictOptions"
:key="dict.dictType"
:label="dict.dictName"
:value="dict.dictType"
>
<span style="float: left">{{ dict.dictName }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ dict.dictType }}</span>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="关系表" width="160">
<template #default="scope">
<el-select v-model="scope.row.fkTableName" clearable filterable placeholder="请选择" @change="handleChangeConfig(scope.row,scope.$index)">
<el-option
v-for="table in tableTree"
:key="table.tableName"
:label="table.tableName"
:value="table.tableName"
>
<span style="float: left">{{ table.tableName }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ table.tableComment }}</span>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="关系表key" width="150">
<template #default="scope">
<el-select v-model="scope.row.fkLabelId" clearable filterable placeholder="请选择">
<el-option
v-for="column in scope.row.fkCol"
:key="column.columnName"
:label="column.columnName"
:value="column.jsonField"
>
<span style="float: left">{{ column.jsonField }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ column.columnComment }}</span>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="关系表value" width="150">
<template #default="scope">
<el-select v-model="scope.row.fkLabelName" clearable filterable placeholder="请选择">
<el-option
v-for="column in scope.row.fkCol"
:key="column.columnName"
:label="column.columnName"
:value="column.jsonField"
>
<span style="float: left">{{ column.jsonField }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ column.columnComment }}</span>
</el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="生成信息" name="genInfo">
<gen-info-form ref="genInfo" :info="info" />
</el-tab-pane>
</el-tabs>
<el-form label-width="100px">
<el-form-item style="text-align: center;margin-left:-100px;margin-top:10px;">
<el-button type="primary" @click="submitForm()">提交</el-button>
<el-button @click="close()">返回</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script>
import { getGenTable, updateGenTable, getTableTree } from '@/api/tools/gen'
// import { listTable } from '@/api/tools/gen'
import { optionselect as getDictOptionselect } from '@/api/admin/dict/type'
import basicInfoForm from './basicInfoForm'
import genInfoForm from './genInfoForm'
export default {
name: 'GenEdit',
components: {
basicInfoForm,
genInfoForm
},
data() {
return {
// 选中选项卡的 name
activeName: 'cloum',
// 表格的高度
tableHeight: document.documentElement.scrollHeight - 245 + 'px',
// 表列信息
columns: [],
tableTree: [],
// 字典信息
dictOptions: [],
// 表详细信息
info: {}
}
},
beforeCreate() {
getTableTree().then(response => {
this.tableTree = response.data
this.tableTree.unshift({ tableId: 0, className: '请选择' })
})
const { tableId } = this.$route.query
if (tableId) {
// 获取表详细信息
getGenTable(tableId).then(res => {
this.columns = res.data.list
this.info = res.data.info
this.info.isDataScope = this.info.isDataScope.toString()
this.info.isActions = this.info.isActions.toString()
this.info.isAuth = this.info.isAuth.toString()
this.columns.forEach(item => {
this.tableTree.filter(function(e) {
if (e.tableId === item.fkTableNameClass) {
item.fkCol = e.columns || [{ columnId: 0, columnName: '请选择' }]
// item.fkCol.unshift({ columnId: 0, columnName: '请选择' })
}
})
})
})
/** 查询字典下拉列表 */
getDictOptionselect().then(response => {
this.dictOptions = response.data
})
}
},
methods: {
renderHeadeUpdate(h, { column, $index }) {
// h 是一个渲染函数 column 是一个对象表示当前列 $index 第几列
return h('div', [
h('span', column.label + ' ', { align: 'center', marginTop: '0px' }),
h(
'el-popover',
{ props: { placement: 'top-start', width: '270', trigger: 'hover' }},
[
h('p', '是否在表单编辑时能够编辑,打√表示需要', { class: 'text-align: center; margin: 0' }),
// 生成 i 标签 ,添加icon 设置 样式,slot 必填
h('i', { class: 'ri-question-line', style: 'color:#ccc,padding-top:5px', slot: 'reference' })
]
)
])
},
renderHeadeList(h, { column, $index }) {
// h 是一个渲染函数 column 是一个对象表示当前列 $index 第几列
return h('div', [
h('span', column.label + ' ', { align: 'center', marginTop: '0px' }),
h(
'el-popover',
{ props: { placement: 'top-start', width: '260', trigger: 'hover' }},
[
h('p', '是否在列表中展示,打√表示需要展示', { class: 'text-align: center; margin: 0' }),
h('i', { class: 'ri-question-line', style: 'color:#ccc,padding-top:5px', slot: 'reference' })
]
)
])
},
renderHeadeSearch(h, { column, $index }) {
return h('div', [
h('span', column.label + ' ', { align: 'center', marginTop: '0px' }),
h(
'el-popover',
{ props: { placement: 'top-start', width: '270', trigger: 'hover' }},
[
h('p', '是都当做搜索条件,打√表示做为搜索条件', { class: 'text-align: center; margin: 0' }),
h('i', { class: 'ri-question-line', style: 'color:#ccc,padding-top:5px', slot: 'reference' })
]
)
])
},
handleChangeConfig(row, index) {
this.tableTree.filter(function(item) {
if (item.tableName === row.fkTableName) {
row.fkCol = item.columns
// row.fkCol.unshift({ columnId: 0, columnName: '请选择' })
}
})
},
/** 提交按钮 */
submitForm() {
const basicForm = this.$refs.basicInfo.$refs.basicInfoForm
const genForm = this.$refs.genInfo.$refs.genInfoForm
Promise.all([basicForm, genForm].map(this.getFormPromise)).then(res => {
const validateResult = res.every(item => !!item)
if (validateResult) {
const genTable = Object.assign({}, basicForm.model, genForm.model)
genTable.columns = this.columns
genTable.params = {
treeCode: genTable.treeCode,
treeName: genTable.treeName,
treeParentCode: genTable.treeParentCode
}
genTable.isDataScope = JSON.parse(genTable.isDataScope)
genTable.isActions = JSON.parse(genTable.isActions)
genTable.isAuth = JSON.parse(genTable.isAuth)
updateGenTable(genTable).then(res => {
this.msgSuccess(res.msg)
if (res.code === 200) {
this.close()
}
})
} else {
this.msgError('表单校验未通过,请重新检查提交内容')
}
})
},
getTables() {
getTableTree().then(response => {
this.tableTree = response.data
this.tableTree.unshift({ tableId: 0, className: '请选择' })
})
},
getTablesCol(tableName) {
return this.tableTree.filter(function(item) {
if (item.tableName === tableName) {
return item.columns
}
})
},
getFormPromise(form) {
return new Promise(resolve => {
form.validate(res => {
resolve(res)
})
})
},
/** 关闭按钮 */
close() {
this.$store.dispatch('tagsView/delView', this.$route)
this.$router.push({ path: '/dev-tools/gen', query: { t: Date.now() }})
}
}
}
</script>
@@ -0,0 +1,213 @@
<template>
<el-form ref="genInfoForm" :model="formData" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item prop="tplCategory">
<template #label>生成模板</template>
<el-select v-model="formData.tplCategory">
<el-option label="关系表(增删改查)" value="crud" />
<!-- <el-option label="关系表(增删改查)" value="mcrud" />
<el-option label="树表(增删改查)" value="tree" /> -->
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="packageName">
<template #label>应用名
<el-tooltip content="应用名,例如:在app文件夹下将该功能发到那个应用中,默认:admin" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.packageName" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-form-item prop="moduleFrontName">
<template #label>前端文件名
<el-tooltip content="前端项目文件名,例如 sys-user.js " placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.moduleFrontName" />
</el-form-item>
</el-col> -->
<el-col :span="12">
<el-form-item prop="businessName">
<template #label>业务名
<el-tooltip content="可理解为功能英文名,例如 user" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.businessName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="functionName">
<template #label>功能描述
<el-tooltip content="同步的数据库表备注,用作类描述,例如:用户" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.functionName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="moduleName">
<template #label>接口路径
<el-tooltip content="接口路径,例如:api/v1/{sys-user}" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-input v-model="formData.moduleName">
<template #prepend>api/{version}/</template>
<template #append>...</template>
</el-input>
</el-form-item>
<!-- <el-alert
title="接口地址示例"
description="[get]api/{version}/{接口路径} \r\n [post]"
type="success"
show-icon
/> -->
</el-col>
<!-- <el-col :span="12">
<el-form-item prop="isDataScope">
<template #label>是否认证
<el-tooltip content="是指是否使用用户和角色验证中间件" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-select v-model="formData.isAuth">
<el-option label="true" value="1" />
<el-option label="false" value="2" />
</el-select>
</el-form-item>
</el-col> -->
<!-- <el-col :span="12">
<el-form-item prop="isDataScope">
<template #label>数据权限
<el-tooltip content="暂不支持" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-select v-model="formData.isDataScope" disabled>
<el-option label="true" value="1" />
<el-option label="false" value="2" />
</el-select>
</el-form-item>
</el-col> -->
<!-- <el-col :span="12">
<el-form-item prop="isActions">
<template #label>是否actions
<el-tooltip content="系统通用增删改查中间件方法" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-select v-model="formData.isActions" disabled>
<el-option label="false" value="2" />
</el-select>
</el-form-item>
</el-col> -->
</el-row>
<el-row v-show="info.tplCategory == 'tree'">
<h4 class="form-header">其他信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>树编码字段
<el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-select v-model="formData.treeCode" placeholder="请选择">
<el-option
v-for="column in info.columns"
:key="column.columnName"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>树父编码字段
<el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-select v-model="formData.treeParentCode" placeholder="请选择">
<el-option
v-for="column in info.columns"
:key="column.columnName"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>树名称字段
<el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
<i class="ri-question-line" />
</el-tooltip></template>
<el-select v-model="formData.treeName" placeholder="请选择">
<el-option
v-for="column in info.columns"
:key="column.columnName"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
export default {
name: 'GenInfoFormComponent',
props: {
info: {
type: Object,
default: null
}
},
emits: ['update:info'],
data() {
return {
formData: { ...this.info },
rules: {
tplCategory: [
{ required: true, message: '请选择生成模板', trigger: 'blur' }
],
packageName: [
{ required: true, message: '请输入生成包路径', trigger: 'blur' },
{ pattern: /^[a-z]*$/g, trigger: 'blur', message: '只允许小写字母,例如 system 格式' }
],
moduleName: [
{ required: true, message: '请输入生成模块名', trigger: 'blur' },
{ pattern: /^[a-z\-]*[a-z]$/g, trigger: 'blur', message: '只允许小写字母,例如 sys-demo 格式' }
],
businessName: [
{ required: true, message: '请输入生成业务名', trigger: 'blur' },
{ pattern: /^[a-z][A-Za-z]+$/, trigger: 'blur', message: '校验规则: 只允许输入字母 a-z 或大写 A-Z ,并且小写字母开头' }
],
functionName: [
{ required: true, message: '请输入生成功能名', trigger: 'blur' }
]
}
}
},
watch: {
info: {
handler(val) {
this.formData = { ...val }
},
deep: true
},
formData: {
handler(val) {
this.$emit('update:info', val)
},
deep: true
}
},
created() {}
}
</script>
@@ -0,0 +1,122 @@
<template>
<!-- 导入表 -->
<el-dialog v-model="visible" title="导入表" width="800px" top="5vh">
<el-form ref="queryForm" :model="queryParams" :inline="true">
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
placeholder="请输入表名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
placeholder="请输入表描述"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="handleQuery">搜索</el-button>
<el-button size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row>
<el-table ref="table" :data="dbTableList" height="260px" @row-click="clickRow" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="tableName" label="表名称" />
<el-table-column prop="tableComment" label="表描述" />
<el-table-column prop="createdAt" label="创建时间" />
<el-table-column prop="updatedAt" label="更新时间" />
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-row>
<template #footer><div class="dialog-footer">
<el-button type="primary" :loading="loading" @click="handleImportTable">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div></template>
</el-dialog>
</template>
<script>
import { listDbTable, importTable } from '@/api/tools/gen'
export default {
emits: ['ok'],
data() {
return {
loading: false,
// 遮罩层
visible: false,
// 选中数组值
tables: [],
// 总条数
total: 0,
// 表数据
dbTableList: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
tableName: undefined,
tableComment: undefined
}
}
},
methods: {
// 显示弹框
show() {
this.getList()
this.visible = true
},
clickRow(row) {
this.$refs.table.toggleRowSelection(row)
},
// 多选框选中数据
handleSelectionChange(selection) {
this.tables = selection.map(item => item.tableName)
},
// 查询表数据
getList() {
listDbTable(this.queryParams).then(res => {
if (res.code === 200) {
this.dbTableList = res.data.list
this.total = res.data.count
}
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
/** 导入按钮操作 */
handleImportTable() {
this.loading = true
this.visible = true
importTable({ tables: this.tables.join(',') }).then(res => {
this.msgSuccess(res.msg)
if (res.code === 200) {
this.visible = false
this.$emit('ok')
}
this.loading = false
})
}
}
}
</script>
+356
View File
@@ -0,0 +1,356 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
placeholder="请输入表名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="菜单名称" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
placeholder="请输入菜单名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button type="primary" size="small" :icon="Upload" @click="openImportTable">导入</el-button>
<el-button type="primary" size="small" :icon="Edit" :disabled="single" @click="handleEditTable">修改</el-button>
<el-button type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</div>
<el-table v-loading="loading" :data="tableList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="序号" align="center" prop="tableId" width="50px" />
<el-table-column
label="表名称"
align="center"
prop="tableName"
:show-overflow-tooltip="true"
width="130"
/>
<el-table-column
label="菜单名称"
align="center"
prop="tableComment"
:show-overflow-tooltip="true"
width="130"
/>
<el-table-column
label="模型名称"
align="center"
prop="className"
:show-overflow-tooltip="true"
width="130"
/>
<el-table-column label="创建时间" align="center" prop="createdAt" width="165">
<template #default="scope">
<span>{{ parseTime(scope.row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="340">
<template #default="scope">
<el-button type="primary" link size="small" :icon="Edit" @click="handleEditTable(scope.row)">编辑</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link size="small" @click="handlePreview(scope.row)">预览</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link size="small" @click="handleToProject(scope.row)">代码生成</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link size="small" @click="handleToDB(scope.row)">生成配置</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link size="small" @click="handleToApiFile(scope.row)">生成迁移脚本</el-button>
<el-divider direction="vertical" />
<el-button type="danger" link size="small" :icon="Delete" @click="handleSingleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</el-card>
<!-- 预览界面 -->
<el-dialog v-model="open" class="preview" :title="preview.title" :close-on-click-modal="false" fullscreen>
<div class="el-dialog-container">
<div class="tag-group">
<!-- eslint-disable-next-line vue/valid-v-for -->
<el-tag v-for="(value, key) in preview.data" :key="key" @click="codeChange(key)">
{{ key.substring(key.lastIndexOf('/')+1,key.indexOf('.go.template')) }}
</el-tag>
</div>
<div id="codemirror">
<codemirror ref="cmEditor" :value="codestr" :options="cmOptions" />
</div>
<!-- <el-tabs v-model="preview.activeName" tab-position="left">
<el-tab-pane
v-for="(value, key) in preview.data"
:key="key"
:label="key.substring(key.lastIndexOf('/')+1,key.indexOf('.template'))"
:name="key.substring(key.lastIndexOf('/')+1,key.indexOf('.template'))"
>
<pre class="pre"/>
</el-tab-pane>
</el-tabs> -->
</div>
</el-dialog>
<import-table ref="importTB" @ok="handleQuery" />
</template>
</BasicLayout>
</template>
<script>
import { listTable, previewTable, delTable, toDBTable, toProjectTableCheckRole, apiToFile } from '@/api/tools/gen'
import importTable from './importTable'
import { downLoadFile } from '@/utils/zipdownload'
import { codemirror } from 'vue-codemirror'
import { Search, Refresh, Edit, Delete, Upload } from '@element-plus/icons-vue'
// import 'codemirror/theme/material-palenight.css'
// require('codemirror/mode/javascript/javascript')
// import 'codemirror/mode/javascript/javascript'
// import 'codemirror/mode/go/go'
// import 'codemirror/mode/vue/vue'
export default {
name: 'CodeGen',
components: { importTable, codemirror },
setup() {
return { Search, Refresh, Edit, Delete, Upload }
},
data() {
return {
cmOptions: {
tabSize: 4,
theme: 'material-palenight',
mode: 'text/javascript',
lineNumbers: true,
line: true
// more CodeMirror options...
},
codestr: '',
// 遮罩层
loading: true,
// 唯一标识符
uniqueId: '',
// 选中数组
ids: [],
// 选中表数组
tableNames: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 表数据
tableList: [],
// 日期范围
dateRange: '',
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
tableName: undefined,
tableComment: undefined
},
// 预览参数
preview: {
open: false,
title: '代码预览',
data: {},
activeName: 'api.go'
}
}
},
created() {
this.getList()
},
activated() {
const time = this.$route.query.t
if (time !== null && time !== this.uniqueId) {
this.uniqueId = time
this.resetQuery()
}
},
methods: {
/** 查询表集合 */
getList() {
this.loading = true
listTable(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.tableList = response.data.list
this.total = response.data.count
this.loading = false
}
)
},
codeChange(e) {
if (e.indexOf('js') > -1) {
this.cmOptions.mode = 'text/javascript'
}
if (e.indexOf('model') > -1 || e.indexOf('router') > -1 || e.indexOf('api') > -1 || e.indexOf('service') > -1 || e.indexOf('dto') > -1) {
this.cmOptions.mode = 'text/x-go'
}
if (e.indexOf('vue') > -1) {
this.cmOptions.mode = 'text/x-vue'
}
this.codestr = this.preview.data[e]
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 生成代码操作 */
handleGenTable(row) {
const ids = row.tableId || this.ids
if (ids === '') {
this.msgError('请选择要生成的数据')
return
}
downLoadFile('/api/v1/gen/gencode/' + ids)
},
/** 打开导入表弹窗 */
openImportTable() {
this.$refs.importTB.show()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 预览按钮 */
handlePreview(row) {
previewTable(row.tableId).then(response => {
this.preview.data = response.data
this.preview.open = true
this.codeChange('template/api.go.template')
})
},
handleToProject(row) {
toProjectTableCheckRole(row.tableId, false).then((response) => {
this.msgSuccess(response.msg)
}).catch(function() {})
},
handleToApiFile(row) {
apiToFile(row.tableId, true).then((response) => {
this.msgSuccess(response.msg)
}).catch(function() {})
},
handleToDB(row) {
toDBTable(row.tableId).then((response) => {
this.msgSuccess(response.msg)
}).catch(function() {})
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.tableId)
this.tableNames = selection.map(item => item.tableName)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleEditTable(row) {
const tableId = row.tableId || this.ids[0]
this.$router.push({ path: '/dev-tools/editTable', query: { tableId: tableId }})
},
/** 删除按钮操作 */
handleDelete(row) {
const tableIds = row.tableId || this.ids
this.$confirm('是否确认删除表编号为"' + tableIds + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delTable(tableIds)
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
handleSingleDelete(row) {
const tableIds = row.tableId || this.ids
delTable(tableIds).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
}
}
}
</script>
<style lang="scss">
.el-dialog-container {
overflow: hidden;
:deep(.el-scrollbar__view){
height: 100%;
}
.pre{
height: 546px;
overflow: hidden;
.el-scrollbar{
height: 100%;
}
}
:deep(.el-scrollbar__wrap::-webkit-scrollbar){
display: none;
}
}
:deep(.el-dialog__body){
padding: 0 20px;
margin:0;
}
.tag-group {
margin: 0 0 10px -10px;
}
.tag-group .el-tag{
margin-left: 10px;
}
.el-tag {
cursor: pointer;
}
</style>
<style lang="scss">
#codemirror {
height: auto;
margin: 0;
overflow: auto;
}
.CodeMirror {
border: 1px solid #eee;
height: 600px;
}
</style>
@@ -0,0 +1,38 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<div v-loading="loading" :style="'height:' + height">
<iframe
:src="src"
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto"
/>
</div>
</el-card>
</template>
</BasicLayout>
</template>
<script>
export default {
name: 'SwaggerDoc',
components: {},
data() {
return {
src: process.env.VUE_APP_BASE_API + '/swagger/admin/index.html',
height: document.documentElement.clientHeight - 94.5 + 'px;',
loading: true
}
},
mounted: function() {
setTimeout(() => {
this.loading = false
}, 230)
const that = this
window.onresize = function temp() {
that.height = document.documentElement.clientHeight - 94.5 + 'px;'
}
}
}
</script>
+99
View File
@@ -0,0 +1,99 @@
<template>
<div class="errPage-container">
<el-button class="pan-back-btn" @click="back">
返回
</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">
Oops!
</h1>
gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
<h2>你没有权限去该页面</h2>
<h6>如有不满请联系你领导</h6>
<ul class="list-unstyled">
<li>或者你可以去:</li>
<li class="link-type">
<router-link to="/dashboard">
回首页
</router-link>
</li>
<li class="link-type">
<a href="https://www.taobao.com/">随便看看</a>
</li>
<li><a href="#" @click.prevent="dialogVisible=true">点我看图</a></li>
</ul>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
</el-col>
</el-row>
<el-dialog v-model="dialogVisible" title="随便看" :close-on-click-modal="false">
<img :src="ewizardClap" class="pan-img">
</el-dialog>
</div>
</template>
<script>
import errGif from '@/assets/401_images/401.gif'
export default {
name: 'ErrorPage401',
data() {
return {
errGif: errGif + '?' + +new Date(),
ewizardClap: 'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
dialogVisible: false
}
},
methods: {
back() {
if (this.$route.query.noGoBack) {
this.$router.push({ path: '/dashboard' })
} else {
this.$router.go(-1)
}
}
}
}
</script>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none!important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
+228
View File
@@ -0,0 +1,228 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">All rights reserved
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
<a href="" class="bullshit__return-home">Back to home</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return 'The webmaster said that you can not enter this page...'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<template>
<router-view />
</template>
<script>
export default {
name: 'LogContainer'
}
</script>
@@ -0,0 +1,15 @@
<script>
export default {
name: 'AuthRedirect',
created() {
const hash = window.location.search.slice(1)
if (window.localStorage) {
window.localStorage.setItem('x-admin-oauth-code', hash)
window.close()
}
},
render: function(h) {
return h() // avoid warning message
}
}
</script>
@@ -0,0 +1,72 @@
<template>
<div class="social-signup-container">
<div class="sign-btn" @click="wechatHandleClick('wechat')">
<span class="wx-svg-container"><svg-icon icon-class="wechat" class="icon" /></span>
WeChat
</div>
<div class="sign-btn" @click="tencentHandleClick('tencent')">
<span class="qq-svg-container"><svg-icon icon-class="qq" class="icon" /></span>
QQ
</div>
</div>
</template>
<script>
// import openWindow from '@/utils/open-window'
export default {
name: 'SocialSignin',
methods: {
wechatHandleClick(thirdpart) {
alert('ok')
// this.$store.commit('SET_AUTH_TYPE', thirdpart)
// const appid = 'xxxxx'
// const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
// const url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&response_type=code&scope=snsapi_login#wechat_redirect'
// openWindow(url, thirdpart, 540, 540)
},
tencentHandleClick(thirdpart) {
alert('ok')
// this.$store.commit('SET_AUTH_TYPE', thirdpart)
// const client_id = 'xxxxx'
// const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
// const url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirect_uri
// openWindow(url, thirdpart, 540, 540)
}
}
}
</script>
<style lang="scss" scoped>
.social-signup-container {
margin: 20px 0;
.sign-btn {
display: inline-block;
cursor: pointer;
}
.icon {
color: #fff;
font-size: 24px;
margin-top: 8px;
}
.wx-svg-container,
.qq-svg-container {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
padding-top: 1px;
border-radius: 4px;
margin-bottom: 20px;
margin-right: 5px;
}
.wx-svg-container {
background-color: #24da70;
}
.qq-svg-container {
background-color: #6BA2D6;
margin-left: 50px;
}
}
</style>
+698
View File
@@ -0,0 +1,698 @@
<template>
<div class="login-page">
<!-- 左侧品牌区 -->
<div class="login-brand">
<!-- 几何装饰 -->
<div class="brand-deco brand-deco--circle1" />
<div class="brand-deco brand-deco--circle2" />
<div class="brand-deco brand-deco--circle3" />
<div class="brand-content">
<div class="brand-logo-wrap">
<img
v-if="sysInfo && sysInfo.sys_app_logo"
:src="sysInfo.sys_app_logo"
class="brand-logo-img"
alt="logo"
>
<el-icon v-else class="brand-logo-icon"><Monitor /></el-icon>
</div>
<h1 class="brand-title">{{ sysInfo && sysInfo.sys_app_name || 'Sense' }}</h1>
<p class="brand-desc">视频设备接入、监看与区域配置</p>
<div class="brand-features">
<div class="feature-item">
<el-icon><Lock /></el-icon>
<span>安全可靠</span>
</div>
<div class="feature-item">
<el-icon><Monitor /></el-icon>
<span>实时监控</span>
</div>
<div class="feature-item">
<el-icon><User /></el-icon>
<span>权限管理</span>
</div>
</div>
</div>
<div class="brand-footer">
<a href="https://beian.miit.gov.cn" target="_blank" class="icp-link">沪ICP备XXXXXXXXX号-1</a>
</div>
</div>
<!-- 右侧表单区 -->
<div class="login-form-panel">
<!-- 装饰光晕 -->
<div class="panel-blob panel-blob--tr" />
<div class="panel-blob panel-blob--bl" />
<div class="form-box">
<div class="form-header">
<span class="form-badge">账号登录</span>
<h2 class="form-title">欢迎回来</h2>
<p class="form-subtitle">请输入您的账号和密码继续</p>
</div>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
label-position="top"
class="login-form"
autocomplete="on"
@submit.prevent="handleLogin"
>
<el-form-item label="账号" prop="username">
<el-input
ref="username"
v-model="loginForm.username"
placeholder="请输入账号"
name="username"
type="text"
tabindex="1"
autocomplete="on"
size="large"
:prefix-icon="User"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="请输入密码"
name="password"
tabindex="2"
autocomplete="on"
size="large"
:prefix-icon="Lock"
@keyup="checkCapslock"
@blur="capsTooltip = false"
@keyup.enter="handleLogin"
>
<template #suffix>
<el-icon class="pwd-eye" @click="showPwd">
<View v-if="passwordType === 'password'" />
<Hide v-else />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="验证码" prop="code">
<div class="captcha-row">
<el-input
v-model="loginForm.code"
placeholder="请输入验证码"
name="code"
type="text"
tabindex="3"
maxlength="5"
autocomplete="off"
size="large"
:prefix-icon="Key"
@keyup.enter="handleLogin"
/>
<div class="captcha-wrap" title="点击刷新" @click="getCode">
<img v-if="codeUrl" :src="codeUrl" class="captcha-img" alt="验证码">
<div v-else class="captcha-placeholder">
<el-icon class="is-loading"><Loading /></el-icon>
</div>
</div>
</div>
</el-form-item>
<el-button
:loading="loading"
type="primary"
size="large"
class="submit-btn"
@click.prevent="handleLogin"
>
{{ loading ? '登录中...' : '登 录' }}
</el-button>
</el-form>
</div>
</div>
</div>
</template>
<script>
import { getCodeImg } from '@/api/login'
import { User, Lock, Key, View, Hide, Monitor, Loading } from '@element-plus/icons-vue'
export default {
name: 'LoginPage',
setup() {
return { User, Lock, Key, View, Hide, Monitor, Loading }
},
data() {
return {
codeUrl: '',
loginForm: {
username: '',
password: '',
code: '',
uuid: ''
},
loginRules: {
username: [{ required: true, trigger: 'blur', message: '用户名不能为空' }],
password: [{ required: true, trigger: 'blur', message: '密码不能为空' }],
code: [{ required: true, trigger: 'change', message: '验证码不能为空' }]
},
passwordType: 'password',
capsTooltip: false,
loading: false,
redirect: undefined,
otherQuery: {},
sysInfo: ''
}
},
watch: {
$route: {
handler(route) {
const query = route.query
if (query) {
this.redirect = query.redirect
this.otherQuery = this.getOtherQuery(query)
}
},
immediate: true
}
},
created() {
this.getCode()
this.getSystemSetting()
},
mounted() {
if (!this.loginForm.username) {
this.$refs.username.focus()
} else if (!this.loginForm.password) {
this.$refs.password.focus()
}
},
methods: {
getSystemSetting() {
this.$store.dispatch('system/settingDetail').then((ret) => {
this.sysInfo = ret
document.title = ret.sys_app_name
})
},
getCode() {
this.codeUrl = ''
getCodeImg().then((res) => {
if (res !== undefined) {
this.codeUrl = res.data
this.loginForm.uuid = res.id
}
})
},
checkCapslock({ shiftKey, key } = {}) {
if (key && key.length === 1) {
if ((shiftKey && key >= 'a' && key <= 'z') || (!shiftKey && key >= 'A' && key <= 'Z')) {
this.capsTooltip = true
} else {
this.capsTooltip = false
}
}
if (key === 'CapsLock' && this.capsTooltip === true) {
this.capsTooltip = false
}
},
showPwd() {
this.passwordType = this.passwordType === 'password' ? '' : 'password'
this.$nextTick(() => this.$refs.password.focus())
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true
this.$store
.dispatch('user/login', this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery }).catch(() => {})
})
.catch(() => {
this.loading = false
this.getCode()
})
}
})
},
getOtherQuery(query) {
return Object.keys(query).reduce((acc, cur) => {
if (cur !== 'redirect') acc[cur] = query[cur]
return acc
}, {})
}
}
}
</script>
<style lang="scss" scoped>
/* ── 页面容器 ── */
.login-page {
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
}
/* ══════════════════════════════
左侧品牌区
══════════════════════════════ */
.login-brand {
position: relative;
width: 45%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 48px 40px;
overflow: hidden;
background: linear-gradient(145deg, #002c8c 0%, #1677ff 55%, #40a9ff 100%);
/* 浮动圆形装饰 */
.brand-deco {
position: absolute;
border-radius: 50%;
opacity: 0.12;
background: #fff;
}
.brand-deco--circle1 {
width: 380px;
height: 380px;
top: -100px;
right: -120px;
}
.brand-deco--circle2 {
width: 220px;
height: 220px;
bottom: 60px;
left: -60px;
}
.brand-deco--circle3 {
width: 120px;
height: 120px;
bottom: 220px;
right: 40px;
opacity: 0.08;
}
}
.brand-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
margin-top: auto;
margin-bottom: auto;
}
.brand-logo-wrap {
width: 64px;
height: 64px;
background: rgba(255, 255, 255, 0.2);
border: 1.5px solid rgba(255, 255, 255, 0.35);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.brand-logo-img {
width: 42px;
height: 42px;
object-fit: contain;
border-radius: 8px;
}
.brand-logo-icon {
font-size: 30px;
color: #fff;
}
.brand-title {
margin: 0;
font-size: 32px;
font-weight: 700;
color: #fff;
letter-spacing: 1px;
line-height: 1.2;
}
.brand-desc {
margin: 0;
font-size: 15px;
color: rgba(255, 255, 255, 0.72);
line-height: 1.6;
}
.brand-features {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 8px;
}
.feature-item {
display: flex;
align-items: center;
gap: 10px;
color: rgba(255, 255, 255, 0.85);
font-size: 14px;
.el-icon {
font-size: 16px;
background: rgba(255, 255, 255, 0.15);
padding: 6px;
border-radius: 8px;
color: #fff;
}
}
.brand-footer {
position: relative;
z-index: 1;
}
.icp-link {
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
text-decoration: none;
transition: color 0.2s;
&:hover {
color: rgba(255, 255, 255, 0.7);
}
}
/* ══════════════════════════════
右侧表单区
══════════════════════════════ */
.login-form-panel {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
overflow: hidden;
/* 浅渐变底色 + SVG 点阵纹理 */
background-color: #e8f4ff;
background-image:
linear-gradient(135deg, #e8f4ff 0%, #f0f8ff 50%, #f5fbff 100%),
radial-gradient(circle, rgba(22, 119, 255, 0.07) 1px, transparent 1px);
background-size: 100% 100%, 28px 28px;
}
/* 柔光晕装饰 */
.panel-blob {
position: absolute;
border-radius: 50%;
pointer-events: none;
filter: blur(72px);
}
.panel-blob--tr {
width: 340px;
height: 340px;
top: -80px;
right: -80px;
background: rgba(22, 119, 255, 0.15);
}
.panel-blob--bl {
width: 260px;
height: 260px;
bottom: -60px;
left: -60px;
background: rgba(64, 169, 255, 0.12);
}
.form-box {
position: relative;
z-index: 1;
width: 100%;
max-width: 400px;
background: #fff;
border-radius: 16px;
padding: 40px 36px 36px;
box-shadow:
0 0 0 1px rgba(22, 119, 255, 0.08),
0 4px 6px rgba(0, 0, 0, 0.04),
0 16px 40px rgba(22, 119, 255, 0.12);
animation: form-enter 0.4s cubic-bezier(0.34, 1.06, 0.64, 1) forwards;
/* 顶部彩色渐变条 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
border-radius: 16px 16px 0 0;
background: linear-gradient(90deg, #1677ff 0%, #40a9ff 100%);
}
}
@keyframes form-enter {
0% { opacity: 0; transform: translateY(20px) scale(0.98); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
.form-header {
margin-bottom: 28px;
}
.form-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
color: #1677ff;
background: rgba(22, 119, 255, 0.08);
border: 1px solid rgba(22, 119, 255, 0.2);
margin-bottom: 12px;
}
.form-title {
margin: 0 0 6px;
font-size: 26px;
font-weight: 700;
background: linear-gradient(135deg, #002c8c 0%, #1677ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.form-subtitle {
margin: 0;
font-size: 13px;
color: #9ca3af;
}
/* ── 表单样式 ── */
.login-form {
:deep(.el-form-item__label) {
font-size: 13px;
font-weight: 600;
color: #374151;
padding-bottom: 6px;
line-height: 1;
}
:deep(.el-form-item) {
margin-bottom: 20px;
}
:deep(.el-input__wrapper) {
border-radius: 8px;
box-shadow: 0 0 0 1px #e5e7eb !important;
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: 0 0 0 1px #d1d5db !important;
}
}
:deep(.el-input.is-focus .el-input__wrapper) {
box-shadow: 0 0 0 2px #3b82f6 !important;
}
:deep(.el-form-item.is-error .el-input__wrapper) {
box-shadow: 0 0 0 2px #ef4444 !important;
}
:deep(.el-input__prefix-inner .el-icon) {
color: #9ca3af;
font-size: 16px;
}
:deep(.el-input__inner) {
color: #111827;
font-size: 14px;
&::placeholder {
color: #c1c7d0;
}
}
}
.pwd-eye {
color: #9ca3af;
font-size: 16px;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #3b82f6;
}
}
/* ── 验证码 ── */
.captcha-row {
display: flex;
gap: 10px;
align-items: center;
.el-input {
flex: 1;
}
}
.captcha-wrap {
width: 110px;
height: 40px;
flex-shrink: 0;
border-radius: 8px;
border: 1px solid #e5e7eb;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: #f9fafb;
transition: border-color 0.2s;
&:hover {
border-color: #3b82f6;
}
}
.captcha-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.captcha-placeholder {
color: #c1c7d0;
font-size: 18px;
}
/* ── 登录按钮 ── */
.submit-btn {
width: 100%;
height: 46px;
margin-top: 12px;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
letter-spacing: 4px;
background: linear-gradient(135deg, #1677ff 0%, #40a9ff 100%) !important;
border: none !important;
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.4);
transition: all 0.25s ease;
&:hover {
background: linear-gradient(135deg, #0958d9 0%, #1677ff 100%) !important;
box-shadow: 0 8px 24px rgba(22, 119, 255, 0.5);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.3);
}
&.is-loading {
transform: none;
opacity: 0.85;
}
}
/* ── 响应式 ── */
@media (max-width: 900px) {
.login-brand {
width: 40%;
padding: 36px 28px;
}
.brand-title {
font-size: 26px;
}
.form-box {
padding: 36px 28px;
}
}
@media (max-width: 680px) {
.login-page {
flex-direction: column;
}
.login-brand {
width: 100%;
height: 200px;
flex-direction: row;
align-items: center;
padding: 24px 28px;
.brand-content {
flex-direction: row;
align-items: center;
gap: 16px;
margin: 0;
}
.brand-desc,
.brand-features {
display: none;
}
.brand-title {
font-size: 22px;
}
.brand-logo-wrap {
width: 48px;
height: 48px;
}
}
.login-form-panel {
flex: 1;
padding: 24px 16px;
overflow-y: auto;
}
.form-box {
padding: 28px 20px;
}
}
</style>
@@ -0,0 +1,44 @@
<template>
<el-form>
<el-form-item label="Name">
<el-input v-model.trim="userForm.name" />
</el-form-item>
<el-form-item label="Email">
<el-input v-model.trim="userForm.email" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">Update</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'UserAccount',
props: {
user: {
type: Object,
default: () => {
return {
name: '',
email: ''
}
}
}
},
data() {
return {
userForm: { ...this.user }
}
},
methods: {
submit() {
this.$message({
message: 'User information has been updated successfully',
type: 'success',
duration: 5 * 1000
})
}
}
}
</script>
@@ -0,0 +1,186 @@
<template>
<div class="user-activity">
<div class="post">
<div class="user-block">
<img class="img-circle" :src="'https://wpimg.wallstcn.com/57ed425a-c71e-4201-9428-68760c0537c4.jpg'+avatarPrefix">
<span class="username text-muted">Iron Man</span>
<span class="description">Shared publicly - 7:30 PM today</span>
</div>
<p>
Lorem ipsum represents a long-held tradition for designers,
typographers and the like. Some people hate it and argue for
its demise, but others ignore the hate as they create awesome
tools to help create filler text for everyone from bacon lovers
to Charlie Sheen fans.
</p>
<ul class="list-inline">
<li>
<span class="link-black text-sm">
<i class="ri-share-line" />
Share
</span>
</li>
<li>
<span class="link-black text-sm">
<svg-icon icon-class="like" />
Like
</span>
</li>
</ul>
</div>
<div class="post">
<div class="user-block">
<img class="img-circle" :src="'https://wpimg.wallstcn.com/9e2a5d0a-bd5b-457f-ac8e-86554616c87b.jpg'+avatarPrefix">
<span class="username text-muted">Captain American</span>
<span class="description">Sent you a message - yesterday</span>
</div>
<p>
Lorem ipsum represents a long-held tradition for designers,
typographers and the like. Some people hate it and argue for
its demise, but others ignore the hate as they create awesome
tools to help create filler text for everyone from bacon lovers
to Charlie Sheen fans.
</p>
<ul class="list-inline">
<li>
<span class="link-black text-sm">
<i class="ri-share-line" />
Share
</span>
</li>
<li>
<span class="link-black text-sm">
<svg-icon icon-class="like" />
Like
</span>
</li>
</ul>
</div>
<div class="post">
<div class="user-block">
<img class="img-circle" :src="'https://wpimg.wallstcn.com/fb57f689-e1ab-443c-af12-8d4066e202e2.jpg'+avatarPrefix">
<span class="username">Spider Man</span>
<span class="description">Posted 4 photos - 2 days ago</span>
</div>
<div class="user-images">
<el-carousel :interval="6000" type="card" height="220px">
<el-carousel-item v-for="item in carouselImages" :key="item">
<img :src="item+carouselPrefix" class="image">
</el-carousel-item>
</el-carousel>
</div>
<ul class="list-inline">
<li><span class="link-black text-sm"><i class="ri-share-line" /> Share</span></li>
<li>
<span class="link-black text-sm">
<svg-icon icon-class="like" /> Like</span>
</li>
</ul>
</div>
</div>
</template>
<script>
const avatarPrefix = '?imageView2/1/w/80/h/80'
const carouselPrefix = '?imageView2/2/h/440'
export default {
name: 'UserActivity',
data() {
return {
carouselImages: [
'https://wpimg.wallstcn.com/9679ffb0-9e0b-4451-9916-e21992218054.jpg',
'https://wpimg.wallstcn.com/bcce3734-0837-4b9f-9261-351ef384f75a.jpg',
'https://wpimg.wallstcn.com/d1d7b033-d75e-4cd6-ae39-fcd5f1c0a7c5.jpg',
'https://wpimg.wallstcn.com/50530061-851b-4ca5-9dc5-2fead928a939.jpg'
],
avatarPrefix,
carouselPrefix
}
}
}
</script>
<style lang="scss" scoped>
.user-activity {
.user-block {
.username,
.description {
display: block;
margin-left: 50px;
padding: 2px 0;
}
.username{
font-size: 16px;
color: #000;
}
:after {
clear: both;
}
.img-circle {
border-radius: 50%;
width: 40px;
height: 40px;
float: left;
}
span {
font-weight: 500;
font-size: 12px;
}
}
.post {
font-size: 14px;
border-bottom: 1px solid #d2d6de;
margin-bottom: 15px;
padding-bottom: 15px;
color: #666;
.image {
width: 100%;
height: 100%;
}
.user-images {
padding-top: 20px;
}
}
.list-inline {
padding-left: 0;
margin-left: -5px;
list-style: none;
li {
display: inline-block;
padding-right: 5px;
padding-left: 5px;
font-size: 13px;
}
.link-black {
&:hover,
&:focus {
color: #999;
}
}
}
}
.box-center {
margin: 0 auto;
display: table;
}
.text-muted {
color: #777;
}
</style>
@@ -0,0 +1,44 @@
<template>
<div class="block">
<el-timeline>
<el-timeline-item v-for="(item,index) of timeline" :key="index" :timestamp="item.timestamp" placement="top">
<el-card>
<h4>{{ item.title }}</h4>
<p>{{ item.content }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</template>
<script>
export default {
name: 'UserTimeline',
data() {
return {
timeline: [
{
timestamp: '2019/4/20',
title: 'Update Github template',
content: 'PanJiaChen committed 2019/4/20 20:46'
},
{
timestamp: '2019/4/21',
title: 'Update Github template',
content: 'PanJiaChen committed 2019/4/21 20:46'
},
{
timestamp: '2019/4/22',
title: 'Build Template',
content: 'PanJiaChen committed 2019/4/22 20:46'
},
{
timestamp: '2019/4/23',
title: 'Release New Version',
content: 'PanJiaChen committed 2019/4/23 20:46'
}
]
}
}
}
</script>
@@ -0,0 +1,142 @@
<template>
<el-card style="margin-bottom:20px;">
<template #header>
<div class="clearfix">
<span>About me</span>
</div>
</template>
<div class="user-profile">
<div class="box-center">
<pan-thumb :image="user.avatar" :height="'100px'" :width="'100px'" :hoverable="false">
<div>Hello</div>
{{ user.role }}
</pan-thumb>
</div>
<div class="box-center">
<div class="user-name text-center">{{ user.name }}</div>
<div class="user-role text-center text-muted">{{ uppercaseFirst(user.role) }}</div>
</div>
</div>
<div class="user-bio">
<div class="user-education user-bio-section">
<div class="user-bio-section-header"><svg-icon icon-class="education" /><span>Education</span></div>
<div class="user-bio-section-body">
<div class="text-muted">
JS in Computer Science from the University of Technology
</div>
</div>
</div>
<div class="user-skills user-bio-section">
<div class="user-bio-section-header"><svg-icon icon-class="skill" /><span>Skills</span></div>
<div class="user-bio-section-body">
<div class="progress-item">
<span>Vue</span>
<el-progress :percentage="70" />
</div>
<div class="progress-item">
<span>JavaScript</span>
<el-progress :percentage="18" />
</div>
<div class="progress-item">
<span>Css</span>
<el-progress :percentage="12" />
</div>
<div class="progress-item">
<span>ESLint</span>
<el-progress :percentage="100" status="success" />
</div>
</div>
</div>
</div>
</el-card>
</template>
<script>
import PanThumb from '@/components/PanThumb'
export default {
components: { PanThumb },
props: {
user: {
type: Object,
default: () => {
return {
name: '',
email: '',
avatar: '',
roles: ''
}
}
}
},
methods: {
uppercaseFirst(str) {
if (!str) return ''
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}
</script>
<style lang="scss" scoped>
.box-center {
margin: 0 auto;
display: table;
}
.text-muted {
color: #777;
}
.user-profile {
.user-name {
font-weight: bold;
}
.box-center {
padding-top: 10px;
}
.user-role {
padding-top: 10px;
font-weight: 400;
font-size: 14px;
}
.box-social {
padding-top: 30px;
.el-table {
border-top: 1px solid #dfe6ec;
}
}
.user-follow {
padding-top: 20px;
}
}
.user-bio {
margin-top: 20px;
color: #606266;
span {
padding-left: 4px;
}
.user-bio-section {
font-size: 14px;
padding: 15px 0;
.user-bio-section-header {
border-bottom: 1px solid #dfe6ec;
padding-bottom: 10px;
margin-bottom: 10px;
font-weight: bold;
}
}
}
</style>
+125
View File
@@ -0,0 +1,125 @@
<template>
<BasicLayout>
<template #wrapper>
<el-row :gutter="10">
<el-col :span="6" :xs="24">
<el-card class="box-card">
<template #header>
<div class="clearfix">
<span>个人信息</span>
</div>
</template>
<div>
<div class="text-center">
<userAvatar :user="user" />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon-class="user" />用户名称
<div class="pull-right">{{ user.username }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="phone" />手机号码
<div class="pull-right">{{ user.phone }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="email" />用户邮箱
<div class="pull-right">{{ user.email }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />所属部门
<div class="pull-right">{{ deptName }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="peoples" />所属角色
<div class="pull-right">{{ roleName }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="date" />创建日期
<div class="pull-right">{{ user.createdAt }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-col :span="18" :xs="24">
<el-card>
<template #header>
<div class="clearfix">
<span>基本资料</span>
</div>
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="基本资料" name="userinfo">
<userInfo :user="user" />
</el-tab-pane>
<el-tab-pane label="修改密码" name="resetPwd">
<resetPwd :user="user" />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</template>
</BasicLayout>
</template>
<script>
import userAvatar from './userAvatar'
import userInfo from './userInfo'
import resetPwd from './resetPwd'
import { getUserProfile } from '@/api/admin/sys-user'
export default {
name: 'ProfilePage',
components: { userAvatar, userInfo, resetPwd },
data() {
return {
user: {},
roleGroup: {},
postGroup: {},
deptGroup: {},
activeTab: 'userinfo',
roleIds: undefined,
postIds: undefined,
roleName: undefined,
postName: undefined,
dept: {},
deptName: undefined
}
},
created() {
this.getUser()
},
methods: {
getUser() {
getUserProfile().then(response => {
this.user = response.data.user
this.roleIds = response.data.user.roleIds
this.roleGroup = response.data.roles
if (this.roleIds[0]) {
for (const key in this.roleGroup) {
if (this.roleIds[0] === this.roleGroup[key].roleId) {
this.roleName = this.roleGroup[key].roleName
}
}
} else {
this.roleName = '暂无'
}
this.dept = response.data.user.dept
this.deptName = this.dept.deptName
})
}
}
}
</script>
<style lang="scss" scoped>
.list-group-item{
padding: 18px 0;
}
.svg-icon{
margin-right: 5px;
}
</style>
+76
View File
@@ -0,0 +1,76 @@
<template>
<el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="user.confirmPassword" placeholder="请确认密码" type="password" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">保存</el-button>
<el-button type="danger" size="mini" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { updateUserPwd } from '@/api/admin/sys-user'
export default {
data() {
const equalToPassword = (rule, value, callback) => {
if (this.user.newPassword !== value) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
return {
test: '1test',
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
// 表单校验
rules: {
oldPassword: [
{ required: true, message: '旧密码不能为空', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
]
}
}
},
methods: {
submit() {
this.$refs['form'].validate(valid => {
if (valid) {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(
response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
} else {
this.msgError(response.msg)
}
}
)
}
})
},
close() {
this.$store.dispatch('tagsView/delView', this.$route)
this.$router.push({ path: '/index' })
}
}
}
</script>
+137
View File
@@ -0,0 +1,137 @@
<template>
<div>
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" @click="editCropper()">
<el-dialog v-model="open" :title="title" width="800px" :close-on-click-modal="false">
<el-row>
<el-col :xs="24" :md="12" :style="{height: '350px'}">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:auto-crop="options.autoCrop"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:fixed-box="options.fixedBox"
@real-time="realTime"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{height: '350px'}">
<div class="avatar-upload-preview">
<img :src="previews.url" :style="previews.img">
</div>
</el-col>
</el-row>
<br>
<el-row>
<el-col :lg="2" :md="2">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-button size="small">
上传
<i class="ri-upload-line el-icon--right" />
</el-button>
</el-upload>
</el-col>
<el-col :lg="{span: 1, offset: 2}" :md="2">
<el-button size="small" @click="changeScale(1)" />
</el-col>
<el-col :lg="{span: 1, offset: 1}" :md="2">
<el-button size="small" @click="changeScale(-1)" />
</el-col>
<el-col :lg="{span: 1, offset: 1}" :md="2">
<el-button size="small" @click="rotateLeft()" />
</el-col>
<el-col :lg="{span: 1, offset: 1}" :md="2">
<el-button size="small" @click="rotateRight()" />
</el-col>
<el-col :lg="{span: 2, offset: 6}" :md="2">
<el-button type="primary" size="small" @click="uploadImg()">提 交</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import store from '@/store'
import { VueCropper } from 'vue-cropper'
import { uploadAvatar } from '@/api/admin/sys-user'
export default {
components: { VueCropper },
props: {
// eslint-disable-next-line vue/require-default-prop
user: { type: Object }
},
data() {
return {
// 是否显示弹出层
open: false,
// 弹出层标题
title: '修改头像',
options: {
img: store.getters.avatar, // 裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true // 固定截图框大小 不允许改变
},
previews: {}
}
},
methods: {
// 编辑头像
editCropper() {
this.open = true
},
// 覆盖默认的上传行为
requestUpload() {
},
// 向左旋转
rotateLeft() {
this.$refs.cropper.rotateLeft()
},
// 向右旋转
rotateRight() {
this.$refs.cropper.rotateRight()
},
// 图片缩放
changeScale(num) {
num = num || 1
this.$refs.cropper.changeScale(num)
},
// 上传预处理
beforeUpload(file) {
if (file.type.indexOf('image/') === -1) {
this.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
} else {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
this.options.img = reader.result
}
}
},
// 上传图片
uploadImg() {
this.$refs.cropper.getCropBlob(data => {
const formData = new FormData()
formData.append('upload[]', data)
uploadAvatar(formData).then(response => {
if (response.code === 200) {
this.open = false
this.options.img = process.env.VUE_APP_BASE_API + response.data
this.msgSuccess(response.msg)
} else {
this.msgError(response.msg)
}
this.$refs.cropper.clearCrop()
})
})
},
// 实时预览
realTime(data) {
this.previews = data
}
}
}
</script>
+80
View File
@@ -0,0 +1,80 @@
<template>
<el-form ref="form" :model="userForm" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="userForm.nickName" />
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="userForm.phone" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email" maxlength="50" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="userForm.sex">
<el-radio label="0">男</el-radio>
<el-radio label="1">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">保存</el-button>
<el-button type="danger" size="mini" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { updateUser } from '@/api/admin/sys-user'
export default {
props: {
// eslint-disable-next-line vue/require-default-prop
user: { type: Object }
},
data() {
return {
userForm: { ...this.user },
// 表单校验
rules: {
nickName: [
{ required: true, message: '用户昵称不能为空', trigger: 'blur' }
],
email: [
{ required: true, message: '邮箱地址不能为空', trigger: 'blur' },
{
type: 'email',
message: "'请输入正确的邮箱地址",
trigger: ['blur', 'change']
}
],
phone: [
{ required: true, message: '手机号码不能为空', trigger: 'blur' },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
]
}
}
},
methods: {
submit() {
this.$refs['form'].validate(valid => {
if (valid) {
updateUser(this.userForm).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
} else {
this.msgError(response.msg)
}
})
}
})
},
close() {
this.$store.dispatch('tagsView/delView', this.$route)
this.$router.push({ path: '/index' })
}
}
}
</script>
+13
View File
@@ -0,0 +1,13 @@
<script>
export default {
name: 'RedirectPage',
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>
+503
View File
@@ -0,0 +1,503 @@
<template>
<div>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form ref="queryForm" :model="queryParams" :inline="true" class="search-form">
<el-form-item label="名称" prop="jobName">
<el-input
v-model="queryParams.jobName"
placeholder="请输入名称"
clearable
size="small"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="任务分组" prop="jobGroup">
<el-select
v-model="queryParams.jobGroup"
placeholder="定时任务任务分组"
clearable
size="small"
>
<el-option
v-for="dict in jobGroupOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="定时任务状态"
clearable
size="small"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button size="small" :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar mb8">
<el-button v-permisaction="['job:sysJob:add']" type="primary" size="small" :icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-permisaction="['job:sysJob:edit']" type="primary" size="small" :icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button v-permisaction="['job:sysJob:remove']" type="danger" size="small" :icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button v-permisaction="['job:sysJob:log']" size="small" @click="handleLog">日志</el-button>
</div>
<el-table v-loading="loading" :data="sysjobList" border stripe @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column
label="编码"
align="center"
prop="jobId"
:show-overflow-tooltip="true"
/>
<el-table-column
label="名称"
align="center"
prop="jobName"
:show-overflow-tooltip="true"
/>
<el-table-column
label="任务分组"
align="center"
prop="jobGroup"
:formatter="jobGroupFormat"
width="100"
>
<template #default="scope">
{{ jobGroupFormat(scope.row) }}
</template>
</el-table-column>
<el-table-column
label="cron表达式"
align="center"
prop="cronExpression"
:show-overflow-tooltip="true"
/>
<el-table-column
label="调用目标"
align="center"
prop="invokeTarget"
:show-overflow-tooltip="true"
/>
<el-table-column
label="状态"
align="center"
prop="status"
width="100"
>
<template #default="scope">
<el-tag :type="scope.row.status === '2' || scope.row.status === 2 ? 'success' : 'danger'" size="small" disable-transitions>
{{ statusFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
<template #default="scope">
<el-button v-permisaction="['job:sysJob:edit']" type="primary" link size="small" :icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<template v-if="scope.row.entry_id!==0 && scope.row.status!=1">
<el-divider direction="vertical" />
<el-button v-permisaction="['job:sysJob:remove']" type="primary" link size="small" @click="handleRemove(scope.row)">停止</el-button>
</template>
<template v-if="scope.row.entry_id==0 && scope.row.status!=1">
<el-divider direction="vertical" />
<el-button v-permisaction="['job:sysJob:start']" type="primary" link size="small" @click="handleStart(scope.row)">启动</el-button>
</template>
<el-divider direction="vertical" />
<el-button v-permisaction="['job:sysJob:remove']" type="danger" link size="small" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-model:current-page="queryParams.pageIndex"
v-model:page-size="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
<!-- 添加或修改对话框 -->
<el-dialog v-model="open" v-dialogDrag :title="title" width="700px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="名称" prop="jobName">
<el-input
v-model="form.jobName"
placeholder="名称"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务分组" prop="jobGroup">
<el-select
v-model="form.jobGroup"
placeholder="请选择"
>
<el-option
v-for="dict in jobGroupOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="invokeTarget">
<template #label>
调用目标
<el-tooltip placement="top">
<template #content>
调用示例:func (t *EXEC) ExamplesNoParam(){..} 填写 ExamplesNoParam 即可;
<br>参数说明:目前不支持带参调用
</template>
<i class="ri-question-line" />
</el-tooltip>
</template>
<el-input
v-model="form.invokeTarget"
placeholder="调用目标"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="args">
<template #label>
目标参数
<el-tooltip placement="top">
<template #content>
参数示例:有参:请以string格式填写;无参:为空;
<br>参数说明:目前仅支持函数调用
</template>
<i class="ri-question-line" />
</el-tooltip>
</template>
<el-input
v-model="form.args"
placeholder="目标参数"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="cron表达式" prop="cronExpression">
<el-input
v-model="form.cronExpression"
placeholder="cron表达式"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否并发" prop="concurrent">
<el-radio-group v-model="form.concurrent" size="small">
<el-radio-button label="0">允许</el-radio-button>
<el-radio-button label="1">禁止</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="调用类型" prop="jobType">
<el-radio-group v-model="form.jobType" size="small">
<el-radio-button label="1">接口</el-radio-button>
<el-radio-button label="2">函数</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="执行策略" prop="misfirePolicy">
<el-radio-group v-model="form.misfirePolicy" size="small">
<el-radio-button label="1">立即执行</el-radio-button>
<el-radio-button label="2">执行一次</el-radio-button>
<el-radio-button label="3">放弃执行</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select
v-model="form.status"
placeholder="请选择"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</el-card>
</template>
</BasicLayout>
</div>
</template>
<script>
import { addSysJob, delSysJob, getSysJob, listSysJob, updateSysJob, removeJob, startJob } from '@/api/job/sys-job'
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
export default {
name: 'SysJobManage',
components: {
},
setup() {
return { Search, Refresh, Plus, Edit, Delete }
},
data() {
return {
// 遮罩层
loading: true,
id: 0,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 总条数
total: 0,
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
isEdit: false,
// 类型数据字典
typeOptions: [],
sysjobList: [],
jobGroupOptions: [],
statusOptions: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
jobName: undefined,
jobGroup: undefined,
status: undefined
},
// 表单参数
form: {
},
// 表单校验
rules: {
jobId: [{ required: true, message: '编码不能为空', trigger: 'blur' }],
jobName: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
jobGroup: [{ required: true, message: '任务分组不能为空', trigger: 'blur' }],
cronExpression: [{ required: true, message: 'cron表达式不能为空', trigger: 'blur' }],
invokeTarget: [{ required: true, message: '调用目标不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
}
}
},
created() {
this.getList()
this.getDicts('sys_job_group').then(response => {
this.jobGroupOptions = response.data
})
this.getDicts('sys_job_status').then(response => {
this.statusOptions = response.data
})
},
methods: {
/** 查询参数列表 */
getList() {
this.loading = true
listSysJob(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.sysjobList = response.data.list
this.total = response.data.count
this.loading = false
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
jobId: undefined,
jobName: undefined,
jobGroup: undefined,
cronExpression: undefined,
invokeTarget: undefined,
args: undefined,
misfirePolicy: 1,
concurrent: 1,
jobType: 1,
status: undefined
}
this.resetForm('form')
},
jobGroupFormat(row) {
return this.selectDictLabel(this.jobGroupOptions, row.jobGroup)
},
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, row.status)
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageIndex = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加定时任务'
this.isEdit = false
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.jobId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const jobId = row.jobId || this.ids
getSysJob(jobId).then(response => {
this.form = response.data
this.form.status = String(this.form.status)
this.form.misfirePolicy = String(this.form.misfirePolicy)
this.form.concurrent = String(this.form.concurrent)
this.form.jobType = String(this.form.jobType)
this.open = true
this.title = '修改定时任务'
this.isEdit = true
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.jobId !== undefined) {
this.form.status = parseInt(this.form.status)
this.form.misfirePolicy = parseInt(this.form.misfirePolicy)
this.form.concurrent = parseInt(this.form.concurrent)
this.form.jobType = parseInt(this.form.jobType)
updateSysJob(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
} else {
this.form.status = parseInt(this.form.status)
this.form.misfirePolicy = parseInt(this.form.misfirePolicy)
this.form.concurrent = parseInt(this.form.concurrent)
this.form.jobType = parseInt(this.form.jobType)
addSysJob(this.form).then(response => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const Ids = (row.jobId && [row.jobId]) || this.ids
this.$confirm('是否确认删除编号为"' + Ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return delSysJob({ 'ids': Ids })
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 开始按钮操作 */
handleStart(row) {
this.$confirm('是否确认启动编号为"' + row.jobId + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return startJob(row.jobId)
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
/** 停止按钮操作 */
handleRemove(row) {
this.$confirm('是否确认关闭编号为"' + row.jobId + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(function() {
return removeJob(row.jobId)
}).then((response) => {
if (response.code === 200) {
this.msgSuccess(response.msg)
this.open = false
this.getList()
} else {
this.msgError(response.msg)
}
}).catch(function() {})
},
handleLog() {
this.$router.push({ name: 'job_log', params: { }})
}
}
}
</script>
+100
View File
@@ -0,0 +1,100 @@
<template>
<BasicLayout>
<template #wrapper>
<el-card class="box-card">
<el-form>
<el-form-item>
<el-button type="success" size="mini">状态</el-button>
<el-button type="primary" size="mini">清空</el-button>
</el-form-item>
</el-form>
<el-row ref="log" :gutter="10" class="mb8">
<el-scrollbar style="height:500px;background-color: black;color: cornflowerblue;">
<ul
style="line-height: 25px;padding-top: 15px;padding-bottom: 15px;min-height: 500px; margin: 0;list-style-type: none;"
>
<li v-for="(item,index) in arrs" :key="index">
{{ item }}
</li>
</ul>
</el-scrollbar>
</el-row>
</el-card>
</template>
</BasicLayout>
</template>
<script>
import { unWsLogout } from '@/api/ws'
export default {
name: 'SysJobLogManage',
data() {
return {
websock: null,
arrs: [],
id: undefined,
group: undefined
}
},
created() {
this.id = this.guid()
this.group = 'log'
this.initWebSocket()
},
unmounted() {
console.log('断开websocket连接')
this.websock.close() // 离开路由之后断开websocket连接
unWsLogout(this.id, this.group).then(response => {
console.log(response.data)
}
)
},
methods: {
initWebSocket() { // 初始化weosocket
const wsuri = 'ws://127.0.0.1:8000/ws/' + this.id + '/' + this.group + '?token=' + this.$store.state.user.token
this.websock = new WebSocket(wsuri)
this.websock.onmessage = this.websocketonmessage
this.websock.onopen = this.websocketonopen
this.websock.onerror = this.websocketonerror
this.websock.onclose = this.websocketclose
},
websocketonopen() { // 连接建立之后执行send方法发送数据
console.log('连接打开')
// const actions = { 'test': '12345' }
// this.websocketsend(JSON.stringify(actions))
},
websocketonerror() { // 连接建立失败重连
this.initWebSocket()
},
websocketonmessage(e) { // 数据接收
console.log(e.data)
// console.log(this.binaryAgent(e))
// const redata = JSON.parse(e.data)
// console.log(redata)
// this.$refs.log.innerText = e.data + '\n' + this.$refs.log.innerText
this.arrs.unshift(e.data)
},
websocketsend(Data) { // 数据发送
// this.websock.send(Data)
},
websocketclose(e) { // 关闭
unWsLogout(this.id, this.group).then(response => {
console.log(response.data)
}
)
console.log('断开连接', e)
},
guid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0; var v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
}
}
</script>
+197
View File
@@ -0,0 +1,197 @@
<template>
<div>
<BasicLayout>
<template #wrapper>
<el-row :gutter="10" class="mb10">
<el-col :sm="24" :md="8">
<el-card v-if="info.cpu" class="box-card" shadow="always" :body-style="{paddingTop:'0 !important'}">
<template #header>
<div class="clearfix">
<el-row :gutter="10">
<el-col :sm="24" :md="8">
<el-tag
type="success"
effect="dark"
>
Runing
</el-tag>
</el-col>
<el-col :sm="24" :md="8" class="" style="line-height:28px;text-align:center;">
{{ info.location }}
</el-col>
</el-row>
</div>
</template>
<div class="monitor" style="padding-top:0px;">
<div class="monitor-content">
<el-row :gutter="10">
<el-col :sm="24" :md="12">
<Cell label="系统" :value="info.os.goOs" border />
<Cell label="内存" :value="`${info.mem.used}MB/${info.mem.total}MB`" border />
<Cell label="交换" :value="`${info.swap.used}/${info.swap.total}`" border />
</el-col>
<el-col :sm="24" :md="12">
<Cell label="时间" :value="info.os.time" border />
<Cell label="在线" :value="`${info.bootTime}小时`" border />
<Cell label="硬盘" :value="`${info.disk.used}GB/${info.disk.total}GB`" border />
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :sm="12" :md="12" class="line">
<el-row>
<el-col span="12" :sm="8" :md="8" xs="12">
下载<i class="ri-arrow-down-s-fill" />
</el-col>
<el-col span="12" :sm="16" :md="16" xs="12" class="line-value">
{{ info.net.in }}KB
</el-col>
</el-row>
</el-col>
<el-col :sm="12" :md="12" class="line">
<el-row border>
<el-col span="12" :sm="6" :md="8">
上传<i class="ri-arrow-up-s-fill" />
</el-col>
<el-col span="12" :sm="6" :md="16" class="line-value">
{{ info.net.out }}KB
</el-col>
</el-row>
</el-col>
</el-row>
<el-row :gutter="10" class="monitor-progress">
<el-col :sm="24" :md="4">
CPU
</el-col>
<el-col :sm="24" :md="20">
<el-progress :color="customColors" :text-inside="true" :stroke-width="24" :percentage="info.cpu.percent" />
</el-col>
</el-row>
<el-row :gutter="10" class="monitor-progress">
<el-col :sm="24" :md="4">
RAM
</el-col>
<el-col :sm="24" :md="20">
<el-progress :color="customColors" :text-inside="true" :stroke-width="24" :percentage="info.mem.percent" />
</el-col>
</el-row>
<el-row :gutter="10" class="monitor-progress">
<el-col :sm="24" :md="4">
硬盘
</el-col>
<el-col :sm="24" :md="20">
<el-progress :color="customColors" :text-inside="true" :stroke-width="24" :percentage="info.disk.percent" />
</el-col>
</el-row>
<!-- <el-progress :color="$store.state.settings.theme" type="circle" :percentage="info.cpu.Percent" /> -->
</div>
<!-- <div class="monitor-footer">
<Cell label="CPU主频" :value="info.cpu.cpuInfo[0].modelName.split('@ ')[1]" border />
<Cell label="核心数" :value="`${info.cpu.cpuInfo[0].cores}`" />
</div> -->
</div>
</el-card>
</el-col>
<!-- <el-card v-if="info.os" class="box-card">
<div slot="header" class="clearfix">
<span>go运行环境</span>
</div>
<div class="monitor">
<Cell label="GO 版本" :value="info.os.version" border />
<Cell label="Goroutine" :value="`${info.os.numGoroutine}`" border />
<Cell label="项目地址" :value="info.os.projectDir" />
</div>
</el-card> -->
<el-card v-if="info.os" class="box-card">
<template #header>
<div class="clearfix">
<span>服务器信息</span>
</div>
</template>
<div class="monitor">
<Cell label="主机名称" :value="info.os.hostName" border />
<Cell label="操作系统" :value="info.os.goOs" border />
<Cell label="服务器IP" :value="info.os.ip" border />
<Cell label="系统架构" :value="info.os.arch" border />
<Cell label="CPU" :value="info.cpu.cpuInfo[0].modelName" border />
<Cell label="当前时间" :value="info.os.time" />
</div>
</el-card>
</el-row></template>
</BasicLayout>
</div>
</template>
<script>
import Cell from '@/components/Cell/index'
import {
getServer
} from '@/api/monitor/server'
export default {
name: 'MonitorPage',
components: {
Cell
},
data() {
return {
info: {},
customColors: [
{ color: '#13ce66', percentage: 20 },
{ color: '#1890ff', percentage: 40 },
{ color: '#e6a23c', percentage: 60 },
{ color: '#1989fa', percentage: 80 },
{ color: '#F56C6C', percentage: 100 }
],
timer: null
}
},
created() {
this.getServerInfo()
this.timer = setInterval(() => {
this.getServerInfo()
}, 1000)
},
beforeUnmount() {
clearInterval(this.timer)
this.timer = null
},
methods: {
getServerInfo() {
getServer().then(ret => {
if (ret.code === 200) {
this.info = ret
}
})
}
}
}
</script>
<style lang="scss" scoped>
.line{
line-height: 49px;
font-size: 14px ;
padding-left: 5px !important;
padding-right: 5px !important;
border-bottom: 1px solid #e6ebf5;
.line-value{
text-align: right;
color: #969799;
}
}
.monitor {
.monitor-header {
display: flex;
justify-content: center;
align-items: center;
}
.monitor-progress{
padding-top: 15px;
}
}
</style>