角色管理

This commit is contained in:
zhang zhuo 2025-06-23 18:08:16 +08:00
parent d5789311da
commit 9f07ceda4a
17 changed files with 1055 additions and 18 deletions

View File

@ -1,9 +1,7 @@
const files = import.meta.glob('./model/*.ts', { import auth from "@/api/model/auth"
eager: true, import system from "@/api/model/system"
import: 'default' // 可选,指定要导入的 export
}) export default {
const modules = {} auth,
for (const path in files) { system
modules[path.replace(/(\.\/model\/|\.ts)/g, '')] = files[path]
} }
export default modules

View File

@ -20,5 +20,39 @@ export default {
quick: async function(data = {}){ quick: async function(data = {}){
return await http.post("menu/quick", data); return await http.post("menu/quick", data);
}, },
} },
dept: {
list: async function (data = {}) {
return await http.get("dept/list", data);
},
add: async function (data = {}) {
return await http.post("dept/add", data);
},
edit: async function (data = {}) {
return await http.put("dept/edit", data);
},
del: async function (data = {}) {
return await http.delete("dept/del", data);
},
option: async function (data = {}) {
return await http.get("dept/option", data);
},
},
role: {
list: async function (data = {}) {
return await http.get("role/list", data);
},
add: async function (data = {}) {
return await http.post("role/add", data);
},
edit: async function (data = {}) {
return await http.put("role/edit", data);
},
del: async function (data = {}) {
return await http.delete("role/del", data);
},
option: async function (data = {}) {
return await http.get("role/option", data);
},
},
} }

View File

@ -0,0 +1,3 @@
<template>
<svg t="1750659054773" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10383" width="256" height="256"><path d="M298.666667 554.666667l-4.992 0.298666A42.666667 42.666667 0 0 0 256 597.333333v85.333334h42.666667a42.666667 42.666667 0 0 1 42.368 37.674666L341.333333 725.333333v170.666667a42.666667 42.666667 0 0 1-42.666666 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667v-170.666667a42.666667 42.666667 0 0 1 42.666667-42.666666h42.666667v-85.333334l0.213333-7.509333A128 128 0 0 1 298.666667 469.333333h170.666666V341.333333H341.333333a42.666667 42.666667 0 0 1-42.368-37.674666L298.666667 298.666667V128a42.666667 42.666667 0 0 1 42.666666-42.666667h341.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v170.666667a42.666667 42.666667 0 0 1-42.666666 42.666666h-128v128h170.666666a128 128 0 0 1 127.786667 120.490667L853.333333 597.333333v85.333334h42.666667a42.666667 42.666667 0 0 1 42.368 37.674666L938.666667 725.333333v170.666667a42.666667 42.666667 0 0 1-42.666667 42.666667h-170.666667a42.666667 42.666667 0 0 1-42.666666-42.666667v-170.666667a42.666667 42.666667 0 0 1 42.666666-42.666666h42.666667v-85.333334a42.666667 42.666667 0 0 0-37.674667-42.368L725.333333 554.666667H298.666667z" p-id="10384"></path></svg>
</template>

View File

@ -0,0 +1,3 @@
<template>
<svg t="1750659265544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13130" width="256" height="256"><path d="M844.8 236.2H712.7V109.4c0-23.9-19.4-43.3-43.3-43.3h-316c-23.9 0-43.3 19.4-43.3 43.3v126.8H178c-62.1 0-112.6 50.5-112.6 112.6v498.4c0 62.1 50.5 112.6 112.6 112.6h666.8c62.1 0 112.6-50.5 112.6-112.6V348.8c0-62.1-50.5-112.6-112.6-112.6zM362 118.1h298.7v118.1H362V118.1zM178 288.2h666.9c33.4 0 60.6 27.2 60.6 60.6v136.6H117.4V348.8c0-33.4 27.1-60.6 60.6-60.6zM612.1 545c0 55.5-45.2 100.7-100.7 100.7S410.7 600.6 410.7 545c0-2.5 0.1-5.1 0.3-7.6h200.8c0.2 2.5 0.3 5.1 0.3 7.6z m232.7 362.9H178c-33.4 0-60.6-27.2-60.6-60.6V537.4H359c-0.1 2.5-0.2 5.1-0.2 7.6 0 84.2 68.5 152.7 152.7 152.7S664.1 629.2 664.1 545c0-2.5-0.1-5.1-0.2-7.6h241.6v309.9c-0.1 33.4-27.3 60.6-60.7 60.6z" p-id="13131"></path></svg>
</template>

