前端代码
This commit is contained in:
@@ -0,0 +1,884 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="kra-search-box">
|
||||
<el-form ref="searchForm" :inline="true" :model="searchInfo">
|
||||
<el-form-item label="路径">
|
||||
<el-input v-model="searchInfo.path" placeholder="路径" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="searchInfo.description" placeholder="描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API分组">
|
||||
<el-select
|
||||
v-model="searchInfo.apiGroup"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiGroupOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求">
|
||||
<el-select v-model="searchInfo.method" clearable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in methodOptions"
|
||||
:key="item.value"
|
||||
:label="`${item.label}(${item.value})`"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog('addApi')">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button icon="delete" :disabled="!apis.length" @click="onDelete">
|
||||
删除
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="onFresh"> 刷新缓存 </el-button>
|
||||
<el-button icon="Compass" @click="onSync"> 同步API </el-button>
|
||||
<ExportTemplate template-id="api" />
|
||||
<ExportExcel template-id="api" :limit="9999" />
|
||||
<ImportExcel template-id="api" @on-success="getTableData" />
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
@sort-change="sortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="ID"
|
||||
min-width="60"
|
||||
prop="ID"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="left" fixed="right" label="操作" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="edit"
|
||||
type="primary"
|
||||
link
|
||||
@click="editApiFunc(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="user"
|
||||
type="primary"
|
||||
link
|
||||
@click="openAssignRoleDrawer(scope.row)"
|
||||
>
|
||||
分配角色
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteApiFunc(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="kra-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="syncApiFlag"
|
||||
:size="appStore.drawerSize"
|
||||
:before-close="closeSyncDialog"
|
||||
:show-close="false"
|
||||
>
|
||||
<warning-bar
|
||||
title="同步API,不输入路由分组将不会被自动同步,如果api不需要参与鉴权,可以按忽略按钮进行忽略。"
|
||||
/>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">同步路由</span>
|
||||
<div>
|
||||
<el-button @click="closeSyncDialog">
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="syncing"
|
||||
@click="enterSyncDialog"
|
||||
>
|
||||
确 定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h4>
|
||||
新增路由
|
||||
<span class="text-xs text-gray-500 mx-2 font-normal"
|
||||
>存在于当前路由中,但是不存在于api表</span
|
||||
>
|
||||
</h4>
|
||||
<el-table
|
||||
v-loading="syncing"
|
||||
:data="syncApiData.newApis"
|
||||
>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.apiGroup"
|
||||
placeholder="请选择或新增"
|
||||
allow-create
|
||||
filterable
|
||||
default-first-option
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiGroupOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" autocomplete="off" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="plus" type="primary" link @click="addApiFunc(row)">
|
||||
单条新增
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="sunrise"
|
||||
type="primary"
|
||||
link
|
||||
@click="ignoreApiFunc(row, true)"
|
||||
>
|
||||
忽略
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h4>
|
||||
已删除路由
|
||||
<span class="text-xs text-gray-500 ml-2 font-normal"
|
||||
>已经不存在于当前项目的路由中,确定同步后会自动从apis表删除</span
|
||||
>
|
||||
</h4>
|
||||
<el-table :data="syncApiData.deleteApis">
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h4>
|
||||
忽略路由
|
||||
<span class="text-xs text-gray-500 ml-2 font-normal"
|
||||
>忽略路由不参与api同步,常见为不需要进行鉴权行为的路由</span
|
||||
>
|
||||
</h4>
|
||||
<el-table :data="syncApiData.ignoreApis">
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
icon="sunny"
|
||||
type="primary"
|
||||
link
|
||||
@click="ignoreApiFunc(row, false)"
|
||||
>
|
||||
取消忽略
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
v-model="dialogFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:before-close="closeDialog"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{ dialogTitle }}</span>
|
||||
<div>
|
||||
<el-button @click="closeDialog"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDialog"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<warning-bar title="新增API,需要在角色管理内配置权限才可使用" />
|
||||
<el-form ref="apiForm" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="路径" prop="path">
|
||||
<el-input v-model="form.path" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请求" prop="method">
|
||||
<el-select
|
||||
v-model="form.method"
|
||||
placeholder="请选择"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in methodOptions"
|
||||
:key="item.value"
|
||||
:label="`${item.label}(${item.value})`"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="api分组" prop="apiGroup">
|
||||
<el-select
|
||||
v-model="form.apiGroup"
|
||||
placeholder="请选择或新增"
|
||||
allow-create
|
||||
filterable
|
||||
default-first-option
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiGroupOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="api简介" prop="description">
|
||||
<el-input v-model="form.description" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 分配给角色抽屉 -->
|
||||
<el-drawer
|
||||
v-model="assignRoleDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">分配角色 - {{ assignApiRow.description }}</span>
|
||||
<div>
|
||||
<el-button @click="assignRoleDrawerVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="assignRoleSubmitting" @click="confirmAssignRole">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<warning-bar title="注:保存时将全量覆盖该API的角色关联关系,并自动刷新Casbin缓存" />
|
||||
<el-tree
|
||||
ref="roleTreeRef"
|
||||
v-loading="assignRoleLoading"
|
||||
:data="authorityTreeData"
|
||||
:props="{ label: 'authorityName', children: 'children' }"
|
||||
node-key="authorityId"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
default-expand-all
|
||||
/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getApiById,
|
||||
getApiList,
|
||||
createApi,
|
||||
updateApi,
|
||||
deleteApi,
|
||||
deleteApisByIds,
|
||||
freshCasbin,
|
||||
syncApi,
|
||||
getApiGroups,
|
||||
ignoreApi,
|
||||
enterSyncApi,
|
||||
getApiRoles,
|
||||
setApiRoles,
|
||||
} from '@/api/api'
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import { toSQLLine } from '@/utils/stringFun'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import ExportExcel from '@/components/exportExcel/exportExcel.vue'
|
||||
import ExportTemplate from '@/components/exportExcel/exportTemplate.vue'
|
||||
import ImportExcel from '@/components/exportExcel/importExcel.vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'Api'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const methodFilter = (value) => {
|
||||
const target = methodOptions.value.filter((item) => item.value === value)[0]
|
||||
return target && `${target.label}`
|
||||
}
|
||||
|
||||
const apis = ref([])
|
||||
const form = ref({
|
||||
path: '',
|
||||
apiGroup: '',
|
||||
method: '',
|
||||
description: ''
|
||||
})
|
||||
const methodOptions = ref([
|
||||
{
|
||||
value: 'POST',
|
||||
label: '创建',
|
||||
type: 'success'
|
||||
},
|
||||
{
|
||||
value: 'GET',
|
||||
label: '查看',
|
||||
type: ''
|
||||
},
|
||||
{
|
||||
value: 'PUT',
|
||||
label: '更新',
|
||||
type: 'warning'
|
||||
},
|
||||
{
|
||||
value: 'DELETE',
|
||||
label: '删除',
|
||||
type: 'danger'
|
||||
},
|
||||
{
|
||||
value: 'PATCH',
|
||||
label: '修改',
|
||||
type: 'warning'
|
||||
}
|
||||
])
|
||||
|
||||
const type = ref('')
|
||||
const rules = ref({
|
||||
path: [{ required: true, message: '请输入api路径', trigger: 'blur' }],
|
||||
apiGroup: [{ required: true, message: '请输入组名称', trigger: 'blur' }],
|
||||
method: [{ required: true, message: '请选择请求方式', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '请输入api介绍', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
const apiGroupOptions = ref([])
|
||||
const apiGroupMap = ref({})
|
||||
|
||||
const getGroup = async () => {
|
||||
const res = await getApiGroups()
|
||||
if (res.code === 0) {
|
||||
const groups = res.data.groups
|
||||
apiGroupOptions.value = groups.map((item) => ({
|
||||
label: item,
|
||||
value: item
|
||||
}))
|
||||
apiGroupMap.value = res.data.apiGroupMap
|
||||
}
|
||||
}
|
||||
|
||||
const ignoreApiFunc = async (row, flag) => {
|
||||
const res = await ignoreApi({ path: row.path, method: row.method, flag })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
if (flag) {
|
||||
syncApiData.value.newApis = syncApiData.value.newApis.filter(
|
||||
(item) => !(item.path === row.path && item.method === row.method)
|
||||
)
|
||||
syncApiData.value.ignoreApis.push(row)
|
||||
return
|
||||
}
|
||||
syncApiData.value.ignoreApis = syncApiData.value.ignoreApis.filter(
|
||||
(item) => !(item.path === row.path && item.method === row.method)
|
||||
)
|
||||
syncApiData.value.newApis.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
const addApiFunc = async (row) => {
|
||||
if (!row.apiGroup) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请先选择API分组'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!row.description) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请先填写API描述'
|
||||
})
|
||||
return
|
||||
}
|
||||
const res = await createApi(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功,请到角色管理页面分配权限',
|
||||
showClose: true
|
||||
})
|
||||
syncApiData.value.newApis = syncApiData.value.newApis.filter(
|
||||
(item) => !(item.path === row.path && item.method === row.method)
|
||||
)
|
||||
}
|
||||
getTableData()
|
||||
getGroup()
|
||||
}
|
||||
|
||||
const closeSyncDialog = () => {
|
||||
syncApiFlag.value = false
|
||||
}
|
||||
|
||||
const syncing = ref(false)
|
||||
|
||||
const enterSyncDialog = async () => {
|
||||
if (
|
||||
syncApiData.value.newApis.some(
|
||||
(item) => !item.apiGroup || !item.description
|
||||
)
|
||||
) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '存在API未分组或未填写描述'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
syncing.value = true
|
||||
const res = await enterSyncApi(syncApiData.value)
|
||||
syncing.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
syncApiFlag.value = false
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
// 搜索
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 排序
|
||||
const sortChange = ({ prop, order }) => {
|
||||
if (prop) {
|
||||
if (prop === 'ID') {
|
||||
prop = 'id'
|
||||
}
|
||||
searchInfo.value.orderKey = toSQLLine(prop)
|
||||
searchInfo.value.desc = order === 'descending'
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getApiList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
getGroup()
|
||||
// 批量操作
|
||||
const handleSelectionChange = (val) => {
|
||||
apis.value = val
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const ids = apis.value.map((item) => item.ID)
|
||||
const res = await deleteApisByIds({ ids })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
if (tableData.value.length === ids.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const onFresh = async () => {
|
||||
ElMessageBox.confirm('确定要刷新缓存吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await freshCasbin()
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const syncApiData = ref({
|
||||
newApis: [],
|
||||
deleteApis: [],
|
||||
ignoreApis: []
|
||||
})
|
||||
|
||||
const syncApiFlag = ref(false)
|
||||
|
||||
const onSync = async () => {
|
||||
const res = await syncApi()
|
||||
if (res.code === 0) {
|
||||
res.data.newApis.forEach((item) => {
|
||||
item.apiGroup = apiGroupMap.value[item.path.split('/')[1]]
|
||||
})
|
||||
|
||||
syncApiData.value = res.data
|
||||
syncApiFlag.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
const apiForm = ref(null)
|
||||
const initForm = () => {
|
||||
apiForm.value.resetFields()
|
||||
form.value = {
|
||||
path: '',
|
||||
apiGroup: '',
|
||||
method: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
const dialogTitle = ref('新增Api')
|
||||
const dialogFormVisible = ref(false)
|
||||
const openDialog = (key) => {
|
||||
switch (key) {
|
||||
case 'addApi':
|
||||
dialogTitle.value = '新增Api'
|
||||
break
|
||||
case 'edit':
|
||||
dialogTitle.value = '编辑Api'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
type.value = key
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
const closeDialog = () => {
|
||||
initForm()
|
||||
dialogFormVisible.value = false
|
||||
}
|
||||
|
||||
const editApiFunc = async (row) => {
|
||||
const res = await getApiById({ id: row.ID })
|
||||
form.value = res.data.api
|
||||
openDialog('edit')
|
||||
}
|
||||
|
||||
const enterDialog = async () => {
|
||||
apiForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
switch (type.value) {
|
||||
case 'addApi':
|
||||
{
|
||||
const res = await createApi(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
getTableData()
|
||||
getGroup()
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
break
|
||||
case 'edit':
|
||||
{
|
||||
const res = await updateApi(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '编辑成功',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
getTableData()
|
||||
closeDialog()
|
||||
}
|
||||
break
|
||||
default:
|
||||
{
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '未知操作',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteApiFunc = async (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除所有角色下该api, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteApi(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
getGroup()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 分配给角色
|
||||
const assignRoleDrawerVisible = ref(false)
|
||||
const assignApiRow = ref({})
|
||||
const authorityTreeData = ref([])
|
||||
const assignRoleLoading = ref(false)
|
||||
const assignRoleSubmitting = ref(false)
|
||||
const roleTreeRef = ref(null)
|
||||
|
||||
const openAssignRoleDrawer = async (row) => {
|
||||
assignApiRow.value = row
|
||||
assignRoleDrawerVisible.value = true
|
||||
assignRoleLoading.value = true
|
||||
const [authRes, rolesRes] = await Promise.all([
|
||||
getAuthorityList(),
|
||||
getApiRoles(row.path, row.method)
|
||||
])
|
||||
if (authRes.code === 0) {
|
||||
authorityTreeData.value = authRes.data
|
||||
}
|
||||
if (rolesRes.code === 0 && rolesRes.data) {
|
||||
nextTick(() => {
|
||||
roleTreeRef.value?.setCheckedKeys(rolesRes.data)
|
||||
})
|
||||
}
|
||||
assignRoleLoading.value = false
|
||||
}
|
||||
|
||||
const confirmAssignRole = async () => {
|
||||
assignRoleSubmitting.value = true
|
||||
try {
|
||||
const checkedKeys = roleTreeRef.value?.getCheckedKeys(false) || []
|
||||
const res = await setApiRoles({
|
||||
path: assignApiRow.value.path,
|
||||
method: assignApiRow.value.method,
|
||||
authorityIds: checkedKeys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '分配成功!' })
|
||||
assignRoleDrawerVisible.value = false
|
||||
}
|
||||
} catch {
|
||||
ElMessage({ type: 'error', message: '分配失败,请重试' })
|
||||
}
|
||||
assignRoleSubmitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.warning {
|
||||
color: #dc143c;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,750 @@
|
||||
<template>
|
||||
<div class="authority">
|
||||
<warning-bar title="注:右上角头像下拉可切换角色" />
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="addAuthority(0)"
|
||||
>新增角色</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
row-key="authorityId"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column label="角色ID" min-width="180" prop="authorityId" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="角色名称"
|
||||
min-width="180"
|
||||
prop="authorityName"
|
||||
/>
|
||||
<el-table-column align="left" label="数据权限" min-width="150">
|
||||
<template #default="scope">
|
||||
<div class="flex items-center gap-1">
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="(cmd) => selectDataScope(scope.row, cmd)"
|
||||
>
|
||||
<el-button link type="primary">
|
||||
{{ dataScopeLabel(scope.row.dataScope) }}
|
||||
<el-icon class="ml-1"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in dataScopeOptions"
|
||||
:key="item.value"
|
||||
:command="item.value"
|
||||
:disabled="scope.row.dataScope === item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button
|
||||
v-if="scope.row.dataScope === 5"
|
||||
icon="setting"
|
||||
type="primary"
|
||||
link
|
||||
title="配置部门集"
|
||||
@click="openDeptScopeDialog(scope.row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" width="560">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="setting"
|
||||
type="primary"
|
||||
link
|
||||
@click="openDrawer(scope.row)"
|
||||
>设置权限</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="user"
|
||||
type="primary"
|
||||
link
|
||||
@click="openAssignDrawer(scope.row)"
|
||||
>分配给用户</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="plus"
|
||||
type="primary"
|
||||
link
|
||||
@click="addAuthority(scope.row.authorityId)"
|
||||
>新增子角色</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="copy-document"
|
||||
type="primary"
|
||||
link
|
||||
@click="copyAuthorityFunc(scope.row)"
|
||||
>拷贝</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="edit"
|
||||
type="primary"
|
||||
link
|
||||
@click="editAuthority(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteAuth(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 新增角色弹窗 -->
|
||||
<el-drawer v-model="authorityFormVisible" :size="appStore.drawerSize" :show-close="false">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{ authorityTitleForm }}</span>
|
||||
<div>
|
||||
<el-button @click="closeAuthorityForm">取 消</el-button>
|
||||
<el-button type="primary" @click="submitAuthorityForm"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form
|
||||
ref="authorityForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="父级角色" prop="parentId">
|
||||
<el-cascader
|
||||
v-model="form.parentId"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'add'"
|
||||
:options="AuthorityOption"
|
||||
:props="{
|
||||
checkStrictly: true,
|
||||
label: 'authorityName',
|
||||
value: 'authorityId',
|
||||
disabled: 'disabled',
|
||||
emitPath: false
|
||||
}"
|
||||
:show-all-levels="false"
|
||||
filterable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色ID" prop="authorityId">
|
||||
<el-input
|
||||
v-model="form.authorityId"
|
||||
:disabled="dialogType === 'edit'"
|
||||
autocomplete="off"
|
||||
maxlength="15"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色姓名" prop="authorityName">
|
||||
<el-input v-model="form.authorityName" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
v-if="drawer"
|
||||
v-model="drawer"
|
||||
:size="appStore.drawerSize"
|
||||
title="角色配置"
|
||||
>
|
||||
<el-tabs :before-leave="autoEnter" type="border-card">
|
||||
<el-tab-pane label="角色菜单">
|
||||
<Menus ref="menus" :row="activeRow" @changeRow="changeRow" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="角色api">
|
||||
<Apis ref="apis" :row="activeRow" @changeRow="changeRow" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 自定义部门集弹窗(数据权限第5档) -->
|
||||
<el-dialog
|
||||
v-model="deptScopeVisible"
|
||||
title="配置可见部门"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<el-scrollbar max-height="50vh">
|
||||
<el-tree
|
||||
ref="deptScopeTreeRef"
|
||||
:data="deptScopeTree"
|
||||
node-key="ID"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
default-expand-all
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<el-button @click="cancelDeptScope">取 消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="deptScopeSaving"
|
||||
@click="confirmDeptScope"
|
||||
>确 定</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配给用户抽屉 -->
|
||||
<el-drawer
|
||||
v-model="assignDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">分配用户 - {{ assignRow.authorityName }}</span>
|
||||
<div>
|
||||
<el-button @click="assignDrawerVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="assignSubmitting" @click="confirmAssign">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<warning-bar title="注:保存时将全量覆盖该角色的用户关联关系;若用户仅剩此一个角色,移除后其主角色保持不变" />
|
||||
<div class="kra-search-box">
|
||||
<el-form :inline="true" :model="userSearchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="userSearchInfo.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="userSearchInfo.nickName" placeholder="请输入昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="searchUserData">查 询</el-button>
|
||||
<el-button icon="refresh" @click="resetUserSearch">重 置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table
|
||||
ref="userTableRef"
|
||||
v-loading="assignLoading"
|
||||
:data="userTableData"
|
||||
row-key="ID"
|
||||
:default-sort="{ prop: 'ID', order: 'descending' }"
|
||||
@sort-change="sortChange"
|
||||
@select="handleSelect"
|
||||
@select-all="handleSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="ID" width="80" sortable="custom" />
|
||||
<el-table-column label="用户名" prop="userName" min-width="120" />
|
||||
<el-table-column label="昵称" prop="nickName" min-width="120" />
|
||||
</el-table>
|
||||
<div class="flex justify-center mt-4">
|
||||
<el-pagination
|
||||
:current-page="userSearchInfo.page"
|
||||
:page-size="userSearchInfo.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="userTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="handleUserPageChange"
|
||||
@size-change="handleUserSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getAuthorityList,
|
||||
deleteAuthority,
|
||||
createAuthority,
|
||||
updateAuthority,
|
||||
copyAuthority,
|
||||
getUsersByAuthorityId,
|
||||
setRoleUsers,
|
||||
setDataScope,
|
||||
getDataScopeDepts
|
||||
} from '@/api/authority'
|
||||
import { getUserList } from '@/api/user'
|
||||
import { getDepartmentList } from '@/api/department'
|
||||
|
||||
import Menus from '@/view/superAdmin/authority/components/menus.vue'
|
||||
import Apis from '@/view/superAdmin/authority/components/apis.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAppStore } from "@/pinia"
|
||||
import { toSQLLine } from '@/utils/stringFun'
|
||||
|
||||
defineOptions({
|
||||
name: 'Authority'
|
||||
})
|
||||
|
||||
const mustUint = (rule, value, callback) => {
|
||||
if (!/^[0-9]*[1-9][0-9]*$/.test(value)) {
|
||||
return callback(new Error('请输入正整数'))
|
||||
}
|
||||
return callback()
|
||||
}
|
||||
|
||||
const AuthorityOption = ref([
|
||||
{
|
||||
authorityId: 0,
|
||||
authorityName: '根角色/严格模式下为当前角色'
|
||||
}
|
||||
])
|
||||
const drawer = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const activeRow = ref({})
|
||||
const appStore = useAppStore()
|
||||
|
||||
const authorityTitleForm = ref('新增角色')
|
||||
const authorityFormVisible = ref(false)
|
||||
const apiDialogFlag = ref(false)
|
||||
const copyForm = ref({})
|
||||
|
||||
// 数据权限档位(挂在角色上)
|
||||
const dataScopeOptions = [
|
||||
{ value: 1, label: '全部数据' },
|
||||
{ value: 3, label: '本部门' },
|
||||
{ value: 2, label: '本部门及以下' },
|
||||
{ value: 4, label: '仅本人' },
|
||||
{ value: 5, label: '自定义部门' }
|
||||
]
|
||||
|
||||
const form = ref({
|
||||
authorityId: 0,
|
||||
authorityName: '',
|
||||
parentId: 0,
|
||||
dataScope: 1
|
||||
})
|
||||
const rules = ref({
|
||||
authorityId: [
|
||||
{ required: true, message: '请输入角色ID', trigger: 'blur' },
|
||||
{ validator: mustUint, trigger: 'blur', message: '必须为正整数' }
|
||||
],
|
||||
authorityName: [
|
||||
{ required: true, message: '请输入角色名', trigger: 'blur' }
|
||||
],
|
||||
parentId: [{ required: true, message: '请选择父角色', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getAuthorityList()
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data
|
||||
rememberScopes(tableData.value)
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// 记录每个角色当前档位, 用于自定义部门弹窗取消/失败时回退
|
||||
const prevScope = {}
|
||||
const rememberScopes = (rows) => {
|
||||
rows &&
|
||||
rows.forEach((row) => {
|
||||
prevScope[row.authorityId] = row.dataScope
|
||||
if (row.children && row.children.length) rememberScopes(row.children)
|
||||
})
|
||||
}
|
||||
|
||||
// 数据权限: 列表内下拉即时保存; 选"自定义部门"时先弹部门选择
|
||||
const changeDataScope = async (row) => {
|
||||
if (row.dataScope === 5) {
|
||||
openDeptScopeDialog(row)
|
||||
return
|
||||
}
|
||||
const res = await setDataScope({
|
||||
authorityId: row.authorityId,
|
||||
dataScope: row.dataScope
|
||||
})
|
||||
if (res.code === 0) {
|
||||
prevScope[row.authorityId] = row.dataScope
|
||||
ElMessage.success('数据权限设置成功')
|
||||
} else {
|
||||
row.dataScope = prevScope[row.authorityId]
|
||||
}
|
||||
}
|
||||
|
||||
// 档位值 -> 中文标签, 用于列表内以文字(而非常驻下拉框)展示当前数据权限
|
||||
const dataScopeLabel = (value) =>
|
||||
dataScopeOptions.find((item) => item.value === value)?.label || '未设置'
|
||||
|
||||
// 点击下拉菜单切换档位: 复用 changeDataScope 的即时保存与失败回退逻辑
|
||||
const selectDataScope = (row, value) => {
|
||||
if (row.dataScope === value) return
|
||||
row.dataScope = value
|
||||
changeDataScope(row)
|
||||
}
|
||||
|
||||
// 自定义部门集弹窗
|
||||
const deptScopeVisible = ref(false)
|
||||
const deptScopeRow = ref({})
|
||||
const deptScopeTree = ref([])
|
||||
const deptScopeTreeRef = ref(null)
|
||||
const deptScopeSaving = ref(false)
|
||||
|
||||
const openDeptScopeDialog = async (row) => {
|
||||
deptScopeRow.value = row
|
||||
deptScopeVisible.value = true
|
||||
const [treeRes, checkedRes] = await Promise.all([
|
||||
getDepartmentList(),
|
||||
getDataScopeDepts(row.authorityId)
|
||||
])
|
||||
if (treeRes.code === 0) {
|
||||
deptScopeTree.value = treeRes.data || []
|
||||
}
|
||||
await nextTick()
|
||||
if (checkedRes.code === 0 && deptScopeTreeRef.value) {
|
||||
deptScopeTreeRef.value.setCheckedKeys(checkedRes.data || [])
|
||||
}
|
||||
}
|
||||
|
||||
const cancelDeptScope = () => {
|
||||
// 取消 → 回退档位
|
||||
deptScopeRow.value.dataScope = prevScope[deptScopeRow.value.authorityId]
|
||||
deptScopeVisible.value = false
|
||||
}
|
||||
|
||||
const confirmDeptScope = async () => {
|
||||
const deptIds = deptScopeTreeRef.value
|
||||
? deptScopeTreeRef.value.getCheckedKeys()
|
||||
: []
|
||||
if (!deptIds.length) {
|
||||
ElMessage.warning('请至少选择一个部门')
|
||||
return
|
||||
}
|
||||
deptScopeSaving.value = true
|
||||
const res = await setDataScope({
|
||||
authorityId: deptScopeRow.value.authorityId,
|
||||
dataScope: 5,
|
||||
deptIds
|
||||
})
|
||||
if (res.code === 0) {
|
||||
prevScope[deptScopeRow.value.authorityId] = 5
|
||||
ElMessage.success('数据权限设置成功')
|
||||
deptScopeVisible.value = false
|
||||
}
|
||||
deptScopeSaving.value = false
|
||||
}
|
||||
|
||||
const changeRow = (key, value) => {
|
||||
activeRow.value[key] = value
|
||||
}
|
||||
const menus = ref(null)
|
||||
const apis = ref(null)
|
||||
const autoEnter = (activeName, oldActiveName) => {
|
||||
const paneArr = [menus, apis]
|
||||
if (oldActiveName) {
|
||||
if (paneArr[oldActiveName].value.needConfirm) {
|
||||
paneArr[oldActiveName].value.enterAndNext()
|
||||
paneArr[oldActiveName].value.needConfirm = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// 拷贝角色
|
||||
const copyAuthorityFunc = (row) => {
|
||||
setOptions()
|
||||
authorityTitleForm.value = '拷贝角色'
|
||||
dialogType.value = 'copy'
|
||||
for (const k in form.value) {
|
||||
form.value[k] = row[k]
|
||||
}
|
||||
copyForm.value = row
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
const openDrawer = (row) => {
|
||||
drawer.value = true
|
||||
activeRow.value = row
|
||||
}
|
||||
// 删除角色
|
||||
const deleteAuth = (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除该角色, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteAuthority({ authorityId: row.authorityId })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
})
|
||||
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
}
|
||||
// 初始化表单
|
||||
const authorityForm = ref(null)
|
||||
const initForm = () => {
|
||||
if (authorityForm.value) {
|
||||
authorityForm.value.resetFields()
|
||||
}
|
||||
form.value = {
|
||||
authorityId: 0,
|
||||
authorityName: '',
|
||||
parentId: 0,
|
||||
dataScope: 1
|
||||
}
|
||||
}
|
||||
// 关闭窗口
|
||||
const closeAuthorityForm = () => {
|
||||
initForm()
|
||||
authorityFormVisible.value = false
|
||||
apiDialogFlag.value = false
|
||||
}
|
||||
// 确定弹窗
|
||||
|
||||
const submitAuthorityForm = () => {
|
||||
authorityForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
form.value.authorityId = Number(form.value.authorityId)
|
||||
switch (dialogType.value) {
|
||||
case 'add':
|
||||
{
|
||||
const res = await createAuthority(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功!'
|
||||
})
|
||||
getTableData()
|
||||
closeAuthorityForm()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'edit':
|
||||
{
|
||||
const res = await updateAuthority(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功!'
|
||||
})
|
||||
getTableData()
|
||||
closeAuthorityForm()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'copy': {
|
||||
const data = {
|
||||
authority: {
|
||||
authorityId: 0,
|
||||
authorityName: '',
|
||||
parentId: 0
|
||||
},
|
||||
oldAuthorityId: 0
|
||||
}
|
||||
data.authority.authorityId = form.value.authorityId
|
||||
data.authority.authorityName = form.value.authorityName
|
||||
data.authority.parentId = form.value.parentId
|
||||
data.authority.dataScope = form.value.dataScope
|
||||
data.oldAuthorityId = copyForm.value.authorityId
|
||||
const res = await copyAuthority(data)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '复制成功!'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initForm()
|
||||
authorityFormVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const setOptions = () => {
|
||||
AuthorityOption.value = [
|
||||
{
|
||||
authorityId: 0,
|
||||
authorityName: '根角色(严格模式下为当前用户角色)'
|
||||
}
|
||||
]
|
||||
setAuthorityOptions(tableData.value, AuthorityOption.value, false)
|
||||
}
|
||||
const setAuthorityOptions = (AuthorityData, optionsData, disabled) => {
|
||||
AuthorityData &&
|
||||
AuthorityData.forEach((item) => {
|
||||
if (item.children && item.children.length) {
|
||||
const option = {
|
||||
authorityId: item.authorityId,
|
||||
authorityName: item.authorityName,
|
||||
disabled: disabled || item.authorityId === form.value.authorityId,
|
||||
children: []
|
||||
}
|
||||
setAuthorityOptions(
|
||||
item.children,
|
||||
option.children,
|
||||
disabled || item.authorityId === form.value.authorityId
|
||||
)
|
||||
optionsData.push(option)
|
||||
} else {
|
||||
const option = {
|
||||
authorityId: item.authorityId,
|
||||
authorityName: item.authorityName,
|
||||
disabled: disabled || item.authorityId === form.value.authorityId
|
||||
}
|
||||
optionsData.push(option)
|
||||
}
|
||||
})
|
||||
}
|
||||
// 增加角色
|
||||
const addAuthority = (parentId) => {
|
||||
initForm()
|
||||
authorityTitleForm.value = '新增角色'
|
||||
dialogType.value = 'add'
|
||||
form.value.parentId = parentId
|
||||
setOptions()
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
// 编辑角色
|
||||
const editAuthority = (row) => {
|
||||
setOptions()
|
||||
authorityTitleForm.value = '编辑角色'
|
||||
dialogType.value = 'edit'
|
||||
for (const key in form.value) {
|
||||
form.value[key] = row[key]
|
||||
}
|
||||
if (!form.value.dataScope) form.value.dataScope = 1
|
||||
setOptions()
|
||||
authorityForm.value && authorityForm.value.clearValidate()
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
|
||||
// 分配给用户
|
||||
const assignDrawerVisible = ref(false)
|
||||
const assignRow = ref({})
|
||||
const userTableData = ref([])
|
||||
const userTotal = ref(0)
|
||||
const userSearchInfo = ref({ page: 1, pageSize: 10, username: '', nickName: '', orderKey: 'id', desc: true })
|
||||
const assignLoading = ref(false)
|
||||
const assignSubmitting = ref(false)
|
||||
const userTableRef = ref(null)
|
||||
|
||||
const selectedUserIds = ref(new Set())
|
||||
|
||||
const openAssignDrawer = async (row) => {
|
||||
assignRow.value = row
|
||||
userSearchInfo.value = { page: 1, pageSize: 10, username: '', nickName: '' }
|
||||
selectedUserIds.value = new Set()
|
||||
assignDrawerVisible.value = true
|
||||
const res = await getUsersByAuthorityId(row.authorityId)
|
||||
if (res.code === 0 && res.data) {
|
||||
selectedUserIds.value = new Set(res.data)
|
||||
}
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const getUserData = async () => {
|
||||
assignLoading.value = true
|
||||
const res = await getUserList(userSearchInfo.value)
|
||||
if (res.code === 0) {
|
||||
userTableData.value = res.data.list
|
||||
userTotal.value = res.data.total
|
||||
await nextTick()
|
||||
userTableData.value.forEach((user) => {
|
||||
userTableRef.value && userTableRef.value.toggleRowSelection(user, selectedUserIds.value.has(user.ID))
|
||||
})
|
||||
}
|
||||
assignLoading.value = false
|
||||
}
|
||||
|
||||
const handleSelect = (selection, row) => {
|
||||
if (selection.some(u => u.ID === row.ID)) {
|
||||
selectedUserIds.value.add(row.ID)
|
||||
} else {
|
||||
selectedUserIds.value.delete(row.ID)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAll = (selection) => {
|
||||
const selectedIds = new Set(selection.map(u => u.ID))
|
||||
userTableData.value.forEach((user) => {
|
||||
if (selectedIds.has(user.ID)) {
|
||||
selectedUserIds.value.add(user.ID)
|
||||
} else {
|
||||
selectedUserIds.value.delete(user.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sortChange = ({ prop, order }) => {
|
||||
if (prop) {
|
||||
userSearchInfo.value.orderKey = prop === 'ID' ? 'id' : toSQLLine(prop)
|
||||
userSearchInfo.value.desc = order === 'descending'
|
||||
}
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const searchUserData = () => {
|
||||
userSearchInfo.value.page = 1
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const resetUserSearch = () => {
|
||||
userSearchInfo.value = { page: 1, pageSize: 10, username: '', nickName: '' }
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const handleUserPageChange = (page) => {
|
||||
userSearchInfo.value.page = page
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const handleUserSizeChange = (size) => {
|
||||
userSearchInfo.value.pageSize = size
|
||||
userSearchInfo.value.page = 1
|
||||
getUserData()
|
||||
}
|
||||
|
||||
const confirmAssign = async () => {
|
||||
assignSubmitting.value = true
|
||||
try {
|
||||
const res = await setRoleUsers({
|
||||
authorityId: assignRow.value.authorityId,
|
||||
userIds: [...selectedUserIds.value]
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '分配成功!' })
|
||||
assignDrawerVisible.value = false
|
||||
}
|
||||
} catch {
|
||||
ElMessage({ type: 'error', message: '分配失败,请重试' })
|
||||
}
|
||||
assignSubmitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.authority {
|
||||
.el-input-number {
|
||||
margin-left: 15px;
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="sticky top-0.5 z-10 flex space-x-2">
|
||||
<el-input
|
||||
v-model="filterTextName"
|
||||
class="flex-1"
|
||||
placeholder="筛选名字"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filterTextPath"
|
||||
class="flex-1"
|
||||
placeholder="筛选路径"
|
||||
/>
|
||||
<el-button class="float-right" type="primary" @click="authApiEnter"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="h-[calc(100vh-208px)] mt-2 overflow-auto">
|
||||
<el-scrollbar>
|
||||
<el-tree
|
||||
ref="apiTree"
|
||||
:data="apiTreeData"
|
||||
:default-checked-keys="apiTreeIds"
|
||||
:props="apiDefaultProps"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
node-key="onlyId"
|
||||
show-checkbox
|
||||
:filter-node-method="filterNode"
|
||||
@check="nodeChange"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex items-center justify-between w-full pr-1">
|
||||
<span>{{ data.description }} </span>
|
||||
<el-tooltip :content="data.path">
|
||||
<span
|
||||
class="max-w-[240px] break-all overflow-ellipsis overflow-hidden"
|
||||
>{{ data.path }}</span
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getAllApis } from '@/api/api'
|
||||
import { UpdateCasbin, getPolicyPathByAuthorityId } from '@/api/casbin'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'Apis'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
default: function () {
|
||||
return {}
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const apiDefaultProps = ref({
|
||||
children: 'children',
|
||||
label: 'description'
|
||||
})
|
||||
const filterTextName = ref('')
|
||||
const filterTextPath = ref('')
|
||||
const apiTreeData = ref([])
|
||||
const apiTreeIds = ref([])
|
||||
const activeUserId = ref('')
|
||||
const init = async () => {
|
||||
const res2 = await getAllApis()
|
||||
const apis = res2.data.apis
|
||||
|
||||
apiTreeData.value = buildApiTree(apis)
|
||||
const res = await getPolicyPathByAuthorityId({
|
||||
authorityId: props.row.authorityId
|
||||
})
|
||||
activeUserId.value = props.row.authorityId
|
||||
apiTreeIds.value = []
|
||||
res.data.paths &&
|
||||
res.data.paths.forEach((item) => {
|
||||
apiTreeIds.value.push('p:' + item.path + 'm:' + item.method)
|
||||
})
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
const needConfirm = ref(false)
|
||||
const nodeChange = () => {
|
||||
needConfirm.value = true
|
||||
}
|
||||
// 暴露给外层使用的切换拦截统一方法
|
||||
const enterAndNext = () => {
|
||||
authApiEnter()
|
||||
}
|
||||
|
||||
// 创建api树方法
|
||||
const buildApiTree = (apis) => {
|
||||
const apiObj = {}
|
||||
apis &&
|
||||
apis.forEach((item) => {
|
||||
item.onlyId = 'p:' + item.path + 'm:' + item.method
|
||||
if (Object.prototype.hasOwnProperty.call(apiObj, item.apiGroup)) {
|
||||
apiObj[item.apiGroup].push(item)
|
||||
} else {
|
||||
Object.assign(apiObj, { [item.apiGroup]: [item] })
|
||||
}
|
||||
})
|
||||
const apiTree = []
|
||||
for (const key in apiObj) {
|
||||
const treeNode = {
|
||||
ID: key,
|
||||
description: key + '组',
|
||||
children: apiObj[key]
|
||||
}
|
||||
apiTree.push(treeNode)
|
||||
}
|
||||
return apiTree
|
||||
}
|
||||
|
||||
// 关联关系确定
|
||||
const apiTree = ref(null)
|
||||
const authApiEnter = async () => {
|
||||
const checkArr = apiTree.value.getCheckedNodes(true)
|
||||
var casbinInfos = []
|
||||
checkArr &&
|
||||
checkArr.forEach((item) => {
|
||||
var casbinInfo = {
|
||||
path: item.path,
|
||||
method: item.method
|
||||
}
|
||||
casbinInfos.push(casbinInfo)
|
||||
})
|
||||
const res = await UpdateCasbin({
|
||||
authorityId: activeUserId.value,
|
||||
casbinInfos
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: 'api设置成功' })
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
needConfirm,
|
||||
enterAndNext
|
||||
})
|
||||
|
||||
const filterNode = (value, data) => {
|
||||
if (!filterTextName.value && !filterTextPath.value) return true
|
||||
let matchesName, matchesPath
|
||||
if (!filterTextName.value) {
|
||||
matchesName = true
|
||||
} else {
|
||||
matchesName =
|
||||
data.description && data.description.includes(filterTextName.value)
|
||||
}
|
||||
if (!filterTextPath.value) {
|
||||
matchesPath = true
|
||||
} else {
|
||||
matchesPath = data.path && data.path.includes(filterTextPath.value)
|
||||
}
|
||||
return matchesName && matchesPath
|
||||
}
|
||||
watch([filterTextName, filterTextPath], () => {
|
||||
apiTree.value.filter('')
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="sticky top-0.5 z-10 pb-2">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<el-input v-model="filterText" class="flex-1" placeholder="筛选" />
|
||||
<el-button type="primary" @click="relation">确 定</el-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="whitespace-nowrap">默认首页:</span>
|
||||
<el-select
|
||||
:model-value="row.defaultRouter"
|
||||
filterable
|
||||
placeholder="请选择默认首页"
|
||||
class="flex-1"
|
||||
@change="handleDefaultRouterChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in menuOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[calc(100vh-254px)] mt-2 overflow-auto clear-both">
|
||||
<el-scrollbar>
|
||||
<el-tree
|
||||
ref="menuTree"
|
||||
:data="menuTreeData"
|
||||
:default-checked-keys="menuTreeIds"
|
||||
:props="menuDefaultProps"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
node-key="ID"
|
||||
show-checkbox
|
||||
:filter-node-method="filterNode"
|
||||
@check="nodeChange"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ node.label }}</span>
|
||||
<SvgIcon v-if="row.defaultRouter === data.name" icon="ant-design:home-filled" class="inline text-lg text-active" />
|
||||
<span v-if="data.menuBtn.length">
|
||||
<el-button type="primary" link @click.stop="() => OpenBtn(data)">
|
||||
分配按钮
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<el-dialog v-model="btnVisible" title="分配按钮" destroy-on-close>
|
||||
<el-table
|
||||
ref="btnTableRef"
|
||||
:data="btnData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="按钮名称" prop="name" />
|
||||
<el-table-column label="按钮备注" prop="desc" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="enterDialog">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getBaseMenuTree,
|
||||
getMenuAuthority,
|
||||
addMenuAuthority
|
||||
} from '@/api/menu'
|
||||
import { updateAuthority } from '@/api/authority'
|
||||
import { getAuthorityBtnApi, setAuthorityBtnApi } from '@/api/authorityBtn'
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'Menus'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
default: function () {
|
||||
return {}
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['changeRow'])
|
||||
const filterText = ref('')
|
||||
const menuTreeData = ref([])
|
||||
const menuTreeIds = ref([])
|
||||
const needConfirm = ref(false)
|
||||
const menuTree = ref(null)
|
||||
const menuDefaultProps = ref({
|
||||
children: 'children',
|
||||
label: function (data) {
|
||||
return data.meta.title
|
||||
},
|
||||
disabled: function (data) {
|
||||
if (props.row.defaultRouter !== data.name) return false
|
||||
// 只在该节点已勾选时禁用,避免出现“默认首页未勾选却无法勾选”的死锁状态
|
||||
const checkedKeys = menuTree.value?.getCheckedKeys?.() || menuTreeIds.value
|
||||
return checkedKeys.includes(Number(data.ID))
|
||||
}
|
||||
})
|
||||
|
||||
const menuOptions = ref([])
|
||||
|
||||
const isExternalRoute = (name) => {
|
||||
if (!name) return false
|
||||
return name.startsWith('http://') || name.startsWith('https://')
|
||||
}
|
||||
|
||||
const findMenuByName = (menus, name) => {
|
||||
for (const item of menus || []) {
|
||||
if (item?.name === name) return item
|
||||
if (item?.children?.length) {
|
||||
const found = findMenuByName(item.children, name)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const buildOptionsFromCheckedLeafMenus = () => {
|
||||
const checkedLeafMenus = menuTree.value
|
||||
? menuTree.value.getCheckedNodes(false, true)
|
||||
: []
|
||||
const options = checkedLeafMenus
|
||||
.filter((item) => item?.name && !isExternalRoute(item.name))
|
||||
.map((item) => ({
|
||||
label: item?.meta?.title || item.name,
|
||||
value: item.name
|
||||
}))
|
||||
|
||||
// 确保当前默认首页能正常显示(即使历史数据不一致)
|
||||
if (props.row.defaultRouter && !options.some(o => o.value === props.row.defaultRouter)) {
|
||||
const found = findMenuByName(menuTreeData.value, props.row.defaultRouter)
|
||||
if (found && !isExternalRoute(found.name)) {
|
||||
options.push({
|
||||
label: found?.meta?.title || found.name,
|
||||
value: found.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
const refreshDefaultRouterOptions = () => {
|
||||
menuOptions.value = buildOptionsFromCheckedLeafMenus()
|
||||
}
|
||||
|
||||
const isDefaultRouterAllowed = (routeName) => {
|
||||
if (!routeName) return false
|
||||
const checkedLeafMenus = menuTree.value
|
||||
? menuTree.value.getCheckedNodes(false, true)
|
||||
: []
|
||||
return checkedLeafMenus.some((item) => item?.name === routeName)
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
// 获取所有菜单树
|
||||
const res = await getBaseMenuTree()
|
||||
menuTreeData.value = res.data.menus
|
||||
const res1 = await getMenuAuthority({ authorityId: props.row.authorityId })
|
||||
const menus = res1.data.menus
|
||||
const arr = []
|
||||
menus.forEach((item) => {
|
||||
// 防止直接选中父级造成全选
|
||||
if (!menus.some((same) => same.parentId === item.menuId)) {
|
||||
arr.push(Number(item.menuId))
|
||||
}
|
||||
})
|
||||
menuTreeIds.value = arr
|
||||
|
||||
// 确保异步数据加载后,树的勾选状态与选项同步
|
||||
await nextTick()
|
||||
if (menuTree.value?.setCheckedKeys) {
|
||||
menuTree.value.setCheckedKeys(menuTreeIds.value)
|
||||
await nextTick()
|
||||
}
|
||||
refreshDefaultRouterOptions()
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
const setDefault = async (data) => {
|
||||
const res = await updateAuthority({
|
||||
authorityId: props.row.authorityId,
|
||||
AuthorityName: props.row.authorityName,
|
||||
parentId: props.row.parentId,
|
||||
defaultRouter: data.name
|
||||
})
|
||||
if (res.code === 0) {
|
||||
relation()
|
||||
emit('changeRow', 'defaultRouter', res.data.authority.defaultRouter)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDefaultRouterChange = (val) => {
|
||||
// 兜底校验:未勾选菜单不允许被设置为默认首页
|
||||
if (!isDefaultRouterAllowed(val)) {
|
||||
ElMessage.warning('未勾选的菜单不可设置为默认首页,请先勾选后再选择')
|
||||
return
|
||||
}
|
||||
setDefault({ name: val })
|
||||
}
|
||||
|
||||
const nodeChange = () => {
|
||||
needConfirm.value = true
|
||||
refreshDefaultRouterOptions()
|
||||
}
|
||||
// 暴露给外层使用的切换拦截统一方法
|
||||
const enterAndNext = () => {
|
||||
relation()
|
||||
}
|
||||
// 关联树 确认方法
|
||||
const relation = async () => {
|
||||
const checkArr = menuTree.value.getCheckedNodes(false, true)
|
||||
const res = await addMenuAuthority({
|
||||
menus: checkArr,
|
||||
authorityId: props.row.authorityId
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '菜单设置成功!'
|
||||
})
|
||||
|
||||
refreshDefaultRouterOptions()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ enterAndNext, needConfirm })
|
||||
|
||||
const btnVisible = ref(false)
|
||||
|
||||
const btnData = ref([])
|
||||
const multipleSelection = ref([])
|
||||
const btnTableRef = ref()
|
||||
let menuID = ''
|
||||
const OpenBtn = async (data) => {
|
||||
menuID = data.ID
|
||||
const res = await getAuthorityBtnApi({
|
||||
menuID: menuID,
|
||||
authorityId: props.row.authorityId
|
||||
})
|
||||
if (res.code === 0) {
|
||||
openDialog(data)
|
||||
await nextTick()
|
||||
if (res.data.selected) {
|
||||
res.data.selected.forEach((id) => {
|
||||
btnData.value.some((item) => {
|
||||
if (item.ID === id) {
|
||||
btnTableRef.value.toggleRowSelection(item, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
const openDialog = (data) => {
|
||||
btnVisible.value = true
|
||||
btnData.value = data.menuBtn
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
btnVisible.value = false
|
||||
}
|
||||
const enterDialog = async () => {
|
||||
const selected = multipleSelection.value.map((item) => item.ID)
|
||||
const res = await setAuthorityBtnApi({
|
||||
menuID,
|
||||
selected,
|
||||
authorityId: props.row.authorityId
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '设置成功' })
|
||||
btnVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true
|
||||
// console.log(data.mate.title)
|
||||
return data.meta.title.indexOf(value) !== -1
|
||||
}
|
||||
|
||||
watch(filterText, (val) => {
|
||||
menuTree.value.filter(val)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
title="注:no_identity=无身份访问受控表(旁路盲区,需补 ctx 或 WithSystem);blocked_write=数据范围过滤后写操作影响 0 行(疑似越权尝试,启发式信号)"
|
||||
/>
|
||||
<div class="kra-search-box">
|
||||
<el-form :inline="true" :model="searchInfo">
|
||||
<el-form-item label="事件类型">
|
||||
<el-select
|
||||
v-model="searchInfo.eventType"
|
||||
clearable
|
||||
placeholder="全部"
|
||||
class="!w-44"
|
||||
>
|
||||
<el-option label="无身份访问" value="no_identity" />
|
||||
<el-option label="疑似越权写" value="blocked_write" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务表">
|
||||
<el-input v-model="searchInfo.targetTable" placeholder="表名(模糊)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>批量删除</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column align="left" label="时间" min-width="160">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.CreatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="事件类型" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.eventType === 'no_identity' ? 'warning' : 'danger'">
|
||||
{{ scope.row.eventType === 'no_identity' ? '无身份访问' : '疑似越权写' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="业务表" min-width="150" prop="targetTable" />
|
||||
<el-table-column align="left" label="操作" min-width="90" prop="operation" />
|
||||
<el-table-column align="left" label="用户ID" min-width="80">
|
||||
<template #default="scope">
|
||||
{{ scope.row.userId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="角色ID" min-width="80">
|
||||
<template #default="scope">
|
||||
{{ scope.row.authorityId || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="来源请求" min-width="220" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.method ? `${scope.row.method} ${scope.row.path}` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="详情" min-width="240" prop="detail" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="kra-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getDataAccessLogList,
|
||||
deleteDataAccessLogByIds
|
||||
} from '@/api/dataAccessLog'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'DataAccessLog'
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const searchInfo = ref({ eventType: '', targetTable: '' })
|
||||
const multipleSelection = ref([])
|
||||
|
||||
const getTableData = async () => {
|
||||
const res = await getDataAccessLogList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list || []
|
||||
total.value = res.data.total
|
||||
}
|
||||
}
|
||||
getTableData()
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
const onReset = () => {
|
||||
searchInfo.value = { eventType: '', targetTable: '' }
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
ElMessageBox.confirm('确定要删除选中的审计记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const ids = multipleSelection.value.map((item) => item.ID)
|
||||
const res = await deleteDataAccessLogByIds({ ids })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
if (tableData.value.length === ids.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="p-2 flex flex-col overflow-hidden"
|
||||
:style="{ height: rootHeight }"
|
||||
>
|
||||
<el-splitter class="flex-1 min-h-0">
|
||||
<el-splitter-panel size="380px" min="260px" max="700px" collapsible>
|
||||
<div
|
||||
class="kra-table-box !my-0 mr-2 h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
<div class="flex justify-between items-center flex-none">
|
||||
<span class="font-bold">部门列表</span>
|
||||
<el-button type="primary" icon="plus" @click="addDepartment(0)"
|
||||
>新增部门</el-button
|
||||
>
|
||||
</div>
|
||||
<el-scrollbar class="mt-3 flex-1">
|
||||
<el-tree
|
||||
:data="tableData"
|
||||
node-key="ID"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
:expand-on-click-node="false"
|
||||
:current-node-key="selectedDept.ID"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div
|
||||
class="flex-1 flex items-center justify-between pr-2 group"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ data.name }}
|
||||
<el-tag
|
||||
v-if="!data.status"
|
||||
size="small"
|
||||
type="info"
|
||||
class="ml-1"
|
||||
>停用</el-tag
|
||||
>
|
||||
</span>
|
||||
<span class="hidden group-hover:inline-flex items-center gap-2">
|
||||
<el-icon
|
||||
class="!text-blue-500"
|
||||
title="新增子部门"
|
||||
@click.stop="addDepartment(data.ID)"
|
||||
>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="!text-blue-500"
|
||||
title="编辑"
|
||||
@click.stop="editDepartment(data)"
|
||||
>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="!text-red-500"
|
||||
title="删除"
|
||||
@click.stop="deleteRow(data)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
<el-splitter-panel :min="300">
|
||||
<div class="kra-table-box !my-0 ml-2 h-full overflow-hidden">
|
||||
<org-member-assign
|
||||
kind="department"
|
||||
:target-id="selectedDept.ID || 0"
|
||||
:target-name="selectedDept.name || ''"
|
||||
/>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<el-drawer
|
||||
v-model="formVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{ titleForm }}</span>
|
||||
<div>
|
||||
<el-button @click="closeForm">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="deptForm" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="上级部门" prop="parentId">
|
||||
<el-cascader
|
||||
v-model="form.parentId"
|
||||
style="width: 100%"
|
||||
:options="deptOptions"
|
||||
:props="{
|
||||
checkStrictly: true,
|
||||
label: 'name',
|
||||
value: 'ID',
|
||||
disabled: 'disabled',
|
||||
emitPath: false
|
||||
}"
|
||||
:show-all-levels="false"
|
||||
filterable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
autocomplete="off"
|
||||
placeholder="请输入部门名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="leaderId">
|
||||
<el-select
|
||||
v-model="form.leaderId"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择负责人"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in userOptions"
|
||||
:key="u.ID"
|
||||
:label="u.nickName || u.userName"
|
||||
:value="u.ID"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input
|
||||
:model-value="selectedLeader?.phone || ''"
|
||||
disabled
|
||||
placeholder="选择负责人后自动带出"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input
|
||||
:model-value="selectedLeader?.email || ''"
|
||||
disabled
|
||||
placeholder="选择负责人后自动带出"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch v-model="form.status" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getDepartmentList,
|
||||
createDepartment,
|
||||
updateDepartment,
|
||||
deleteDepartment
|
||||
} from '@/api/department'
|
||||
import { getUserList } from '@/api/user'
|
||||
import OrgMemberAssign from '@/view/superAdmin/orgMemberAssign.vue'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'Department'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tableData = ref([])
|
||||
const deptOptions = ref([])
|
||||
const selectedDept = ref({})
|
||||
|
||||
// 整页填满视口高度(照字典管理)
|
||||
const rootRef = ref(null)
|
||||
const rootHeight = ref('auto')
|
||||
const updateRootHeight = () => {
|
||||
const el = rootRef.value
|
||||
if (!el) return
|
||||
const top = el.getBoundingClientRect().top
|
||||
rootHeight.value = `${Math.max(320, window.innerHeight - top - 32)}px`
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(updateRootHeight)
|
||||
window.addEventListener('resize', updateRootHeight)
|
||||
})
|
||||
onActivated(updateRootHeight)
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
|
||||
|
||||
// 负责人从用户列表选取;联系电话/邮箱从所选负责人用户带出
|
||||
const userOptions = ref([])
|
||||
const selectedLeader = computed(() =>
|
||||
userOptions.value.find((u) => u.ID === form.value.leaderId)
|
||||
)
|
||||
const getUsers = async () => {
|
||||
const res = await getUserList({ page: 1, pageSize: 1000 })
|
||||
if (res.code === 0) {
|
||||
userOptions.value = res.data.list || []
|
||||
}
|
||||
}
|
||||
getUsers()
|
||||
|
||||
const getTableData = async () => {
|
||||
const res = await getDepartmentList()
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data || []
|
||||
// 默认选中第一个根部门,保持右侧成员面板有内容
|
||||
if (!selectedDept.value.ID && tableData.value.length) {
|
||||
selectedDept.value = tableData.value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
getTableData()
|
||||
|
||||
const onNodeClick = (data) => {
|
||||
selectedDept.value = data
|
||||
}
|
||||
|
||||
// 组装级联选择器选项(编辑时禁用自身及子孙,防止形成环)
|
||||
const buildOptions = (source, disabled) => {
|
||||
const result = []
|
||||
source &&
|
||||
source.forEach((item) => {
|
||||
const selfDisabled = disabled || item.ID === form.value.ID
|
||||
const option = {
|
||||
ID: item.ID,
|
||||
name: item.name,
|
||||
disabled: selfDisabled
|
||||
}
|
||||
if (item.children && item.children.length) {
|
||||
option.children = buildOptions(item.children, selfDisabled)
|
||||
}
|
||||
result.push(option)
|
||||
})
|
||||
return result
|
||||
}
|
||||
const setOptions = () => {
|
||||
deptOptions.value = [
|
||||
{ ID: 0, name: '顶级部门', disabled: false },
|
||||
...buildOptions(tableData.value, false)
|
||||
]
|
||||
}
|
||||
|
||||
const formVisible = ref(false)
|
||||
const titleForm = ref('新增部门')
|
||||
const dialogType = ref('add')
|
||||
const deptForm = ref(null)
|
||||
const defaultForm = () => ({
|
||||
ID: 0,
|
||||
name: '',
|
||||
parentId: 0,
|
||||
sort: 0,
|
||||
leaderId: undefined,
|
||||
status: true
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
|
||||
parentId: [{ required: true, message: '请选择上级部门', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const initForm = () => {
|
||||
deptForm.value && deptForm.value.resetFields()
|
||||
form.value = defaultForm()
|
||||
}
|
||||
const closeForm = () => {
|
||||
initForm()
|
||||
formVisible.value = false
|
||||
}
|
||||
|
||||
const addDepartment = (parentId) => {
|
||||
initForm()
|
||||
titleForm.value = '新增部门'
|
||||
dialogType.value = 'add'
|
||||
form.value.parentId = parentId
|
||||
setOptions()
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const editDepartment = (row) => {
|
||||
titleForm.value = '编辑部门'
|
||||
dialogType.value = 'edit'
|
||||
form.value = {
|
||||
ID: row.ID,
|
||||
name: row.name,
|
||||
parentId: row.parentId,
|
||||
sort: row.sort,
|
||||
leaderId: row.leaderId || undefined,
|
||||
status: !!row.status
|
||||
}
|
||||
setOptions()
|
||||
deptForm.value && deptForm.value.clearValidate()
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
deptForm.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const api = dialogType.value === 'add' ? createDepartment : updateDepartment
|
||||
const res = await api(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: dialogType.value === 'add' ? '添加成功!' : '编辑成功!'
|
||||
})
|
||||
getTableData()
|
||||
closeForm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除该部门, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteDepartment({ id: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '删除成功!' })
|
||||
if (selectedDept.value.ID === row.ID) selectedDept.value = {}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({ type: 'info', message: '已取消删除' })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,824 @@
|
||||
<template>
|
||||
<div
|
||||
ref="dictRoot"
|
||||
class="flex flex-col overflow-hidden"
|
||||
:style="{ height: rootHeight }"
|
||||
>
|
||||
<warning-bar
|
||||
class="flex-none"
|
||||
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
|
||||
/>
|
||||
<el-splitter class="flex-1 min-h-0 kra-dict-splitter">
|
||||
<el-splitter-panel size="300px" min="200px" max="800px" collapsible>
|
||||
<div class="kra-table-box !my-0 mr-2 h-full flex flex-col overflow-hidden">
|
||||
<div class="flex justify-between items-center relative flex-none">
|
||||
<span class="text font-bold">字典列表</span>
|
||||
<el-input
|
||||
class="!absolute top-0 left-0 z-2 ease-in-out animate-slide-left"
|
||||
placeholder="搜索"
|
||||
v-if="showSearchInput"
|
||||
v-model="searchName"
|
||||
clearable
|
||||
:autofocus="showSearchInput"
|
||||
@clear="clearSearchInput"
|
||||
:prefix-icon="Search"
|
||||
v-click-outside="handleCloseSearchInput"
|
||||
@keydown="handleInputKeyDown"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="getTableData"
|
||||
>搜索</el-button
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<el-tooltip content="搜索" placement="top">
|
||||
<button
|
||||
class="dict-tool-btn"
|
||||
@click="showSearchInputHandler"
|
||||
>
|
||||
<el-icon><Search /></el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="导入字典" placement="top">
|
||||
<button class="dict-tool-btn" @click="openImportDialog">
|
||||
<el-icon><Upload /></el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新建字典" placement="top">
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openDrawer"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="mt-4 flex-1">
|
||||
<div
|
||||
v-for="dictionary in dictionaryData"
|
||||
:key="dictionary.ID"
|
||||
class="rounded flex justify-between items-center px-2 py-4 cursor-pointer mt-2 hover:bg-blue-50 dark:hover:bg-blue-900 bg-gray-50 dark:bg-gray-800 gap-4"
|
||||
:class="[
|
||||
selectID === dictionary.ID
|
||||
? 'text-active'
|
||||
: 'text-slate-700 dark:text-slate-50',
|
||||
dictionary.parentID ? 'ml-4 border-l-2 border-blue-200' : ''
|
||||
]"
|
||||
@click="toDetail(dictionary)"
|
||||
>
|
||||
<div class="max-w-[160px] truncate">
|
||||
<span
|
||||
v-if="dictionary.parentID"
|
||||
class="text-xs text-gray-400 mr-1"
|
||||
>└─</span
|
||||
>
|
||||
{{ dictionary.name }}
|
||||
<span class="mr-auto text-sm">({{ dictionary.type }})</span>
|
||||
</div>
|
||||
|
||||
<div class="min-w-[60px] flex items-center gap-2">
|
||||
<el-icon
|
||||
class="!text-green-500"
|
||||
@click.stop="exportDictionary(dictionary)"
|
||||
title="导出字典"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="!text-blue-500"
|
||||
@click.stop="updateSysDictionaryFunc(dictionary)"
|
||||
>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="!text-red-500"
|
||||
@click="deleteSysDictionaryFunc(dictionary)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
<el-splitter-panel :min="200">
|
||||
<div class="ml-2 h-full overflow-hidden">
|
||||
<sysDictionaryDetail :sys-dictionary-i-d="selectID" />
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:before-close="closeDrawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{
|
||||
type === 'create' ? '添加字典' : '修改字典'
|
||||
}}</span>
|
||||
<div>
|
||||
<el-button @click="closeDrawer"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDrawer"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form
|
||||
ref="drawerForm"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="110px"
|
||||
>
|
||||
<el-form-item label="父级字典" prop="parentID">
|
||||
<el-select
|
||||
v-model="formData.parentID"
|
||||
placeholder="请选择父级字典(可选)"
|
||||
clearable
|
||||
filterable
|
||||
:style="{ width: '100%' }"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in availableParentDictionaries"
|
||||
:key="dict.ID"
|
||||
:label="`${dict.name}(${dict.type})`"
|
||||
:value="dict.ID"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名(中)" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入字典名(中)"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名(英)" prop="type">
|
||||
<el-input
|
||||
v-model="formData.type"
|
||||
placeholder="请输入字典名(英)"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status" required>
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
active-text="开启"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
placeholder="请输入描述"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 导入字典抽屉 -->
|
||||
<el-drawer
|
||||
v-model="importDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:before-close="closeImportDrawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">导入字典JSON</span>
|
||||
<div>
|
||||
<el-button @click="closeImportDrawer"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="handleImport" :loading="importing">
|
||||
确认导入
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="import-drawer-content">
|
||||
<div class="mb-4">
|
||||
<el-alert
|
||||
title="请粘贴、编辑或拖拽JSON文件到下方区域"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽上传区域 -->
|
||||
<div
|
||||
class="drag-upload-area"
|
||||
:class="{ 'is-dragging': isDragging }"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<el-icon class="upload-icon"><Upload /></el-icon>
|
||||
<div class="upload-text">
|
||||
<p>将 JSON 文件拖到此处,或点击选择文件</p>
|
||||
<p class="upload-hint">也可以在下方文本框直接编辑</p>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="json-editor-container mt-4">
|
||||
<el-input
|
||||
v-model="importJsonText"
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
placeholder='请输入JSON数据,例如:
|
||||
{
|
||||
"name": "性别",
|
||||
"type": "gender",
|
||||
"status": true,
|
||||
"desc": "性别字典",
|
||||
"sysDictionaryDetails": [
|
||||
{
|
||||
"label": "男",
|
||||
"value": "1",
|
||||
"status": true,
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"label": "女",
|
||||
"value": "2",
|
||||
"status": true,
|
||||
"sort": 2
|
||||
}
|
||||
]
|
||||
}'
|
||||
class="json-textarea"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" v-if="jsonPreviewError">
|
||||
<el-alert
|
||||
:title="jsonPreviewError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysDictionary,
|
||||
deleteSysDictionary,
|
||||
updateSysDictionary,
|
||||
findSysDictionary,
|
||||
getSysDictionaryList,
|
||||
exportSysDictionary,
|
||||
importSysDictionary
|
||||
} from '@/api/sysDictionary' // 此处请自行替换地址
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import sysDictionaryDetail from './sysDictionaryDetail.vue'
|
||||
import { Edit, Plus, Search, Download, Upload } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'SysDictionary'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 让整页填满视口高度:动态测量根节点距视口顶部的距离,填满到视口底部,
|
||||
// 避免写死 magic number(顶栏/标签栏/告警条高度变化时仍自适应)。
|
||||
const dictRoot = ref(null)
|
||||
const rootHeight = ref('auto')
|
||||
const updateRootHeight = () => {
|
||||
const el = dictRoot.value
|
||||
if (!el) return
|
||||
const top = el.getBoundingClientRect().top
|
||||
rootHeight.value = `${Math.max(320, window.innerHeight - top - 32)}px`
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(updateRootHeight)
|
||||
window.addEventListener('resize', updateRootHeight)
|
||||
})
|
||||
onActivated(updateRootHeight)
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
|
||||
|
||||
const selectID = ref(0)
|
||||
|
||||
const formData = ref({
|
||||
name: null,
|
||||
type: null,
|
||||
status: true,
|
||||
desc: null,
|
||||
parentID: null
|
||||
})
|
||||
const searchName = ref('')
|
||||
const showSearchInput = ref(false)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入字典名(中)',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
type: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入字典名(英)',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
desc: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入描述',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const dictionaryData = ref([])
|
||||
const availableParentDictionaries = ref([])
|
||||
|
||||
// 导入相关
|
||||
const importDrawerVisible = ref(false)
|
||||
const importJsonText = ref('')
|
||||
const importing = ref(false)
|
||||
const jsonPreviewError = ref('')
|
||||
const jsonPreview = ref(null)
|
||||
const isDragging = ref(false)
|
||||
const fileInputRef = ref(null)
|
||||
|
||||
// 监听JSON文本变化,实时预览
|
||||
watch(importJsonText, (newVal) => {
|
||||
if (!newVal.trim()) {
|
||||
jsonPreview.value = null
|
||||
jsonPreviewError.value = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
jsonPreview.value = JSON.parse(newVal)
|
||||
jsonPreviewError.value = ''
|
||||
} catch (e) {
|
||||
jsonPreviewError.value = 'JSON格式错误: ' + e.message
|
||||
jsonPreview.value = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const res = await getSysDictionaryList({
|
||||
name: searchName.value.trim()
|
||||
})
|
||||
if (res.code === 0) {
|
||||
dictionaryData.value = res.data
|
||||
selectID.value = res.data[0].ID
|
||||
// 更新可选父级字典列表
|
||||
updateAvailableParentDictionaries()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新可选父级字典列表
|
||||
const updateAvailableParentDictionaries = () => {
|
||||
// 如果是编辑模式,排除当前字典及其子字典
|
||||
if (type.value === 'update' && formData.value.ID) {
|
||||
availableParentDictionaries.value = dictionaryData.value.filter(
|
||||
(dict) => {
|
||||
return (
|
||||
dict.ID !== formData.value.ID &&
|
||||
!isChildDictionary(dict.ID, formData.value.ID)
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 创建模式,显示所有字典
|
||||
availableParentDictionaries.value = [...dictionaryData.value]
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为子字典(防止循环引用)
|
||||
const isChildDictionary = (dictId, parentId) => {
|
||||
const dict = dictionaryData.value.find((d) => d.ID === dictId)
|
||||
if (!dict || !dict.parentID) return false
|
||||
if (dict.parentID === parentId) return true
|
||||
return isChildDictionary(dict.parentID, parentId)
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
const toDetail = (row) => {
|
||||
selectID.value = row.ID
|
||||
}
|
||||
|
||||
const drawerFormVisible = ref(false)
|
||||
const type = ref('')
|
||||
const updateSysDictionaryFunc = async (row) => {
|
||||
const res = await findSysDictionary({ ID: row.ID, status: row.status })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data.resysDictionary
|
||||
drawerFormVisible.value = true
|
||||
// 更新可选父级字典列表
|
||||
updateAvailableParentDictionaries()
|
||||
}
|
||||
}
|
||||
const closeDrawer = () => {
|
||||
drawerFormVisible.value = false
|
||||
formData.value = {
|
||||
name: null,
|
||||
type: null,
|
||||
status: true,
|
||||
desc: null,
|
||||
parentID: null
|
||||
}
|
||||
}
|
||||
const deleteSysDictionaryFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteSysDictionary({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const drawerForm = ref(null)
|
||||
const enterDrawer = async () => {
|
||||
drawerForm.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createSysDictionary(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateSysDictionary(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createSysDictionary(formData.value)
|
||||
break
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('操作成功')
|
||||
closeDrawer()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const openDrawer = () => {
|
||||
type.value = 'create'
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
drawerFormVisible.value = true
|
||||
// 更新可选父级字典列表
|
||||
updateAvailableParentDictionaries()
|
||||
}
|
||||
|
||||
const clearSearchInput = () => {
|
||||
if (!showSearchInput.value) return
|
||||
searchName.value = ''
|
||||
showSearchInput.value = false
|
||||
getTableData()
|
||||
}
|
||||
const handleCloseSearchInput = () => {
|
||||
if (!showSearchInput.value || searchName.value.trim() != '') return
|
||||
showSearchInput.value = false
|
||||
}
|
||||
|
||||
const showSearchInputHandler = () => {
|
||||
showSearchInput.value = true
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && searchName.value.trim() !== '') {
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 导出字典
|
||||
const exportDictionary = async (row) => {
|
||||
try {
|
||||
const res = await exportSysDictionary({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
// 将JSON数据转换为字符串并下载
|
||||
const jsonStr = JSON.stringify(res.data, null, 2)
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${row.type}_${row.name}_dictionary.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('导出失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开导入抽屉
|
||||
const openImportDialog = () => {
|
||||
importDrawerVisible.value = true
|
||||
importJsonText.value = ''
|
||||
jsonPreview.value = null
|
||||
jsonPreviewError.value = ''
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 关闭导入抽屉
|
||||
const closeImportDrawer = () => {
|
||||
importDrawerVisible.value = false
|
||||
importJsonText.value = ''
|
||||
jsonPreview.value = null
|
||||
jsonPreviewError.value = ''
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 处理拖拽进入
|
||||
const handleDragOver = () => {
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
// 处理拖拽离开
|
||||
const handleDragLeave = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
// 处理文件拖拽
|
||||
const handleDrop = (e) => {
|
||||
isDragging.value = false
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
readJsonFile(file)
|
||||
}
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (e) => {
|
||||
const files = e.target.files
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
readJsonFile(file)
|
||||
|
||||
// 清空input,以便可以重复选择同一文件
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
// 读取JSON文件
|
||||
const readJsonFile = (file) => {
|
||||
// 检查文件类型
|
||||
if (!file.name.endsWith('.json')) {
|
||||
ElMessage.warning('请上传 JSON 文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const content = event.target.result
|
||||
// 验证是否为有效的 JSON
|
||||
JSON.parse(content)
|
||||
importJsonText.value = content
|
||||
ElMessage.success('文件读取成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('文件内容不是有效的 JSON 格式')
|
||||
}
|
||||
}
|
||||
reader.onerror = () => {
|
||||
ElMessage.error('文件读取失败')
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 处理导入
|
||||
const handleImport = async () => {
|
||||
if (!importJsonText.value.trim()) {
|
||||
ElMessage.warning('请输入JSON数据')
|
||||
return
|
||||
}
|
||||
|
||||
if (jsonPreviewError.value) {
|
||||
ElMessage.error('JSON格式错误,请检查后重试')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
importing.value = true
|
||||
const res = await importSysDictionary({ json: importJsonText.value })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('导入成功')
|
||||
closeImportDrawer()
|
||||
getTableData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('导入失败: ' + error.message)
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 拖拽分隔条:去掉默认整条实线,改成居中一颗浅色小握把;
|
||||
hover 变主色并变长,保留拖拽能力,观感更克制。 */
|
||||
:deep(.el-splitter-bar__dragger) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
:deep(.el-splitter-bar) {
|
||||
position: relative;
|
||||
}
|
||||
:deep(.el-splitter-bar)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 4px;
|
||||
height: 40px;
|
||||
border-radius: 999px;
|
||||
background-color: var(--el-border-color);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
height 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
:deep(.el-splitter-bar:hover)::after {
|
||||
background-color: var(--el-color-primary);
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.dict-box {
|
||||
height: calc(100vh - 240px);
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 字典列表标题栏的工具按钮:32px 淡色块图标按钮(无边框),
|
||||
色块托底保证可见性又不显杂乱,hover 变主色淡底;
|
||||
主操作「新建」保留实心主色突出层级 */
|
||||
.dict-tool-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color);
|
||||
color: rgb(var(--muted-foreground-color));
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.dict-tool-btn:hover {
|
||||
background: rgb(var(--primary-color) / 0.12);
|
||||
color: rgb(var(--primary-color));
|
||||
}
|
||||
|
||||
.import-drawer-content {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* 拖拽上传区域 */
|
||||
.drag-upload-area {
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drag-upload-area:hover {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.drag-upload-area.is-dragging {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.drag-upload-area.is-dragging .upload-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.upload-text p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.json-editor-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.json-textarea :deep(.el-textarea__inner) {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.json-preview pre {
|
||||
margin: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.dark .drag-upload-area {
|
||||
background-color: #1d1e1f;
|
||||
border-color: #414243;
|
||||
}
|
||||
|
||||
.dark .drag-upload-area:hover,
|
||||
.dark .drag-upload-area.is-dragging {
|
||||
background-color: #1a3a52;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.dark .json-preview {
|
||||
background-color: #1d1e1f;
|
||||
border-color: #414243;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div class="kra-table-box !my-0 h-full flex flex-col overflow-hidden">
|
||||
<div class="kra-btn-list justify-between flex items-center flex-none">
|
||||
<span class="text font-bold">字典详细内容</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input
|
||||
placeholder="搜索展示值"
|
||||
v-model="searchName"
|
||||
clearable
|
||||
class="!w-64"
|
||||
@clear="clearSearchInput"
|
||||
:prefix-icon="Search"
|
||||
v-click-outside="handleCloseSearchInput"
|
||||
@keydown="handleInputKeyDown"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
:type="searchName ? 'primary' : 'info'"
|
||||
@click="applySearch"
|
||||
>搜索</el-button
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" icon="plus" @click="openDrawer">
|
||||
新增字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格视图 -->
|
||||
<div class="flex-1 min-h-0">
|
||||
<el-table
|
||||
height="100%"
|
||||
:data="displayTreeData"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:tree-props="{ children: 'children'}"
|
||||
row-key="ID"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="展示值" prop="label" min-width="100"/>
|
||||
|
||||
<el-table-column align="left" label="字典值" prop="value" />
|
||||
|
||||
<el-table-column align="left" label="扩展值" prop="extend" />
|
||||
|
||||
<el-table-column align="left" label="层级" prop="level" width="80" />
|
||||
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="启用状态"
|
||||
prop="status"
|
||||
width="100"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ formatBoolean(scope.row.status) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="排序标记"
|
||||
prop="sort"
|
||||
width="100"
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="操作"
|
||||
:min-width="appStore.operateMinWith"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="plus"
|
||||
@click="addChildNode(scope.row)"
|
||||
>
|
||||
添加子项
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="updateSysDictionaryDetailFunc(scope.row)"
|
||||
>
|
||||
变更
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteSysDictionaryDetailFunc(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:before-close="closeDrawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{
|
||||
type === 'create' ? '添加字典项' : '修改字典项'
|
||||
}}</span>
|
||||
<div>
|
||||
<el-button @click="closeDrawer"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDrawer"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form
|
||||
ref="drawerForm"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="110px"
|
||||
>
|
||||
<el-form-item label="父级字典项" prop="parentID">
|
||||
<el-cascader
|
||||
v-model="formData.parentID"
|
||||
:options="[rootOption,...treeData]"
|
||||
:props="cascadeProps"
|
||||
placeholder="请选择父级字典项(可选)"
|
||||
clearable
|
||||
filterable
|
||||
:style="{ width: '100%' }"
|
||||
@change="handleParentChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="展示值" prop="label">
|
||||
<el-input
|
||||
v-model="formData.label"
|
||||
placeholder="请输入展示值"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="value">
|
||||
<el-input
|
||||
v-model="formData.value"
|
||||
placeholder="请输入字典值"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="扩展值" prop="extend">
|
||||
<el-input
|
||||
v-model="formData.extend"
|
||||
placeholder="请输入扩展值"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态" prop="status" required>
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
active-text="开启"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序标记" prop="sort">
|
||||
<el-input-number
|
||||
v-model.number="formData.sort"
|
||||
placeholder="排序标记"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysDictionaryDetail,
|
||||
deleteSysDictionaryDetail,
|
||||
updateSysDictionaryDetail,
|
||||
findSysDictionaryDetail,
|
||||
getDictionaryTreeList
|
||||
} from '@/api/sysDictionaryDetail' // 此处请自行替换地址
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatBoolean } from '@/utils/format'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'SysDictionaryDetail'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
const searchName = ref('')
|
||||
|
||||
const props = defineProps({
|
||||
sysDictionaryID: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const formData = ref({
|
||||
label: null,
|
||||
value: null,
|
||||
status: true,
|
||||
sort: null,
|
||||
parentID: null
|
||||
})
|
||||
|
||||
const rules = ref({
|
||||
label: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入展示值',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入字典值',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
sort: [
|
||||
{
|
||||
required: true,
|
||||
message: '排序标记',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const treeData = ref([])
|
||||
const displayTreeData = ref([])
|
||||
|
||||
// 级联选择器配置
|
||||
const cascadeProps = {
|
||||
value: 'ID',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
checkStrictly: true, // 允许选择任意级别
|
||||
emitPath: false // 只返回选中节点的值
|
||||
}
|
||||
|
||||
|
||||
const normalizeSearch = (value) => (value ?? '').toString().toLowerCase()
|
||||
|
||||
const filterTree = (nodes, keyword) => {
|
||||
const trimmed = normalizeSearch(keyword).trim()
|
||||
if (!trimmed) {
|
||||
return nodes
|
||||
}
|
||||
const walk = (list) => {
|
||||
const result = []
|
||||
for (const node of list) {
|
||||
const label = normalizeSearch(node.label)
|
||||
const children = Array.isArray(node.children) ? walk(node.children) : []
|
||||
if (label.includes(trimmed) || children.length > 0) {
|
||||
result.push({
|
||||
...node,
|
||||
children
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return walk(nodes)
|
||||
}
|
||||
|
||||
const applySearch = () => {
|
||||
displayTreeData.value = filterTree(treeData.value, searchName.value)
|
||||
}
|
||||
|
||||
// 获取树形数据
|
||||
const getTreeData = async () => {
|
||||
if (!props.sysDictionaryID) return
|
||||
try {
|
||||
const res = await getDictionaryTreeList({
|
||||
sysDictionaryID: props.sysDictionaryID
|
||||
})
|
||||
if (res.code === 0) {
|
||||
treeData.value = res.data.list || []
|
||||
applySearch()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取树形数据失败:', error)
|
||||
ElMessage.error('获取层级数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const rootOption = {
|
||||
ID: null,
|
||||
label: '无父级(根级)'
|
||||
}
|
||||
|
||||
|
||||
// 初始加载
|
||||
getTreeData()
|
||||
|
||||
const type = ref('')
|
||||
const drawerFormVisible = ref(false)
|
||||
|
||||
const updateSysDictionaryDetailFunc = async (row) => {
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
const res = await findSysDictionaryDetail({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data.reSysDictionaryDetail
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 添加子节点
|
||||
const addChildNode = (parentNode) => {
|
||||
console.log(parentNode)
|
||||
type.value = 'create'
|
||||
formData.value = {
|
||||
label: null,
|
||||
value: null,
|
||||
status: true,
|
||||
sort: null,
|
||||
parentID: parentNode.ID,
|
||||
sysDictionaryID: props.sysDictionaryID
|
||||
}
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
|
||||
// 处理父级选择变化
|
||||
const handleParentChange = (value) => {
|
||||
formData.value.parentID = value
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
drawerFormVisible.value = false
|
||||
formData.value = {
|
||||
label: null,
|
||||
value: null,
|
||||
status: true,
|
||||
sort: null,
|
||||
parentID: null,
|
||||
sysDictionaryID: props.sysDictionaryID
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSysDictionaryDetailFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteSysDictionaryDetail({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
await getTreeData() // 重新加载数据
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const drawerForm = ref(null)
|
||||
const enterDrawer = async () => {
|
||||
drawerForm.value.validate(async (valid) => {
|
||||
formData.value.sysDictionaryID = props.sysDictionaryID
|
||||
if (!valid) return
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createSysDictionaryDetail(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateSysDictionaryDetail(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createSysDictionaryDetail(formData.value)
|
||||
break
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
closeDrawer()
|
||||
await getTreeData() // 重新加载数据
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openDrawer = () => {
|
||||
type.value = 'create'
|
||||
formData.value.parentID = null
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
|
||||
const clearSearchInput = () => {
|
||||
searchName.value = ''
|
||||
applySearch()
|
||||
}
|
||||
|
||||
const handleCloseSearchInput = () => {
|
||||
// 处理搜索输入框关闭
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
applySearch()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.sysDictionaryID,
|
||||
() => {
|
||||
getTreeData()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition mode="out-in" name="el-fade-in-linear">
|
||||
<keep-alive :include="routerStore.keepAliveRouters">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
defineOptions({
|
||||
name: 'SuperAdmin'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="flex justify-between items-center gap-2 w-full">
|
||||
<el-cascader
|
||||
v-if="pathIsSelect"
|
||||
placeholder="请选择文件路径"
|
||||
:options="pathOptions"
|
||||
v-model="activeComponent"
|
||||
filterable
|
||||
class="!w-full"
|
||||
clearable
|
||||
@change="emitChange"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="tempPath"
|
||||
placeholder="页面:view/xxx/xx.vue 模块:modules/xx/xx.vue"
|
||||
@change="emitChange"
|
||||
/>
|
||||
<el-button @click="togglePathIsSelect"
|
||||
>{{ pathIsSelect ? '手动输入' : '快捷选择' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import pathInfo from '@/pathInfo.json'
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['change'])
|
||||
|
||||
const pathOptions = ref([])
|
||||
const tempPath = ref('')
|
||||
const activeComponent = ref([])
|
||||
const pathIsSelect = ref(true)
|
||||
|
||||
const togglePathIsSelect = () => {
|
||||
if (pathIsSelect.value) {
|
||||
tempPath.value = activeComponent.value?.join('/') || ''
|
||||
} else {
|
||||
activeComponent.value = tempPath.value?.split('/') || []
|
||||
}
|
||||
|
||||
pathIsSelect.value = !pathIsSelect.value
|
||||
emitChange()
|
||||
}
|
||||
|
||||
function convertToCascaderOptions(data) {
|
||||
const result = []
|
||||
|
||||
for (const path in data) {
|
||||
const label = data[path]
|
||||
const parts = path.split('/').filter(Boolean)
|
||||
|
||||
// 如果第一个部分是 'src',则从第二个部分开始处理
|
||||
const startIndex = parts[0] === 'src' ? 1 : 0
|
||||
|
||||
let currentLevel = result
|
||||
|
||||
for (let i = startIndex; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
let node = currentLevel.find((item) => item.value === part)
|
||||
|
||||
if (!node) {
|
||||
node = {
|
||||
value: part,
|
||||
label: part,
|
||||
children: []
|
||||
}
|
||||
currentLevel.push(node)
|
||||
}
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
// 如果是路径的最后一部分,设置标签并移除 children
|
||||
node.label = label
|
||||
delete node.children
|
||||
}
|
||||
|
||||
currentLevel = node.children || []
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.component,
|
||||
(value) => {
|
||||
initCascader(value)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
pathOptions.value = convertToCascaderOptions(pathInfo)
|
||||
initCascader(props.component)
|
||||
})
|
||||
|
||||
const initCascader = (value) => {
|
||||
// 新增的时候
|
||||
if (value === '') {
|
||||
pathIsSelect.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 编辑的时候,根据路径判断是选择框还是输入框
|
||||
if (pathInfo[`/src/${value}`]) {
|
||||
activeComponent.value = value.split('/').filter(Boolean)
|
||||
tempPath.value = ''
|
||||
pathIsSelect.value = true
|
||||
return
|
||||
}
|
||||
tempPath.value = value
|
||||
activeComponent.value = []
|
||||
pathIsSelect.value = false
|
||||
}
|
||||
|
||||
const emitChange = () => {
|
||||
emits(
|
||||
'change',
|
||||
pathIsSelect.value ? activeComponent.value?.join('/') : tempPath.value
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,932 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="addMenu(0)">
|
||||
新增根菜单
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 由于此处菜单跟左侧列表一一对应所以不需要分页 pageSize默认999 -->
|
||||
<el-table :data="tableData" row-key="ID">
|
||||
<el-table-column align="left" label="ID" min-width="100" prop="ID" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="展示名称"
|
||||
min-width="120"
|
||||
prop="authorityName"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.meta.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="图标"
|
||||
min-width="140"
|
||||
prop="authorityName"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.meta.icon" class="icon-column">
|
||||
<el-icon>
|
||||
<component :is="scope.row.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ scope.row.meta.icon }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="路由Name"
|
||||
show-overflow-tooltip
|
||||
min-width="160"
|
||||
prop="name"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="路由Path"
|
||||
show-overflow-tooltip
|
||||
min-width="160"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="是否隐藏"
|
||||
min-width="100"
|
||||
prop="hidden"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.hidden ? '隐藏' : '显示' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="父节点"
|
||||
min-width="90"
|
||||
prop="parentId"
|
||||
/>
|
||||
<el-table-column align="left" label="排序" min-width="70" prop="sort" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="文件路径"
|
||||
min-width="360"
|
||||
prop="component"
|
||||
/>
|
||||
<el-table-column align="left" fixed="right" label="操作" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="plus"
|
||||
@click="addMenu(scope.row.ID)"
|
||||
>
|
||||
添加子菜单
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="editMenu(scope.row.ID)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="user"
|
||||
@click="openAssignRoleDrawer(scope.row)"
|
||||
>
|
||||
分配角色
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteMenu(scope.row.ID)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-drawer
|
||||
v-model="dialogFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:before-close="handleClose"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{ dialogTitle }}</span>
|
||||
<div>
|
||||
<el-button @click="closeDialog"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDialog"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<warning-bar title="新增菜单,需要在角色管理内配置权限才可使用" />
|
||||
|
||||
<!-- 基础信息区域 -->
|
||||
<div class="border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-700 mb-4">基础信息</h3>
|
||||
<el-form
|
||||
v-if="dialogFormVisible"
|
||||
ref="menuForm"
|
||||
:inline="true"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-row class="w-full" :gutter="16">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="文件路径" prop="component">
|
||||
<components-cascader
|
||||
:component="form.component"
|
||||
@change="fmtComponent"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
<span>如果菜单包含子菜单,请创建router-view二级路由页面或者</span>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="form.component = 'view/routerHolder.vue'"
|
||||
>
|
||||
点我设置
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full" :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="展示名称" prop="meta.title">
|
||||
<el-input
|
||||
v-model="form.meta.title"
|
||||
autocomplete="off"
|
||||
placeholder="请输入菜单展示名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="路由Name" prop="path">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
autocomplete="off"
|
||||
placeholder="唯一英文字符串"
|
||||
@change="changeName"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 路由配置区域 -->
|
||||
<div class="border-b border-gray-200 mt-2">
|
||||
<h3 class="font-semibold text-gray-700 mb-4">路由配置</h3>
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-row class="w-full" :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="父节点ID">
|
||||
<el-cascader
|
||||
v-model="form.parentId"
|
||||
style="width: 100%"
|
||||
:disabled="!isEdit"
|
||||
:options="menuOption"
|
||||
:props="{
|
||||
checkStrictly: true,
|
||||
label: 'title',
|
||||
value: 'ID',
|
||||
disabled: 'disabled',
|
||||
emitPath: false
|
||||
}"
|
||||
:show-all-levels="false"
|
||||
filterable
|
||||
placeholder="请选择父节点"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="path">
|
||||
<template #label>
|
||||
<div class="inline-flex items-center h-4">
|
||||
<span>路由Path</span>
|
||||
<el-checkbox
|
||||
class="ml-2"
|
||||
v-model="checkFlag"
|
||||
>添加参数</el-checkbox
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.path"
|
||||
:disabled="!checkFlag"
|
||||
autocomplete="off"
|
||||
placeholder="建议只在后方拼接参数"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 显示设置区域 -->
|
||||
<div class="border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-700 mb-4 mt-2">显示设置</h3>
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-row class="w-full" :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="图标" prop="meta.icon">
|
||||
<icon v-model="form.meta.icon" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排序标记" prop="sort">
|
||||
<el-input
|
||||
v-model.number="form.sort"
|
||||
autocomplete="off"
|
||||
placeholder="请输入排序数字"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否隐藏">
|
||||
<el-select
|
||||
v-model="form.hidden"
|
||||
style="width: 100%"
|
||||
placeholder="是否在列表隐藏"
|
||||
>
|
||||
<el-option :value="false" label="否" />
|
||||
<el-option :value="true" label="是" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 高级配置区域 -->
|
||||
<div class="border-b border-gray-200 mt-2">
|
||||
<h3 class="font-semibold text-gray-700 mb-4">高级配置</h3>
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-row class="w-full" gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="meta.activeName">
|
||||
<template #label>
|
||||
<div class="label-with-tooltip">
|
||||
<span>高亮菜单</span>
|
||||
<el-tooltip
|
||||
content="注:当到达此路由时候,指定左侧菜单指定name会处于活跃状态(亮起),可为空,为空则为本路由Name。"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.meta.activeName"
|
||||
:placeholder="form.name || '请输入高亮菜单名称'"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="KeepAlive" prop="meta.keepAlive">
|
||||
<el-select
|
||||
v-model="form.meta.keepAlive"
|
||||
style="width: 100%"
|
||||
placeholder="是否keepAlive缓存页面"
|
||||
>
|
||||
<el-option :value="false" label="否" />
|
||||
<el-option :value="true" label="是" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full" :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="CloseTab" prop="meta.closeTab">
|
||||
<el-select
|
||||
v-model="form.meta.closeTab"
|
||||
style="width: 100%"
|
||||
placeholder="是否自动关闭tab"
|
||||
>
|
||||
<el-option :value="false" label="否" />
|
||||
<el-option :value="true" label="是" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-with-tooltip">
|
||||
<span>是否为基础页面</span>
|
||||
<el-tooltip
|
||||
content="此项选择为是,则不会展示左侧菜单以及顶部信息。"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-select
|
||||
v-model="form.meta.defaultMenu"
|
||||
style="width: 100%"
|
||||
placeholder="是否为基础页面"
|
||||
>
|
||||
<el-option :value="false" label="否" />
|
||||
<el-option :value="true" label="是" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-with-tooltip">
|
||||
<span>路由切换动画</span>
|
||||
<el-tooltip
|
||||
content="如果设置了路由切换动画,在本路由下的动画优先级高于全局动画切换优先级。"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-select
|
||||
v-model="form.meta.transitionType"
|
||||
style="width: 100%"
|
||||
placeholder="跟随全局"
|
||||
clearable
|
||||
>
|
||||
<el-option value="fade" label="淡入淡出" />
|
||||
<el-option value="slide" label="滑动" />
|
||||
<el-option value="zoom" label="缩放" />
|
||||
<el-option value="none" label="无动画" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 菜单参数配置区域 -->
|
||||
<div class="mt-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-gray-700">菜单参数配置</h3>
|
||||
<el-button type="primary" size="small" @click="addParameter(form)">
|
||||
新增菜单参数
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="form.parameters"
|
||||
style="width: 100%"
|
||||
class="parameter-table"
|
||||
>
|
||||
<el-table-column
|
||||
align="center"
|
||||
prop="type"
|
||||
label="参数类型"
|
||||
width="150"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.type"
|
||||
placeholder="请选择"
|
||||
size="small"
|
||||
>
|
||||
<el-option key="query" value="query" label="query" />
|
||||
<el-option key="params" value="params" label="params" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="key" label="参数key" width="150">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.key"
|
||||
size="small"
|
||||
placeholder="请输入参数key"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="value" label="参数值">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.value"
|
||||
size="small"
|
||||
placeholder="请输入参数值"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="操作" width="100">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteParameter(form.parameters, scope.$index)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 可控按钮配置区域 -->
|
||||
<div class="mb-2 mt-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-gray-700">可控按钮配置</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" size="small" @click="addBtn(form)">
|
||||
新增可控按钮
|
||||
</el-button>
|
||||
<el-tooltip
|
||||
content="点击查看按钮权限配置文档"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-icon
|
||||
class="cursor-pointer text-blue-500 hover:text-blue-700"
|
||||
@click="toDoc('https://www.kra.com/guide/web/button-auth.html')"
|
||||
>
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="form.menuBtn"
|
||||
style="width: 100%"
|
||||
class="button-table"
|
||||
>
|
||||
<el-table-column
|
||||
align="center"
|
||||
prop="name"
|
||||
label="按钮名称"
|
||||
width="150"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.name"
|
||||
size="small"
|
||||
placeholder="请输入按钮名称"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="desc" label="备注">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.desc"
|
||||
size="small"
|
||||
placeholder="请输入按钮备注"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="操作" width="100">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteBtn(form.menuBtn, scope.$index)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 分配给角色抽屉 -->
|
||||
<el-drawer
|
||||
v-model="assignRoleDrawerVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">分配角色 - {{ assignMenuRow.meta?.title }}</span>
|
||||
<div>
|
||||
<el-button @click="assignRoleDrawerVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="assignRoleSubmitting" @click="confirmAssignRole">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<warning-bar title="注:保存时将全量覆盖该菜单的角色关联关系;作为角色首页的菜单不可取消勾选" />
|
||||
<el-tree
|
||||
ref="roleTreeRef"
|
||||
v-loading="assignRoleLoading"
|
||||
:data="authorityTreeData"
|
||||
:props="{ label: 'authorityName', children: 'children', disabled: isRoleDisabled }"
|
||||
node-key="authorityId"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
default-expand-all
|
||||
/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
updateBaseMenu,
|
||||
getMenuList,
|
||||
addBaseMenu,
|
||||
deleteBaseMenu,
|
||||
getBaseMenuById,
|
||||
getMenuRoles,
|
||||
setMenuRoles
|
||||
} from '@/api/menu'
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import icon from '@/view/superAdmin/menu/icon.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { canRemoveAuthorityBtnApi } from '@/api/authorityBtn'
|
||||
import { reactive, ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { QuestionFilled, InfoFilled, Delete } from '@element-plus/icons-vue'
|
||||
import { toDoc } from '@/utils/doc'
|
||||
import { toLowerCase } from '@/utils/stringFun'
|
||||
import ComponentsCascader from '@/view/superAdmin/menu/components/components-cascader.vue'
|
||||
|
||||
import pathInfo from '@/pathInfo.json'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'Menus'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const rules = reactive({
|
||||
path: [{ required: true, message: '请输入菜单name', trigger: 'blur' }],
|
||||
component: [{ required: true, message: '请输入文件路径', trigger: 'blur' }],
|
||||
'meta.title': [
|
||||
{ required: true, message: '请输入菜单展示名称', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getMenuList()
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// 新增参数
|
||||
const addParameter = (form) => {
|
||||
if (!form.parameters) {
|
||||
form.parameters = []
|
||||
}
|
||||
form.parameters.push({
|
||||
type: 'query',
|
||||
key: '',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
|
||||
const fmtComponent = (component) => {
|
||||
form.value.component = component.replace(/\\/g, '/')
|
||||
form.value.name = toLowerCase(pathInfo['/src/' + component])
|
||||
form.value.path = form.value.name
|
||||
}
|
||||
|
||||
// 删除参数
|
||||
const deleteParameter = (parameters, index) => {
|
||||
parameters.splice(index, 1)
|
||||
}
|
||||
|
||||
// 新增可控按钮
|
||||
const addBtn = (form) => {
|
||||
if (!form.menuBtn) {
|
||||
form.menuBtn = []
|
||||
}
|
||||
form.menuBtn.push({
|
||||
name: '',
|
||||
desc: ''
|
||||
})
|
||||
}
|
||||
// 删除可控按钮
|
||||
const deleteBtn = async (btns, index) => {
|
||||
const btn = btns[index]
|
||||
if (btn.ID === 0) {
|
||||
btns.splice(index, 1)
|
||||
return
|
||||
}
|
||||
const res = await canRemoveAuthorityBtnApi({ id: btn.ID })
|
||||
if (res.code === 0) {
|
||||
btns.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const form = ref({
|
||||
ID: 0,
|
||||
path: '',
|
||||
name: '',
|
||||
hidden: false,
|
||||
parentId: 0,
|
||||
component: '',
|
||||
meta: {
|
||||
activeName: '',
|
||||
title: '',
|
||||
icon: '',
|
||||
defaultMenu: false,
|
||||
closeTab: false,
|
||||
keepAlive: false
|
||||
},
|
||||
parameters: [],
|
||||
menuBtn: []
|
||||
})
|
||||
const changeName = () => {
|
||||
form.value.path = form.value.name
|
||||
}
|
||||
|
||||
const handleClose = (done) => {
|
||||
initForm()
|
||||
done()
|
||||
}
|
||||
// 删除菜单
|
||||
const deleteMenu = (ID) => {
|
||||
ElMessageBox.confirm(
|
||||
'此操作将永久删除所有角色下该菜单, 是否继续?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
const res = await deleteBaseMenu({ ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
})
|
||||
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
}
|
||||
// 初始化弹窗内表格方法
|
||||
const menuForm = ref(null)
|
||||
const checkFlag = ref(false)
|
||||
const initForm = () => {
|
||||
checkFlag.value = false
|
||||
menuForm.value.resetFields()
|
||||
form.value = {
|
||||
ID: 0,
|
||||
path: '',
|
||||
name: '',
|
||||
hidden: false,
|
||||
parentId: 0,
|
||||
component: '',
|
||||
meta: {
|
||||
title: '',
|
||||
icon: '',
|
||||
defaultMenu: false,
|
||||
closeTab: false,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
}
|
||||
// 关闭弹窗
|
||||
|
||||
const dialogFormVisible = ref(false)
|
||||
const closeDialog = () => {
|
||||
initForm()
|
||||
dialogFormVisible.value = false
|
||||
}
|
||||
// 添加menu
|
||||
const enterDialog = async () => {
|
||||
menuForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
let res
|
||||
if (isEdit.value) {
|
||||
res = await updateBaseMenu(form.value)
|
||||
} else {
|
||||
res = await addBaseMenu(form.value)
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: isEdit.value ? '编辑成功' : '添加成功,请到角色管理页面分配权限'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
initForm()
|
||||
dialogFormVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const menuOption = ref([
|
||||
{
|
||||
ID: '0',
|
||||
title: '根菜单'
|
||||
}
|
||||
])
|
||||
const setOptions = () => {
|
||||
menuOption.value = [
|
||||
{
|
||||
ID: 0,
|
||||
title: '根目录'
|
||||
}
|
||||
]
|
||||
setMenuOptions(tableData.value, menuOption.value, false)
|
||||
}
|
||||
const setMenuOptions = (menuData, optionsData, disabled) => {
|
||||
menuData &&
|
||||
menuData.forEach((item) => {
|
||||
if (item.children && item.children.length) {
|
||||
const option = {
|
||||
title: item.meta.title,
|
||||
ID: item.ID,
|
||||
disabled: disabled || item.ID === form.value.ID,
|
||||
children: []
|
||||
}
|
||||
setMenuOptions(
|
||||
item.children,
|
||||
option.children,
|
||||
disabled || item.ID === form.value.ID
|
||||
)
|
||||
optionsData.push(option)
|
||||
} else {
|
||||
const option = {
|
||||
title: item.meta.title,
|
||||
ID: item.ID,
|
||||
disabled: disabled || item.ID === form.value.ID
|
||||
}
|
||||
optionsData.push(option)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加菜单方法,id为 0则为添加根菜单
|
||||
const isEdit = ref(false)
|
||||
const dialogTitle = ref('新增菜单')
|
||||
const addMenu = (id) => {
|
||||
dialogTitle.value = '新增菜单'
|
||||
form.value.parentId = id
|
||||
isEdit.value = false
|
||||
setOptions()
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
// 修改菜单方法
|
||||
const editMenu = async (id) => {
|
||||
dialogTitle.value = '编辑菜单'
|
||||
const res = await getBaseMenuById({ id })
|
||||
form.value = res.data.menu
|
||||
isEdit.value = true
|
||||
setOptions()
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 分配给角色
|
||||
const assignRoleDrawerVisible = ref(false)
|
||||
const assignMenuRow = ref({})
|
||||
const authorityTreeData = ref([])
|
||||
const assignRoleLoading = ref(false)
|
||||
const assignRoleSubmitting = ref(false)
|
||||
const roleTreeRef = ref(null)
|
||||
const defaultRouterAuthorityIds = ref(new Set())
|
||||
|
||||
const isRoleDisabled = (data) => {
|
||||
return defaultRouterAuthorityIds.value.has(data.authorityId)
|
||||
}
|
||||
|
||||
const openAssignRoleDrawer = async (row) => {
|
||||
assignMenuRow.value = row
|
||||
defaultRouterAuthorityIds.value = new Set()
|
||||
assignRoleDrawerVisible.value = true
|
||||
assignRoleLoading.value = true
|
||||
// 并行加载角色树和当前菜单已分配的角色
|
||||
const [authRes, rolesRes] = await Promise.all([
|
||||
getAuthorityList(),
|
||||
getMenuRoles(row.ID)
|
||||
])
|
||||
if (authRes.code === 0) {
|
||||
authorityTreeData.value = authRes.data
|
||||
}
|
||||
if (rolesRes.code === 0 && rolesRes.data) {
|
||||
if (rolesRes.data.defaultRouterAuthorityIds) {
|
||||
defaultRouterAuthorityIds.value = new Set(rolesRes.data.defaultRouterAuthorityIds)
|
||||
}
|
||||
nextTick(() => {
|
||||
roleTreeRef.value?.setCheckedKeys(rolesRes.data.authorityIds || [])
|
||||
})
|
||||
}
|
||||
assignRoleLoading.value = false
|
||||
}
|
||||
|
||||
const confirmAssignRole = async () => {
|
||||
assignRoleSubmitting.value = true
|
||||
try {
|
||||
const checkedKeys = roleTreeRef.value?.getCheckedKeys(false) || []
|
||||
const halfCheckedKeys = roleTreeRef.value?.getHalfCheckedKeys() || []
|
||||
const authorityIds = [...checkedKeys, ...halfCheckedKeys]
|
||||
const res = await setMenuRoles({
|
||||
menuId: assignMenuRow.value.ID,
|
||||
authorityIds
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '分配成功!' })
|
||||
assignRoleDrawerVisible.value = false
|
||||
}
|
||||
} catch {
|
||||
ElMessage({ type: 'error', message: '分配失败,请重试' })
|
||||
}
|
||||
assignRoleSubmitting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.warning {
|
||||
color: #dc143c;
|
||||
}
|
||||
.icon-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.label-with-tooltip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.el-icon {
|
||||
color: #909399;
|
||||
cursor: help;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-table,
|
||||
.button-table {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
|
||||
:deep(.el-table__header) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.el-table__body) {
|
||||
.el-table__row {
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="kra-search-box">
|
||||
<el-form :inline="true" :model="searchInfo">
|
||||
<el-form-item label="请求方法">
|
||||
<el-input v-model="searchInfo.method" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请求路径">
|
||||
<el-input v-model="searchInfo.path" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结果状态码">
|
||||
<el-input v-model="searchInfo.status" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button
|
||||
icon="delete"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column align="left" type="selection" width="55" />
|
||||
<el-table-column align="left" label="操作人" width="140">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.user.userName }}({{ scope.row.user.nickName }})
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="日期" width="180">
|
||||
<template #default="scope">{{
|
||||
formatDate(scope.row.CreatedAt)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="状态码" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-tag type="success">{{ scope.row.status }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="请求IP" prop="ip" width="120" />
|
||||
<el-table-column align="left" label="请求ID" prop="request_id" width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="text-xs text-gray-500">{{ row.request_id || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="链路ID" prop="trace_id" width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="text-xs text-gray-500">{{ row.trace_id || '无' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="设备ID" prop="device_id" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="text-xs text-gray-500">{{ row.device_id || '无' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求方法"
|
||||
prop="method"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求路径"
|
||||
prop="path"
|
||||
width="240"
|
||||
/>
|
||||
<el-table-column align="left" label="请求" prop="path" width="80">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-popover
|
||||
v-if="scope.row.body"
|
||||
placement="left-start"
|
||||
:width="444"
|
||||
>
|
||||
<div class="popover-box">
|
||||
<pre>{{ fmtBody(scope.row.body) }}</pre>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-icon style="cursor: pointer"><warning /></el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<span v-else>无</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="响应" prop="path" width="80">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-popover
|
||||
v-if="scope.row.resp"
|
||||
placement="left-start"
|
||||
:width="444"
|
||||
>
|
||||
<div class="popover-box">
|
||||
<pre>{{ fmtBody(scope.row.resp) }}</pre>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-icon style="cursor: pointer"><warning /></el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
<span v-else>无</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteSysOperationRecordFunc(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="kra-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
deleteSysOperationRecord,
|
||||
getSysOperationRecordList,
|
||||
deleteSysOperationRecordByIds
|
||||
} from '@/api/sysOperationRecord' // 此处请自行替换地址
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'SysOperationRecord'
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
}
|
||||
// 条件搜索前端看此方法
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
if (searchInfo.value.status === '') {
|
||||
searchInfo.value.status = null
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getSysOperationRecordList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
const multipleSelection = ref([])
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const ids = []
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.forEach((item) => {
|
||||
ids.push(item.ID)
|
||||
})
|
||||
const res = await deleteSysOperationRecordByIds({ ids })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === ids.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const deleteSysOperationRecordFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteSysOperationRecord({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const fmtBody = (value) => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (_) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.table-expand {
|
||||
padding-left: 60px;
|
||||
font-size: 0;
|
||||
label {
|
||||
width: 90px;
|
||||
color: #99a9bf;
|
||||
.el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover-box {
|
||||
background: #112435;
|
||||
color: #f08047;
|
||||
height: 600px;
|
||||
width: 420px;
|
||||
overflow: auto;
|
||||
}
|
||||
.popover-box::-webkit-scrollbar {
|
||||
display: none; /* Chrome Safari */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
v-if="!targetId"
|
||||
class="flex-1 flex items-center justify-center text-gray-400"
|
||||
>
|
||||
请选择左侧{{ kind === 'department' ? '部门' : '岗位' }}后分配成员
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex justify-between items-center flex-none mb-3">
|
||||
<span class="font-bold text-base truncate"
|
||||
>「{{ targetName }}」成员</span
|
||||
>
|
||||
<el-button type="primary" :loading="saving" @click="save"
|
||||
>保存成员</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="kra-search-box !mb-2 flex-none">
|
||||
<el-form :inline="true" :model="search">
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="search.username"
|
||||
placeholder="用户名"
|
||||
@keyup.enter="query"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input
|
||||
v-model="search.nickName"
|
||||
placeholder="昵称"
|
||||
@keyup.enter="query"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="query"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="users"
|
||||
row-key="ID"
|
||||
height="100%"
|
||||
@select="onSelect"
|
||||
@select-all="onSelectAll"
|
||||
>
|
||||
<el-table-column type="selection" width="55" reserve-selection />
|
||||
<el-table-column label="ID" prop="ID" width="70" />
|
||||
<el-table-column label="用户名" prop="userName" min-width="120" />
|
||||
<el-table-column label="昵称" prop="nickName" min-width="120" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex justify-end mt-2 flex-none">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="onPage"
|
||||
@size-change="onSize"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserList } from '@/api/user'
|
||||
import { getDepartmentUsers, setDepartmentUsers } from '@/api/department'
|
||||
import { getPositionUsers, setPositionUsers } from '@/api/position'
|
||||
|
||||
const props = defineProps({
|
||||
kind: { type: String, required: true }, // 'department' | 'position'
|
||||
targetId: { type: Number, default: 0 },
|
||||
targetName: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const tableRef = ref(null)
|
||||
const users = ref([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const search = ref({ username: '', nickName: '' })
|
||||
const selected = ref(new Set())
|
||||
|
||||
const getMembers = () =>
|
||||
props.kind === 'department'
|
||||
? getDepartmentUsers({ deptId: props.targetId })
|
||||
: getPositionUsers({ positionId: props.targetId })
|
||||
|
||||
const setMembers = () =>
|
||||
props.kind === 'department'
|
||||
? setDepartmentUsers({
|
||||
deptId: props.targetId,
|
||||
userIds: [...selected.value]
|
||||
})
|
||||
: setPositionUsers({
|
||||
positionId: props.targetId,
|
||||
userIds: [...selected.value]
|
||||
})
|
||||
|
||||
const syncChecks = () => {
|
||||
users.value.forEach((u) => {
|
||||
tableRef.value &&
|
||||
tableRef.value.toggleRowSelection(u, selected.value.has(u.ID))
|
||||
})
|
||||
}
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true
|
||||
const res = await getUserList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...search.value
|
||||
})
|
||||
if (res.code === 0) {
|
||||
users.value = res.data.list || []
|
||||
total.value = res.data.total
|
||||
await nextTick()
|
||||
syncChecks()
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!props.targetId) return
|
||||
selected.value = new Set()
|
||||
const res = await getMembers()
|
||||
if (res.code === 0 && res.data) selected.value = new Set(res.data)
|
||||
page.value = 1
|
||||
await loadUsers()
|
||||
}
|
||||
|
||||
watch(() => props.targetId, load, { immediate: true })
|
||||
|
||||
const onSelect = (selection, row) => {
|
||||
if (selection.some((u) => u.ID === row.ID)) selected.value.add(row.ID)
|
||||
else selected.value.delete(row.ID)
|
||||
}
|
||||
const onSelectAll = (selection) => {
|
||||
const ids = new Set(selection.map((u) => u.ID))
|
||||
users.value.forEach((u) => {
|
||||
if (ids.has(u.ID)) selected.value.add(u.ID)
|
||||
else selected.value.delete(u.ID)
|
||||
})
|
||||
}
|
||||
const onPage = (p) => {
|
||||
page.value = p
|
||||
loadUsers()
|
||||
}
|
||||
const onSize = (s) => {
|
||||
pageSize.value = s
|
||||
page.value = 1
|
||||
loadUsers()
|
||||
}
|
||||
const query = () => {
|
||||
page.value = 1
|
||||
loadUsers()
|
||||
}
|
||||
const reset = () => {
|
||||
search.value = { username: '', nickName: '' }
|
||||
page.value = 1
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
saving.value = true
|
||||
const res = await setMembers()
|
||||
if (res.code === 0) ElMessage.success('成员保存成功')
|
||||
saving.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,604 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar title="获取参数且缓存方法已在前端utils/params 已经封装完成 不必自己书写 使用方法查看文件内注释" />
|
||||
<div class="kra-search-box">
|
||||
<el-form
|
||||
ref="elSearchFormRef"
|
||||
:inline="true"
|
||||
:model="searchInfo"
|
||||
class="demo-form-inline"
|
||||
:rules="searchRule"
|
||||
@keyup.enter="onSubmit"
|
||||
>
|
||||
<el-form-item label="创建日期" prop="createdAt">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip
|
||||
content="搜索范围是开始日期(包含)至结束日期(不包含)"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-date-picker
|
||||
v-model="searchInfo.startCreatedAt"
|
||||
type="datetime"
|
||||
placeholder="开始日期"
|
||||
:disabled-date="
|
||||
(time) =>
|
||||
searchInfo.endCreatedAt
|
||||
? time.getTime() > searchInfo.endCreatedAt.getTime()
|
||||
: false
|
||||
"
|
||||
></el-date-picker>
|
||||
—
|
||||
<el-date-picker
|
||||
v-model="searchInfo.endCreatedAt"
|
||||
type="datetime"
|
||||
placeholder="结束日期"
|
||||
:disabled-date="
|
||||
(time) =>
|
||||
searchInfo.startCreatedAt
|
||||
? time.getTime() < searchInfo.startCreatedAt.getTime()
|
||||
: false
|
||||
"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="参数名称" prop="name">
|
||||
<el-input v-model="searchInfo.name" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键" prop="key">
|
||||
<el-input v-model="searchInfo.key" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="showAllQuery">
|
||||
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="arrow-down"
|
||||
@click="showAllQuery = true"
|
||||
v-if="!showAllQuery"
|
||||
>展开</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="arrow-up"
|
||||
@click="showAllQuery = false"
|
||||
v-else
|
||||
>收起</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
style="margin-left: 10px"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="createdAt" width="180">
|
||||
<template #default="scope">{{
|
||||
formatDate(scope.row.CreatedAt)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="参数名称"
|
||||
prop="name"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column align="left" label="参数键" prop="key" width="120" />
|
||||
<el-table-column align="left" label="参数值" prop="value" width="120" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="参数说明"
|
||||
prop="desc"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="240"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="table-button"
|
||||
@click="getDetails(scope.row)"
|
||||
><el-icon style="margin-right: 5px"><InfoFilled /></el-icon
|
||||
>查看详情</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
class="table-button"
|
||||
@click="updateSysParamsFunc(scope.row)"
|
||||
>变更</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteRow(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="kra-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer
|
||||
destroy-on-close
|
||||
size="800"
|
||||
v-model="dialogFormVisible"
|
||||
:show-close="false"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{ type === 'create' ? '添加' : '修改' }}</span>
|
||||
<div>
|
||||
<el-button type="primary" @click="enterDialog">确 定</el-button>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
ref="elFormRef"
|
||||
:rules="rule"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="参数名称:" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键:" prop="key">
|
||||
<el-input
|
||||
v-model="formData.key"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数键"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数值:" prop="value">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
v-model="formData.value"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数值"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数说明:" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div
|
||||
class="usage-instructions bg-gray-100 border border-gray-300 rounded-lg p-4 mt-5"
|
||||
>
|
||||
<h3 class="mb-3 text-lg text-gray-800">使用说明</h3>
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
前端可以通过引入
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>import { getParams } from '@/utils/params'</code
|
||||
>
|
||||
然后通过
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>await getParams("{{ formData.key }}")</code
|
||||
>
|
||||
来获取对应的参数。
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
后端需要提前
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>import
|
||||
"github.com/kra/kra/server/service/system"</code
|
||||
>
|
||||
</p>
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
然后调用
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>new(system.SysParamsService).GetSysParam("{{
|
||||
formData.key
|
||||
}}")</code
|
||||
>
|
||||
来获取对应的 value 值。
|
||||
</p>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
destroy-on-close
|
||||
size="800"
|
||||
v-model="detailShow"
|
||||
:show-close="true"
|
||||
:before-close="closeDetailShow"
|
||||
>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="参数名称">
|
||||
{{ detailForm.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数键">
|
||||
{{ detailForm.key }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数值">
|
||||
{{ detailForm.value }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数说明">
|
||||
{{ detailForm.desc }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysParams,
|
||||
deleteSysParams,
|
||||
deleteSysParamsByIds,
|
||||
updateSysParams,
|
||||
findSysParams,
|
||||
getSysParamsList
|
||||
} from '@/api/sysParams'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import WarningBar from "@/components/warningBar/warningBar.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'SysParams'
|
||||
})
|
||||
|
||||
// 控制更多查询条件显示/隐藏状态
|
||||
const showAllQuery = ref(false)
|
||||
|
||||
// 自动化生成的字典(可能为空)以及字段
|
||||
const formData = ref({
|
||||
name: '',
|
||||
key: '',
|
||||
value: '',
|
||||
desc: ''
|
||||
})
|
||||
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const searchRule = reactive({
|
||||
createdAt: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (
|
||||
searchInfo.value.startCreatedAt &&
|
||||
!searchInfo.value.endCreatedAt
|
||||
) {
|
||||
callback(new Error('请填写结束日期'))
|
||||
} else if (
|
||||
!searchInfo.value.startCreatedAt &&
|
||||
searchInfo.value.endCreatedAt
|
||||
) {
|
||||
callback(new Error('请填写开始日期'))
|
||||
} else if (
|
||||
searchInfo.value.startCreatedAt &&
|
||||
searchInfo.value.endCreatedAt &&
|
||||
(searchInfo.value.startCreatedAt.getTime() ===
|
||||
searchInfo.value.endCreatedAt.getTime() ||
|
||||
searchInfo.value.startCreatedAt.getTime() >
|
||||
searchInfo.value.endCreatedAt.getTime())
|
||||
) {
|
||||
callback(new Error('开始日期应当早于结束日期'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
// =========== 表格控制部分 ===========
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 修改页面容量
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getSysParamsList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// ============== 表格控制部分结束 ===============
|
||||
|
||||
// 获取需要的字典 可能为空 按需保留
|
||||
const setOptions = async () => {}
|
||||
|
||||
// 获取需要的字典 可能为空 按需保留
|
||||
setOptions()
|
||||
|
||||
// 多选数据
|
||||
const multipleSelection = ref([])
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteSysParamsFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const IDs = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map((item) => {
|
||||
IDs.push(item.ID)
|
||||
})
|
||||
const res = await deleteSysParamsByIds({ IDs })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 行为控制标记(弹窗内部需要增还是改)
|
||||
const type = ref('')
|
||||
|
||||
// 更新行
|
||||
const updateSysParamsFunc = async (row) => {
|
||||
const res = await findSysParams({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteSysParamsFunc = async (row) => {
|
||||
const res = await deleteSysParams({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗控制标记
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
name: '',
|
||||
key: '',
|
||||
value: '',
|
||||
desc: ''
|
||||
}
|
||||
}
|
||||
// 弹窗确定
|
||||
const enterDialog = async () => {
|
||||
elFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createSysParams(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateSysParams(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createSysParams(formData.value)
|
||||
break
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const detailForm = ref({})
|
||||
|
||||
// 查看详情控制标记
|
||||
const detailShow = ref(false)
|
||||
|
||||
// 打开详情弹窗
|
||||
const openDetailShow = () => {
|
||||
detailShow.value = true
|
||||
}
|
||||
|
||||
// 打开详情
|
||||
const getDetails = async (row) => {
|
||||
// 打开弹窗
|
||||
const res = await findSysParams({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
detailForm.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailForm.value = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="p-2 flex flex-col overflow-hidden"
|
||||
:style="{ height: rootHeight }"
|
||||
>
|
||||
<el-splitter class="flex-1 min-h-0">
|
||||
<el-splitter-panel size="380px" min="260px" max="700px" collapsible>
|
||||
<div
|
||||
class="kra-table-box !my-0 mr-2 h-full flex flex-col overflow-hidden"
|
||||
>
|
||||
<div class="flex justify-between items-center flex-none gap-2">
|
||||
<span class="font-bold flex-none">岗位列表</span>
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
placeholder="搜索岗位"
|
||||
clearable
|
||||
class="!flex-1"
|
||||
@keyup.enter="getTableData"
|
||||
@clear="getTableData"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
class="flex-none"
|
||||
@click="addPosition"
|
||||
>新增</el-button
|
||||
>
|
||||
</div>
|
||||
<el-scrollbar class="mt-3 flex-1">
|
||||
<div
|
||||
v-for="pos in tableData"
|
||||
:key="pos.ID"
|
||||
class="rounded flex justify-between items-center px-3 py-3 cursor-pointer mt-2 gap-2 group bg-gray-50 dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900"
|
||||
:class="
|
||||
selectedPos.ID === pos.ID
|
||||
? 'text-active'
|
||||
: 'text-slate-700 dark:text-slate-50'
|
||||
"
|
||||
@click="selectedPos = pos"
|
||||
>
|
||||
<div class="truncate">
|
||||
{{ pos.name }}
|
||||
<span v-if="pos.code" class="text-sm text-gray-400"
|
||||
>({{ pos.code }})</span
|
||||
>
|
||||
<el-tag v-if="!pos.status" size="small" type="info" class="ml-1"
|
||||
>停用</el-tag
|
||||
>
|
||||
</div>
|
||||
<div class="hidden group-hover:inline-flex items-center gap-2">
|
||||
<el-icon
|
||||
class="!text-blue-500"
|
||||
title="编辑"
|
||||
@click.stop="editPosition(pos)"
|
||||
>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="!text-red-500"
|
||||
title="删除"
|
||||
@click.stop="deleteRow(pos)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!tableData.length"
|
||||
description="暂无岗位"
|
||||
:image-size="80"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
<el-splitter-panel :min="300">
|
||||
<div class="kra-table-box !my-0 ml-2 h-full overflow-hidden">
|
||||
<org-member-assign
|
||||
kind="position"
|
||||
:target-id="selectedPos.ID || 0"
|
||||
:target-name="selectedPos.name || ''"
|
||||
/>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<el-drawer
|
||||
v-model="formVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">{{ titleForm }}</span>
|
||||
<div>
|
||||
<el-button @click="closeForm">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form ref="posForm" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="岗位名称" prop="name">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
autocomplete="off"
|
||||
placeholder="请输入岗位名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位编码" prop="code">
|
||||
<el-input
|
||||
v-model="form.code"
|
||||
autocomplete="off"
|
||||
placeholder="请输入岗位编码"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch v-model="form.status" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
autocomplete="off"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getPositionList,
|
||||
createPosition,
|
||||
updatePosition,
|
||||
deletePosition
|
||||
} from '@/api/position'
|
||||
import OrgMemberAssign from '@/view/superAdmin/orgMemberAssign.vue'
|
||||
import { Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'Position'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tableData = ref([])
|
||||
const selectedPos = ref({})
|
||||
const searchName = ref('')
|
||||
|
||||
// 整页填满视口高度(照字典管理)
|
||||
const rootRef = ref(null)
|
||||
const rootHeight = ref('auto')
|
||||
const updateRootHeight = () => {
|
||||
const el = rootRef.value
|
||||
if (!el) return
|
||||
const top = el.getBoundingClientRect().top
|
||||
rootHeight.value = `${Math.max(320, window.innerHeight - top - 32)}px`
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(updateRootHeight)
|
||||
window.addEventListener('resize', updateRootHeight)
|
||||
})
|
||||
onActivated(updateRootHeight)
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
|
||||
|
||||
const getTableData = async () => {
|
||||
const res = await getPositionList({
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
name: searchName.value.trim()
|
||||
})
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list || []
|
||||
if (!selectedPos.value.ID && tableData.value.length) {
|
||||
selectedPos.value = tableData.value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
getTableData()
|
||||
|
||||
const formVisible = ref(false)
|
||||
const titleForm = ref('新增岗位')
|
||||
const dialogType = ref('add')
|
||||
const posForm = ref(null)
|
||||
const defaultForm = () => ({
|
||||
ID: 0,
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: true,
|
||||
remark: ''
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const initForm = () => {
|
||||
posForm.value && posForm.value.resetFields()
|
||||
form.value = defaultForm()
|
||||
}
|
||||
const closeForm = () => {
|
||||
initForm()
|
||||
formVisible.value = false
|
||||
}
|
||||
|
||||
const addPosition = () => {
|
||||
initForm()
|
||||
titleForm.value = '新增岗位'
|
||||
dialogType.value = 'add'
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const editPosition = (row) => {
|
||||
titleForm.value = '编辑岗位'
|
||||
dialogType.value = 'edit'
|
||||
form.value = {
|
||||
ID: row.ID,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
sort: row.sort,
|
||||
status: !!row.status,
|
||||
remark: row.remark
|
||||
}
|
||||
posForm.value && posForm.value.clearValidate()
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
posForm.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const api = dialogType.value === 'add' ? createPosition : updatePosition
|
||||
const res = await api(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: dialogType.value === 'add' ? '添加成功!' : '编辑成功!'
|
||||
})
|
||||
getTableData()
|
||||
closeForm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除该岗位, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deletePosition({ id: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '删除成功!' })
|
||||
if (selectedPos.value.ID === row.ID) selectedPos.value = {}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({ type: 'info', message: '已取消删除' })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,753 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar title="注:右上角头像下拉可切换角色" />
|
||||
<div class="kra-search-box">
|
||||
<el-form ref="searchForm" :inline="true" :model="searchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchInfo.username" placeholder="用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="searchInfo.nickname" placeholder="昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="searchInfo.phone" placeholder="手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="searchInfo.email" placeholder="邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="addUser"
|
||||
>新增用户</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table :data="tableData" row-key="ID" :default-sort="{ prop: 'ID', order: 'descending' }" @sort-change="sortChange">
|
||||
<el-table-column align="left" label="头像" min-width="75">
|
||||
<template #default="scope">
|
||||
<CustomPic style="margin-top: 8px" :pic-src="scope.row.headerImg" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="ID" min-width="50" prop="ID" sortable="custom" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="用户名"
|
||||
min-width="150"
|
||||
prop="userName"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="昵称"
|
||||
min-width="150"
|
||||
prop="nickName"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="手机号"
|
||||
min-width="180"
|
||||
prop="phone"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="邮箱"
|
||||
min-width="180"
|
||||
prop="email"
|
||||
/>
|
||||
<el-table-column align="left" label="用户角色" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-cascader
|
||||
v-model="scope.row.authorityIds"
|
||||
:options="authOptions"
|
||||
:show-all-levels="false"
|
||||
collapse-tags
|
||||
:props="{
|
||||
multiple: true,
|
||||
checkStrictly: true,
|
||||
label: 'authorityName',
|
||||
value: 'authorityId',
|
||||
disabled: 'disabled',
|
||||
emitPath: false
|
||||
}"
|
||||
:clearable="false"
|
||||
@visible-change="
|
||||
(flag) => {
|
||||
changeAuthority(scope.row, flag, 0)
|
||||
}
|
||||
"
|
||||
@remove-tag="
|
||||
(removeAuth) => {
|
||||
changeAuthority(scope.row, false, removeAuth)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="所属部门" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-cascader
|
||||
v-model="scope.row.departmentIds"
|
||||
:options="deptOptions"
|
||||
:show-all-levels="false"
|
||||
collapse-tags
|
||||
:props="{
|
||||
multiple: true,
|
||||
checkStrictly: true,
|
||||
label: 'name',
|
||||
value: 'ID',
|
||||
emitPath: false
|
||||
}"
|
||||
placeholder="未分配"
|
||||
@visible-change="
|
||||
(flag) => {
|
||||
changeDepartment(scope.row, flag, 0)
|
||||
}
|
||||
"
|
||||
@remove-tag="
|
||||
(removeId) => {
|
||||
changeDepartment(scope.row, false, removeId)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="岗位" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.positionIds"
|
||||
multiple
|
||||
collapse-tags
|
||||
placeholder="未分配"
|
||||
@visible-change="
|
||||
(flag) => {
|
||||
changePosition(scope.row, flag)
|
||||
}
|
||||
"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in positionOptions"
|
||||
:key="p.ID"
|
||||
:label="p.name"
|
||||
:value="p.ID"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="启用" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.enable"
|
||||
inline-prompt
|
||||
:active-value="1"
|
||||
:inactive-value="2"
|
||||
@change="
|
||||
() => {
|
||||
switchEnable(scope.row)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" :min-width="appStore.operateMinWith" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteUserFunc(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="openEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="magic-stick"
|
||||
@click="resetPasswordFunc(scope.row)"
|
||||
>重置密码</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="kra-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 重置密码对话框 -->
|
||||
<el-dialog
|
||||
v-model="resetPwdDialog"
|
||||
title="重置密码"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form :model="resetPwdInfo" ref="resetPwdForm" label-width="100px">
|
||||
<el-form-item label="用户账号">
|
||||
<el-input v-model="resetPwdInfo.userName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称">
|
||||
<el-input v-model="resetPwdInfo.nickName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<div class="flex w-full">
|
||||
<el-input class="flex-1" v-model="resetPwdInfo.password" placeholder="请输入新密码" show-password />
|
||||
<el-button type="primary" @click="generateRandomPassword" style="margin-left: 10px">
|
||||
生成随机密码
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeResetPwdDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="confirmResetPassword">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
v-model="addUserDialog"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-base">用户</span>
|
||||
<div>
|
||||
<el-button @click="closeAddUserDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="enterAddUserDialog"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="userForm"
|
||||
:rules="rules"
|
||||
:model="userInfo"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item
|
||||
v-if="dialogFlag === 'add'"
|
||||
label="用户名"
|
||||
prop="userName"
|
||||
>
|
||||
<el-input v-model="userInfo.userName" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogFlag === 'add'" label="密码" prop="password">
|
||||
<el-input v-model="userInfo.password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nickName">
|
||||
<el-input v-model="userInfo.nickName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="userInfo.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="userInfo.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户角色" prop="authorityId">
|
||||
<el-cascader
|
||||
v-model="userInfo.authorityIds"
|
||||
style="width: 100%"
|
||||
:options="authOptions"
|
||||
:show-all-levels="false"
|
||||
:props="{
|
||||
multiple: true,
|
||||
checkStrictly: true,
|
||||
label: 'authorityName',
|
||||
value: 'authorityId',
|
||||
disabled: 'disabled',
|
||||
emitPath: false
|
||||
}"
|
||||
:clearable="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="disabled">
|
||||
<el-switch
|
||||
v-model="userInfo.enable"
|
||||
inline-prompt
|
||||
:active-value="1"
|
||||
:inactive-value="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像" label-width="80px">
|
||||
<SelectImage v-model="userInfo.headerImg" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getUserList,
|
||||
setUserAuthorities,
|
||||
setUserDepartments,
|
||||
setUserPositions,
|
||||
register,
|
||||
deleteUser
|
||||
} from '@/api/user'
|
||||
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import { getDepartmentList } from '@/api/department'
|
||||
import { getPositionList } from '@/api/position'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { setUserInfo, resetPassword } from '@/api/user.js'
|
||||
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import SelectImage from '@/components/selectImage/selectImage.vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
import { toSQLLine } from '@/utils/stringFun'
|
||||
|
||||
defineOptions({
|
||||
name: 'User'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const searchInfo = ref({
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
}
|
||||
orderKey.value = 'id'
|
||||
desc.value = true
|
||||
getTableData()
|
||||
}
|
||||
// 初始化相关
|
||||
const setAuthorityOptions = (AuthorityData, optionsData) => {
|
||||
AuthorityData &&
|
||||
AuthorityData.forEach((item) => {
|
||||
if (item.children && item.children.length) {
|
||||
const option = {
|
||||
authorityId: item.authorityId,
|
||||
authorityName: item.authorityName,
|
||||
children: []
|
||||
}
|
||||
setAuthorityOptions(item.children, option.children)
|
||||
optionsData.push(option)
|
||||
} else {
|
||||
const option = {
|
||||
authorityId: item.authorityId,
|
||||
authorityName: item.authorityName
|
||||
}
|
||||
optionsData.push(option)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const orderKey = ref('id')
|
||||
const desc = ref(true)
|
||||
|
||||
const sortChange = ({ prop, order }) => {
|
||||
if (prop) {
|
||||
orderKey.value = prop === 'ID' ? 'id' : toSQLLine(prop)
|
||||
desc.value = order === 'descending'
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getUserList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
orderKey: orderKey.value,
|
||||
desc: desc.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => tableData.value,
|
||||
() => {
|
||||
setAuthorityIds()
|
||||
setDeptPositionIds()
|
||||
}
|
||||
)
|
||||
|
||||
const authOptions = ref([])
|
||||
const deptOptions = ref([])
|
||||
const positionOptions = ref([])
|
||||
const setOptions = (authData) => {
|
||||
authOptions.value = []
|
||||
setAuthorityOptions(authData, authOptions.value)
|
||||
}
|
||||
|
||||
const initPage = async () => {
|
||||
getTableData()
|
||||
const res = await getAuthorityList()
|
||||
setOptions(res.data)
|
||||
const deptRes = await getDepartmentList()
|
||||
if (deptRes.code === 0) {
|
||||
deptOptions.value = deptRes.data || []
|
||||
}
|
||||
const posRes = await getPositionList({ page: 1, pageSize: 1000 })
|
||||
if (posRes.code === 0) {
|
||||
positionOptions.value = posRes.data.list || []
|
||||
}
|
||||
}
|
||||
|
||||
initPage()
|
||||
|
||||
// 把后端返回的部门/岗位关联映射成前端多选控件需要的 id 数组
|
||||
const setDeptPositionIds = () => {
|
||||
tableData.value &&
|
||||
tableData.value.forEach((user) => {
|
||||
user.departmentIds = user.departments
|
||||
? user.departments.map((i) => i.ID)
|
||||
: []
|
||||
user.positionIds = user.positions
|
||||
? user.positions.map((i) => i.ID)
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
// 重置密码对话框相关
|
||||
const resetPwdDialog = ref(false)
|
||||
const resetPwdForm = ref(null)
|
||||
const resetPwdInfo = ref({
|
||||
ID: '',
|
||||
userName: '',
|
||||
nickName: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 生成随机密码
|
||||
const generateRandomPassword = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
|
||||
let password = ''
|
||||
for (let i = 0; i < 12; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
resetPwdInfo.value.password = password
|
||||
// 复制到剪贴板
|
||||
navigator.clipboard.writeText(password).then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '密码已复制到剪贴板'
|
||||
})
|
||||
}).catch(() => {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '复制失败,请手动复制'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 打开重置密码对话框
|
||||
const resetPasswordFunc = (row) => {
|
||||
resetPwdInfo.value.ID = row.ID
|
||||
resetPwdInfo.value.userName = row.userName
|
||||
resetPwdInfo.value.nickName = row.nickName
|
||||
resetPwdInfo.value.password = ''
|
||||
resetPwdDialog.value = true
|
||||
}
|
||||
|
||||
// 确认重置密码
|
||||
const confirmResetPassword = async () => {
|
||||
if (!resetPwdInfo.value.password) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请输入或生成密码'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const res = await resetPassword({
|
||||
ID: resetPwdInfo.value.ID,
|
||||
password: resetPwdInfo.value.password
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg || '密码重置成功'
|
||||
})
|
||||
resetPwdDialog.value = false
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: res.msg || '密码重置失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭重置密码对话框
|
||||
const closeResetPwdDialog = () => {
|
||||
resetPwdInfo.value.password = ''
|
||||
resetPwdDialog.value = false
|
||||
}
|
||||
const setAuthorityIds = () => {
|
||||
tableData.value &&
|
||||
tableData.value.forEach((user) => {
|
||||
user.authorityIds =
|
||||
user.authorities &&
|
||||
user.authorities.map((i) => {
|
||||
return i.authorityId
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteUserFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteUser({ id: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
const userInfo = ref({
|
||||
userName: '',
|
||||
password: '',
|
||||
nickName: '',
|
||||
headerImg: '',
|
||||
authorityId: '',
|
||||
authorityIds: [],
|
||||
enable: 1
|
||||
})
|
||||
|
||||
const rules = ref({
|
||||
userName: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 5, message: '最低5位字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入用户密码', trigger: 'blur' },
|
||||
{ min: 6, message: '最低6位字符', trigger: 'blur' }
|
||||
],
|
||||
nickName: [{ required: true, message: '请输入用户昵称', trigger: 'blur' }],
|
||||
phone: [
|
||||
{
|
||||
pattern: /^1([38][0-9]|4[014-9]|[59][0-35-9]|6[2567]|7[0-8])\d{8}$/,
|
||||
message: '请输入合法手机号',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
email: [
|
||||
{
|
||||
pattern: /^([0-9A-Za-z\-_.]+)@([0-9a-z]+\.[a-z]{2,3}(\.[a-z]{2})?)$/g,
|
||||
message: '请输入正确的邮箱',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
authorityId: [
|
||||
{ required: true, message: '请选择用户角色', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
const userForm = ref(null)
|
||||
const enterAddUserDialog = async () => {
|
||||
userInfo.value.authorityId = userInfo.value.authorityIds[0]
|
||||
userForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const req = {
|
||||
...userInfo.value
|
||||
}
|
||||
if (dialogFlag.value === 'add') {
|
||||
const res = await register(req)
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '创建成功' })
|
||||
await getTableData()
|
||||
closeAddUserDialog()
|
||||
}
|
||||
}
|
||||
if (dialogFlag.value === 'edit') {
|
||||
const res = await setUserInfo(req)
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '编辑成功' })
|
||||
await getTableData()
|
||||
closeAddUserDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addUserDialog = ref(false)
|
||||
const closeAddUserDialog = () => {
|
||||
userForm.value.resetFields()
|
||||
userInfo.value.headerImg = ''
|
||||
userInfo.value.authorityIds = []
|
||||
addUserDialog.value = false
|
||||
}
|
||||
|
||||
const dialogFlag = ref('add')
|
||||
|
||||
const addUser = () => {
|
||||
dialogFlag.value = 'add'
|
||||
addUserDialog.value = true
|
||||
}
|
||||
|
||||
const tempAuth = {}
|
||||
const changeAuthority = async (row, flag, removeAuth) => {
|
||||
if (flag) {
|
||||
if (!removeAuth) {
|
||||
tempAuth[row.ID] = [...row.authorityIds]
|
||||
}
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
const res = await setUserAuthorities({
|
||||
ID: row.ID,
|
||||
authorityIds: row.authorityIds
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '角色设置成功' })
|
||||
} else {
|
||||
if (!removeAuth) {
|
||||
row.authorityIds = [...tempAuth[row.ID]]
|
||||
delete tempAuth[row.ID]
|
||||
} else {
|
||||
row.authorityIds = [removeAuth, ...row.authorityIds]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户归属部门(即时保存,主部门默认沿用原主部门或取首个)
|
||||
const tempDept = {}
|
||||
const changeDepartment = async (row, flag, removeId) => {
|
||||
if (flag) {
|
||||
if (!removeId) {
|
||||
tempDept[row.ID] = [...(row.departmentIds || [])]
|
||||
}
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
const deptIds = row.departmentIds || []
|
||||
let primaryDeptId = 0
|
||||
if (deptIds.length) {
|
||||
primaryDeptId = deptIds.includes(row.deptId) ? row.deptId : deptIds[0]
|
||||
}
|
||||
const res = await setUserDepartments({
|
||||
ID: row.ID,
|
||||
deptIds,
|
||||
primaryDeptId
|
||||
})
|
||||
if (res.code === 0) {
|
||||
row.deptId = primaryDeptId
|
||||
ElMessage({ type: 'success', message: '部门设置成功' })
|
||||
} else {
|
||||
if (!removeId) {
|
||||
row.departmentIds = [...(tempDept[row.ID] || [])]
|
||||
delete tempDept[row.ID]
|
||||
} else {
|
||||
row.departmentIds = [removeId, ...(row.departmentIds || [])]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户岗位(即时保存)
|
||||
const tempPos = {}
|
||||
const changePosition = async (row, flag) => {
|
||||
if (flag) {
|
||||
tempPos[row.ID] = [...(row.positionIds || [])]
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
const res = await setUserPositions({
|
||||
ID: row.ID,
|
||||
positionIds: row.positionIds || []
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '岗位设置成功' })
|
||||
} else {
|
||||
row.positionIds = [...(tempPos[row.ID] || [])]
|
||||
delete tempPos[row.ID]
|
||||
}
|
||||
}
|
||||
|
||||
const openEdit = (row) => {
|
||||
dialogFlag.value = 'edit'
|
||||
userInfo.value = JSON.parse(JSON.stringify(row))
|
||||
addUserDialog.value = true
|
||||
}
|
||||
|
||||
const switchEnable = async (row) => {
|
||||
userInfo.value = JSON.parse(JSON.stringify(row))
|
||||
await nextTick()
|
||||
const req = {
|
||||
...userInfo.value
|
||||
}
|
||||
const res = await setUserInfo(req)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: `${req.enable === 2 ? '禁用' : '启用'}成功`
|
||||
})
|
||||
await getTableData()
|
||||
userInfo.value.headerImg = ''
|
||||
userInfo.value.authorityIds = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header-img-box {
|
||||
@apply w-52 h-52 border border-solid border-gray-300 rounded-xl flex justify-center items-center cursor-pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user