View File

@ -0,0 +1,3 @@
<template>
<svg t="1750661439662" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8602" width="256" height="256"><path d="M512 87.04c-136.704 0-247.808 111.104-247.808 247.808 0 98.816 58.368 184.32 142.336 223.744-155.136 45.568-268.8 188.928-268.8 358.912 0 10.752 8.704 19.456 19.456 19.456 10.752 0 19.456-8.704 19.456-19.456 0-184.832 150.528-335.36 335.36-335.36 136.704 0 247.808-111.104 247.808-247.808S648.704 87.04 512 87.04z m0 456.192c-115.2 0-208.384-93.696-208.384-208.384 0-115.2 93.696-208.384 208.384-208.384 115.2 0 208.384 93.696 208.384 208.384 0 114.688-93.184 208.384-208.384 208.384z m226.816 76.8c-8.704-6.656-20.992-5.12-27.648 3.584-6.656 8.704-5.12 20.992 3.584 27.648 83.968 64 132.096 161.28 132.096 266.752 0 10.752 8.704 19.456 19.456 19.456 10.752 0 19.456-8.704 19.456-19.456 0.512-118.272-53.248-226.816-146.944-297.984z" p-id="8603"></path></svg>
</template>

View File

@ -0,0 +1,11 @@
<template>
</template>
<script setup>
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
import { h, resolveComponent } from 'vue'
export default {
render() {
return h (
resolveComponent("el-table-column"),
{
index: this.index,
...this.$attrs
},
this.$slots
)
},
methods: {
index(index){
if(this.$attrs.type=="index"){
let page = this.$parent.$parent.currentPage
let pageSize = this.$parent.$parent.pageSize
return (page - 1) * pageSize + index + 1
}
}
}
}

View File

@ -0,0 +1,120 @@
<template>
<div v-if="usercolumn.length>0" class="setting-column" v-loading="isSave">
<div class="setting-column__title">
<span class="move_b"></span>
<span class="show_b">显示</span>
<span class="name_b">名称</span>
<span class="width_b">宽度</span>
<span class="sortable_b">排序</span>
<span class="fixed_b">固定</span>
</div>
<div class="setting-column__list" ref="list">
<ul>
<li v-for="item in usercolumn" :key="item.prop">
<span class="move_b">
<el-tag class="move" style="cursor: move;"><el-icon-d-caret style="width: 1em; height: 1em;"/></el-tag>
</span>
<span class="show_b">
<el-switch v-model="item.hide" :active-value="false" :inactive-value="true"></el-switch>
</span>
<span class="name_b" :title="item.prop">{{ item.label }}</span>
<span class="width_b">
<el-input v-model="item.width" placeholder="auto" size="small"></el-input>
</span>
<span class="sortable_b">
<el-switch v-model="item.sortable"></el-switch>
</span>
<span class="fixed_b">
<el-switch v-model="item.fixed"></el-switch>
</span>
</li>
</ul>
</div>
<div class="setting-column__bottom">
<el-button @click="backDefaul" :disabled="isSave">重置</el-button>
<el-button @click="save" type="primary">保存</el-button>
</div>
</div>
<el-empty v-else description="暂无可配置的列" :image-size="80"></el-empty>
</template>
<script>
import Sortable from 'sortablejs'
export default {
components: {
Sortable
},
props: {
column: { type: Object, default: () => {} }
},
data() {
return {
isSave: false,
usercolumn: JSON.parse(JSON.stringify(this.column||[]))
}
},
watch:{
usercolumn: {
handler(){
this.$emit('userChange', this.usercolumn)
},
deep: true
}
},
mounted() {
this.usercolumn.length>0 && this.rowDrop()
},
methods: {
rowDrop(){
const _this = this
const tbody = this.$refs.list.querySelector('ul')
Sortable.create(tbody, {
handle: ".move",
animation: 300,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.usercolumn
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
}
})
},
backDefaul(){
this.$emit('back', this.usercolumn)
},
save(){
this.$emit('save', this.usercolumn)
}
}
}
</script>
<style scoped>
.setting-column {}
.setting-column__title {border-bottom: 1px solid #EBEEF5;padding-bottom:15px;}
.setting-column__title span {display: inline-block;font-weight: bold;color: #909399;font-size: 12px;}
.setting-column__title span.move_b {width: 30px;margin-right:15px;}
.setting-column__title span.show_b {width: 60px;}
.setting-column__title span.name_b {width: 140px;}
.setting-column__title span.width_b {width: 60px;margin-right:15px;}
.setting-column__title span.sortable_b {width: 60px;}
.setting-column__title span.fixed_b {width: 60px;}
.setting-column__list {max-height:314px;overflow: auto;}
.setting-column__list li {list-style: none;margin:10px 0;display: flex;align-items: center;}
.setting-column__list li>span {display: inline-block;font-size: 12px;}
.setting-column__list li span.move_b {width: 30px;margin-right:15px;}
.setting-column__list li span.show_b {width: 60px;}
.setting-column__list li span.name_b {width: 140px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;cursor:default;}
.setting-column__list li span.width_b {width: 60px;margin-right:15px;}
.setting-column__list li span.sortable_b {width: 60px;}
.setting-column__list li span.fixed_b {width: 60px;}
.setting-column__list li.ghost {opacity: 0.3;}
.setting-column__bottom {border-top: 1px solid #EBEEF5;padding-top:15px;text-align: right;}
.dark .setting-column__title {border-color: var(--el-border-color-light);}
.dark .setting-column__bottom {border-color: var(--el-border-color-light);}
</style>

View File

@ -0,0 +1,472 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
<slot name="do"></slot>
</div>
<div class="right-panel">
<slot name="search"></slot>
</div>
</el-header>
<el-main class="nopadding">
<div :style="{'height':_height}" v-loading="loading">
<div class="pi-table" :style="{'height':_table_height}">
<el-table v-bind="$attrs" :data="tableData" :row-key="rowKey" :key="toggleIndex" ref="piTableRef"
:height="height=='auto'?null:'100%'" :size="_config.size" :border="_config.border"
:stripe="_config.stripe" :summary-method="remoteSummary?remoteSummaryMethod:summaryMethod"
@sort-change="sortChange" @filter-change="filterChange">
<slot></slot>
<template v-for="(item, index) in userColumn" :key="index">
<el-table-column v-if="!item.hide" :column-key="item.prop" :label="item.label"
:prop="item.prop" :width="item.width" :sortable="item.sortable"
:fixed="item.fixed" :filters="item.filters"
:filter-method="remoteFilter||!item.filters?null:filterHandler"
:show-overflow-tooltip="item.showOverflowTooltip">
<template #default="scope">
<slot :name="item.prop" v-bind="scope">
{{ scope.row[item.prop] }}
</slot>
</template>
</el-table-column>
</template>
<el-table-column min-width="1"></el-table-column>
<template #empty>
<el-empty :description="emptyText" :image-size="100"></el-empty>
</template>
</el-table>
</div>
<div class="pi-table-page" v-if="!hidePagination || !hideDo">
<div class="pi-table-pagination">
<el-pagination v-if="!hidePagination" background size="small" :layout="paginationLayout"
:total="total" :page-size="piPageSize" :page-sizes="pageSizes"
v-model:currentPage="currentPage" @current-change="paginationChange"
@update:page-size="pageSizeChange"></el-pagination>
</div>
<div class="pi-table-do" v-if="!hideDo">
<el-button v-if="!hideRefresh" @click="refresh" icon="el-icon-refresh" circle
style="margin-left:15px"></el-button>
<el-popover v-if="column" placement="top" title="列设置" :width="500" trigger="click"
:hide-after="0" @show="customColumnShow=true" @after-leave="customColumnShow=false">
<template #reference>
<el-button icon="el-icon-set-up" circle style="margin-left:15px"></el-button>
</template>
<columnSetting v-if="customColumnShow" ref="columnSettingRef" @userChange="columnSettingChange"
@save="columnSettingSave" @back="columnSettingBack"
:column="userColumn"></columnSetting>
</el-popover>
<el-popover v-if="!hideSetting" placement="top" title="表格设置" :width="400" trigger="click"
:hide-after="0">
<template #reference>
<el-button icon="el-icon-setting" circle style="margin-left:15px"></el-button>
</template>
<el-form label-width="80px" label-position="left">
<el-form-item label="表格尺寸">
<el-radio-group v-model="_config.size" size="small" @change="configSizeChange">
<el-radio-button value="large"></el-radio-button>
<el-radio-button value="default">正常</el-radio-button>
<el-radio-button value="small"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="样式">
<el-checkbox v-model="_config.border" label="纵向边框"></el-checkbox>
<el-checkbox v-model="_config.stripe" label="斑马纹"></el-checkbox>
</el-form-item>
</el-form>
</el-popover>
</div>
</div>
</div>
</el-main>
</el-container>
</template>
<script setup>
import columnSetting from './columnSetting'
import config from "@/config/table"
import {ref, watch, computed, onMounted, onActivated, onDeactivated, getCurrentInstance} from "vue"
import tools from "@/utils/tools.js";
defineOptions({
name: 'piTable'
})
defineExpose({
refresh,
upData,
reload
})
const {proxy} = getCurrentInstance()
const emit = defineEmits(['dataChange'])
const props = defineProps({
tableName: {type: String, default: ""},
apiObj: {
type: Function, default: () => {}
},
workbench: {type: Boolean, default: false},
params: {type: Object, default: () => ({})},
data: {
type: Object, default: () => {}
},
height: {type: [String, Number], default: "100%"},
size: {type: String, default: "default"},
border: {type: Boolean, default: false},
stripe: {type: Boolean, default: false},
pageSize: {type: Number, default: config.pageSize},
pageSizes: {type: Array, default: config.pageSizes},
rowKey: {type: String, default: ""},
summaryMethod: {type: Function, default: null},
column: {
type: Object, default: () => {}
},
remoteSort: {type: Boolean, default: false},
remoteFilter: {type: Boolean, default: false},
remoteSummary: {type: Boolean, default: false},
hidePagination: {type: Boolean, default: false},
hideDo: {type: Boolean, default: false},
hideRefresh: {type: Boolean, default: false},
hideSetting: {type: Boolean, default: false},
paginationLayout: {type: String, default: config.paginationLayout},
})
const piTableRef = ref(null)
const columnSettingRef = ref(null)
let piPageSize = ref(props.pageSize)
let isActive = ref(true)
let emptyText = ref("暂无数据")
let toggleIndex = ref(0)
let tableData = ref([])
let total = ref(0)
let currentPage = ref(0)
let prop = ref(null)
let order = ref(null)
let loading = ref(false)
let tableHeight = ref('100%')
let tableParams = ref(props.params)
let userColumn = ref([])
let customColumnShow = ref(false)
let summary = ref({})
let _config = ref({
size: props.size,
border: props.border,
stripe: props.stripe
})
let nowWork = ref(null)
watch(() => props.data, () => {
tableData.value = props.data;
total.value = tableData.value.length;
})
watch(() => props.apiObj, () => {
tableParams.value = props.params;
refresh();
})
const _height = computed(() => {
return Number(props.height) ? Number(props.height) + 'px' : props.height
})
const _table_height = computed(() => {
return props.hidePagination && props.hideDo ? "100%" : "calc(100% - 50px)"
})
onMounted(() => {
//
if (props.column) {
getCustomColumn()
} else {
userColumn = props.column
}
//
if (props.apiObj) {
getData()
} else if (data.value) {
tableData.value = data.value
total.value = tableData.value.length
}
})
onActivated(() => {
if (!isActive.value) {
piTableRef.value.doLayout()
}
})
onDeactivated(() => {
isActive.value = false
})
async function getCustomColumn() {
userColumn.value = await config.columnSettingGet(props.tableName, props.column)
}
async function getData() {
loading.value = true;
var reqData = {
[config.request.page]: currentPage.value,
[config.request.pageSize]: piPageSize.value,
[config.request.prop]: prop.value,
[config.request.order]: order.value
}
if (props.hidePagination) {
delete reqData[config.request.page]
delete reqData[config.request.pageSize]
}
Object.assign(reqData, tableParams.value)
try {
var res = await props.apiObj(reqData)
} catch (error) {
loading.value = false;
emptyText.value = error.statusText;
return false;
}
try {
var response = config.parseData(res);
} catch (error) {
loading.value = false;
emptyText.value = "数据格式错误";
return false;
}
if (response.code !== config.successCode) {
loading.value = false;
emptyText.value = response.msg;
} else {
emptyText.value = "暂无数据";
if (props.hidePagination) {
tableData.value = response.data || [];
} else {
tableData.value = response.rows || [];
}
if (props.rowKey) {
tableData.value = tools.makeTreeData(tableData.value, 0, props.rowKey)
}
total.value = response.total || 0;
summary.value = response.summary || {};
loading.value = false;
}
piTableRef.value.setScrollTop(0)
emit('dataChange', res, tableData.value)
}
//
function paginationChange(){
getData();
}
//
function pageSizeChange(size){
piPageSize.value = size
getData()
}
//
function refresh(){
piTableRef.value.clearSelection()
getData()
}
// params
function upData(params, page=1){
currentPage.value = page;
piTableRef.value.clearSelection();
Object.assign(tableParams.value, params || {})
getData()
}
// params
function reload(params, page=1){
currentPage.value = page;
tableParams.value = params || {}
piTableRef.value.clearSelection();
piTableRef.value.clearSort()
piTableRef.value.clearFilter()
getData()
}
//
function columnSettingChange(column){
userColumn.value = column;
toggleIndex.value += 1;
}
//
async function columnSettingSave(column){
columnSettingRef.value.isSave = true
try {
await config.columnSettingSave(props.tableName, column)
}catch(error){
proxy.$message.error('保存失败')
columnSettingRef.value.isSave = false
}
proxy.$message.success('保存成功')
columnSettingRef.value.isSave = false
}
//
async function columnSettingBack(){
columnSettingRef.value.isSave = true
try {
const column = await config.columnSettingReset(props.tableName, props.column)
userColumn.value = column
columnSettingRef.value.usercolumn = tools.objCopy(userColumn.value)
}catch(error){
proxy.$message.error('重置失败')
columnSettingRef.value.isSave = false
}
columnSettingRef.value.isSave = false
}
//
function sortChange(obj){
if(!proxy.remoteSort){
return false
}
if(obj.column && obj.prop){
prop.value = obj.prop
order.value = obj.order
}else{
prop.value = null
order.value = null
}
getData()
}
//
function filterHandler(value, row, column){
const property = column.property;
return row[property] === value;
}
//
function filterChange(filters){
if(!props.remoteFilter){
return false
}
Object.keys(filters).forEach(key => {
filters[key] = filters[key].join(',')
})
upData(filters)
}
//
function remoteSummaryMethod(param){
const {columns} = param
const sums = []
columns.forEach((column, index) => {
if(index === 0) {
sums[index] = '合计'
return
}
const values = summary.value[column.property]
if(values){
sums[index] = values
}else{
sums[index] = ''
}
})
return sums
}
function configSizeChange(){
piTableRef.value.doLayout()
}
// unshiftRow
function unshiftRow(row){
tableData.value.unshift(row)
}
// pushRow
function pushRow(row){
tableData.value.push(row)
}
//key
function updateKey(row, rowKey=props.rowKey){
tableData.value.filter(item => item[rowKey]===row[rowKey] ).forEach(item => {
Object.assign(item, row)
})
}
//index
function updateIndex(row, index){
Object.assign(tableData.value[index], row)
}
//index
function removeIndex(index){
tableData.value.splice(index, 1)
}
//index
function removeIndexes(indexes=[]){
indexes.forEach(index => {
tableData.value.splice(index, 1)
})
}
//key
function removeKey(key, rowKey=props.rowKey){
tableData.value.splice(tableData.value.findIndex(item => item[rowKey]===key), 1)
}
//keys
function removeKeys(keys=[], rowKey=props.rowKey){
keys.forEach(key => {
tableData.value.splice(tableData.value.findIndex(item => item[rowKey]===key), 1)
})
}
//
function clearSelection(){
piTableRef.value.clearSelection()
}
function toggleRowSelection(row, selected){
piTableRef.value.toggleRowSelection(row, selected)
}
function toggleAllSelection(){
piTableRef.value.toggleAllSelection()
}
function toggleRowExpansion(row, expanded){
piTableRef.value.toggleRowExpansion(row, expanded)
}
function setCurrentRow(row){
piTableRef.value.setCurrentRow(row)
}
function clearSort(){
piTableRef.value.clearSort()
}
function clearFilter(columnKey){
piTableRef.value.clearFilter(columnKey)
}
function doLayout(){
piTableRef.value.doLayout()
}
function sort(prop, order){
piTableRef.value.sort(prop, order)
}
</script>
<style scoped>
.pi-table {
height: calc(100% - 50px);
}
.pi-table-page {
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
}
.pi-table-do {
white-space: nowrap;
}
.pi-table:deep(.el-table__footer) .cell {
font-weight: bold;
}
.pi-table:deep(.el-table__body-wrapper) .el-scrollbar__bar.is-horizontal {
height: 12px;
border-radius: 12px;
}
.pi-table:deep(.el-table__body-wrapper) .el-scrollbar__bar.is-vertical {
width: 12px;
border-radius: 12px;
}
</style>

68
src/config/table.ts Normal file
View File

@ -0,0 +1,68 @@
import tools from '@/utils/tools'
export default {
successCode: 0, //请求完成代码
pageSize: 20, //表格每一页条数
pageSizes: [10, 20, 30, 40, 50], //表格可设置的一页条数
paginationLayout: "total, sizes, prev, pager, next, jumper", //表格分页布局,可设置"total, sizes, prev, pager, next, jumper"
parseData: function (res) { //数据分析
return {
data: res.data, //分析无分页的数据字段结构
rows: res.data, //分析行数据字段结构
total: res.count, //分析总数字段结构
summary: res.summary, //分析合计行字段结构
msg: res.msg, //分析描述字段结构
code: res.code //分析状态字段结构
}
},
request: { //请求规定字段
page: 'page', //规定当前分页字段
pageSize: 'limit', //规定一页条数字段
prop: 'prop', //规定排序字段名字段
order: 'order' //规定排序规格字段
},
/**
*
* @tableName scTable组件的props->tableName
* @column
*/
columnSettingSave: function (tableName, column) {
return new Promise((resolve) => {
setTimeout(()=>{
//这里为了演示使用了session和setTimeout演示开发时应用数据请求
tools.data.set(tableName, column)
resolve(true)
},1000)
})
},
/**
*
* @tableName scTable组件的props->tableName
* @column props->column
*/
columnSettingGet: function (tableName, column) {
return new Promise((resolve) => {
//这里为了演示使用了session和setTimeout演示开发时应用数据请求
const userColumn = tools.data.get(tableName)
if(userColumn){
resolve(userColumn)
}else{
resolve(column)
}
})
},
/**
*
* @tableName scTable组件的props->tableName
* @column props->column
*/
columnSettingReset: function (tableName, column) {
return new Promise((resolve) => {
//这里为了演示使用了session和setTimeout演示开发时应用数据请求
setTimeout(()=>{
tools.data.remove(tableName)
resolve(column)
},1000)
})
}
}

View File

@ -100,16 +100,16 @@
defineOptions({ defineOptions({
name: "userBar" name: "userBar"
}) })
import {ref, onMounted, getCurrentInstance} from "vue"; import {ref, onMounted, getCurrentInstance} from "vue"
import search from './search.vue' import search from './search.vue'
import setting from './setting.vue' import setting from './setting.vue'
import tasks from './tasks.vue' import tasks from './tasks.vue'
import websocket from "@/utils/websocket"; import websocket from "@/utils/websocket"
import {ElNotification} from 'element-plus'; import {ElNotification} from 'element-plus'
import tools from "@/utils/tools.js"; import tools from "@/utils/tools"
import api from "@/api/index.js"; import api from "@/api/index"
import {useRouter} from "vue-router"; import {useRouter} from "vue-router"
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n"
const {proxy} = getCurrentInstance() const {proxy} = getCurrentInstance()
const router = useRouter() const router = useRouter()

View File

@ -10,11 +10,15 @@ import drag from './directives/drag'
import errorHandler from "@/utils/errorHandler"; import errorHandler from "@/utils/errorHandler";
import piDialog from "@/components/piDialog" import piDialog from "@/components/piDialog"
import piTable from "@/components/piTable"
import piPage from "@/components/piPage"
export default { export default {
install(app: App) { install(app: App) {
// 注册全局组件 // 注册全局组件
app.component('piDialog', piDialog) app.component('piDialog', piDialog)
app.component('piTable', piTable)
app.component('piPage', piPage)
//注册全局指令 //注册全局指令
app.directive('auth', auth) app.directive('auth', auth)

View File

@ -105,7 +105,7 @@ const tools = {
}, },
/* 复制对象 */ /* 复制对象 */
objCopy: function (obj) { objCopy: function (obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj || []));
}, },
/* 日期格式化 */ /* 日期格式化 */
dateFormat: function (date, fmt='yyyy-MM-dd hh:mm:ss') { dateFormat: function (date, fmt='yyyy-MM-dd hh:mm:ss') {
@ -128,6 +128,21 @@ const tools = {
} }
} }
return fmt; return fmt;
},
makeTreeData: function (data, pid = 0, key = "id") {
const arr = [];
for (let item of data) {
if(item.pid == pid){
// 数据格式处理
const tmp = item;
const children = tools.makeTreeData(data, item[key], key);
if (children.length > 0) {
tmp['children'] = children
}
arr.push(tmp)
}
}
return arr
} }
} }

View File

@ -158,7 +158,7 @@ async function delMenu() {
} }
menuloading.value = true menuloading.value = true
var ids = CheckedNodes.map(item => item.menu_id) var ids = CheckedNodes.map(item => item.menu_id)
var res = await api.system.menu.del({menu_id: ids.toString()}) var res = await api.system.menu.del({ids: ids.toString()})
menuloading.value = false menuloading.value = false
proxy.$message.success(res.msg) proxy.$message.success(res.msg)
CheckedNodes.forEach(item => { CheckedNodes.forEach(item => {

View File

@ -0,0 +1,113 @@
<template>
<pi-table ref="tableRef" :apiObj="api.system.role.list" @selection-change="selectionChange" stripe>
<template #do>
<el-button v-auth="'role:add'" type="primary" icon="el-icon-plus" @click="add"></el-button>
<el-button v-auth="'role:del'" type="danger" plain icon="el-icon-delete" :disabled="selection.length==0" @click="batch_del"></el-button>
</template>
<template #search>
<el-input v-model="search.role_name" placeholder="角色名称" clearable style="width: 200px;"></el-input>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</template>
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column label="#" type="index" width="50"></el-table-column>
<el-table-column label="名称" prop="role_name"></el-table-column>
<el-table-column label="状态" prop="status">
<template #default="scope">
<el-tag v-if="scope.row.status===1" type="success">启用</el-tag>
<el-tag v-if="scope.row.status===0" type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" prop="rank"></el-table-column>
<el-table-column label="创建时间" prop="create_time"></el-table-column>
<el-table-column label="操作" fixed="right" align="right" width="170">
<template #default="scope">
<el-button-group>
<el-button text type="primary" size="small" @click="table_show(scope.row, scope.$index)">查看</el-button>
<el-button v-auth="'role:edit'" text type="primary" size="small" @click="table_edit(scope.row, scope.$index)">编辑</el-button>
<el-popconfirm title="确定删除吗?" @confirm="table_del(scope.row, scope.$index)">
<template #reference>
<el-button v-auth="'role:del'" text type="primary" size="small">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
</template>
</el-table-column>
</pi-table>
<save-dialog v-if="dialogShow" ref="saveRef" @success="tableRef.refresh()" @closed="dialogShow = false"></save-dialog>
</template>
<script setup>
import saveDialog from './save'
import {getCurrentInstance, nextTick, ref} from "vue";
import api from "@/api/index";
defineOptions({
name: "systemRole"
})
const {proxy} = getCurrentInstance()
const tableRef = ref(null)
const saveRef = ref(null)
let search = ref({
real_name: ''
})
let selection = ref([])
let dialogShow = ref(false)
//
function add() {
dialogShow.value = true
nextTick(() => {
saveRef.value.open()
})
}
//
function table_edit(row){
dialogShow.value = true
nextTick(() => {
saveRef.value.open('edit')
saveRef.value.setData(row)
})
}
//
function table_show(row){
dialogShow.value = true
nextTick(() => {
saveRef.value.open('show')
saveRef.value.setData(row)
})
}
//
async function table_del(row){
const loading = proxy.$loading();
var res = await api.system.role.del({ids: row.role_id});
tableRef.value.refresh()
loading.close();
proxy.$message.success(res.msg)
}
//
async function batch_del(){
proxy.$confirm(`确定删除选中的 ${selection.value.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
type: 'warning'
}).then(async () => {
const loading = proxy.$loading();
const res = await api.system.role.del({ids: selection.value.map(item => item.role_id).toString()});
tableRef.value.refresh()
loading.close();
proxy.$message.success(res.msg)
})
}
//
function selectionChange(val){
selection.value = val;
}
//
function upsearch(){
tableRef.value.upData(search.value)
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,124 @@
<template>
<pi-dialog :title="titleMap[mode]" v-model="visible" :width="500" destroy-on-close @closed="$emit('closed')">
<el-form :model="form" :rules="rules" :disabled="mode==='show'" ref="formRef" label-width="100px"
label-position="right">
<el-form-item label="名称" prop="role_name">
<el-input v-model="form.role_name" clearable></el-input>
</el-form-item>
<el-form-item label="权限" prop="alias">
<el-tree style="width: 100%;" ref="menuRef" node-key="menu_id" :data="menu.list"
:default-checked-keys="menu.checked"
:props="menu.props" show-checkbox></el-tree>
</el-form-item>
<el-form-item label="排序" prop="rank">
<el-input-number v-model="form.rank" controls-position="right" :min="1"
style="width: 100%;"></el-input-number>
</el-form-item>
<el-form-item label="是否有效" prop="status">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible=false"> </el-button>
<el-button v-if="mode!=='show'" type="primary" :loading="isSaveing" @click="submit()"> </el-button>
</template>
</pi-dialog>
</template>
<script setup>
import {ref, onMounted, getCurrentInstance} from "vue"
import tools from "@/utils/tools"
import api from "@/api/index"
defineExpose({
open,
setData
})
const emit = defineEmits(['success', 'closed'])
const formRef = ref(null)
const menuRef = ref(null)
const {proxy} = getCurrentInstance()
let mode = ref('add')
let titleMap = ref({
add: '新增',
edit: '编辑',
show: '查看'
})
let visible = ref(false)
let isSaveing = ref(false)
let form = ref({
role_id: "",
role_name: "",
menus: [],
checked_menus: "",
rank: 1,
status: 1
})
let menu = ref({
list: [],
props: {
label: (data) => {
return data.title
}
},
checked: []
})
let rules = ref({
rank: [
{required: true, message: '请输入排序', trigger: 'change'}
],
role_name: [
{required: true, message: '请输入角色名称'}
]
})
onMounted(() => {
getMenu()
})
async function getMenu() {
const res = await api.system.menu.list();
menu.value.list = tools.makeTreeData(res.data, 0, "menu_id");
}
//
function open(m = 'add') {
mode.value = m
visible.value = true
}
//
async function submit() {
let checked = menuRef.value.getCheckedNodes().map(item => item.menu_id);
let allChecked = checked.concat(menuRef.value.getHalfCheckedNodes().map(item => item.menu_id))
if (allChecked.length <= 0) return proxy.$message.error("请至少权限")
form.value.menus = allChecked
form.value.checked_menus = checked.toString()
//
const validate = await formRef.value.validate().catch(() => {
});
if (!validate) {
return false
}
isSaveing.value = true;
const res = form.value.role_id ? await api.system.role.edit(form.value) : await api.system.role.add(form.value);
isSaveing.value = false;
emit('success', form.value, mode.value)
visible.value = false;
proxy.$message.success(res.msg)
}
//
function setData(data) {
Object.assign(form.value, data)
if (data.checked_menus) {
menu.value.checked = data.checked_menus.split(",")
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,46 @@
<template>
<el-main>
<el-row :gutter="20">
<el-col :lg="8" :xs="24" :sm="24" :md="24">
<el-card shadow="never" class="pi-left">
<el-avatar :src="userInfo.avatar" :size="64">{{nicknameF}}</el-avatar>
<p style="font-size: 20px;font-weight: bold;margin: 8px;">{{userInfo.nickname||userInfo.username}}</p>
<p>{{userInfo.bio}}</p>
</el-card>
</el-col>
<el-col :lg="16" :xs="24" :sm="24" :md="24">
<el-card shadow="never">
</el-card>
</el-col>
</el-row>
</el-main>
</template>
<script setup>
import tools from "@/utils/tools"
import api from "@/api/index.js";
import {ref, onMounted} from "vue";
defineOptions({
name: "systemUser"
})
let userInfo = ref(tools.data.get("USER_INFO"));
let nickname = userInfo.value.nickname || userInfo.value.username;
let nicknameF = nickname.substring(0, 1);
onMounted(() => {
loadUser()
})
async function loadUser() {
const res = await api.auth.info()
userInfo.value = res.data
}
</script>
<style scoped>
.pi-left {text-align: center;}
</style>