123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- <template>
- <div class="flex">
- <el-card class="w-1/5 user" :gutter="12" shadow="always">
- <template #header>
- <div class="card-header">
- <span>部门列表</span>
- <XTextButton title="修改部门" @click="handleDeptEdit()" />
- </div>
- </template>
- <el-input v-model="filterText" placeholder="搜索部门" />
- <el-tree
- ref="treeRef"
- node-key="id"
- default-expand-all
- :data="deptOptions"
- :props="defaultProps"
- :highlight-current="true"
- :filter-node-method="filterNode"
- :expand-on-click-node="false"
- @node-click="handleDeptNodeClick"
- />
- </el-card>
- <el-card class="w-4/5 user" style="margin-left: 10px" :gutter="12" shadow="hover">
- <template #header>
- <div class="card-header">
- <span>{{ tableTitle }}</span>
- </div>
- </template>
- <!-- 列表 -->
- <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
- <!-- 操作:新增 -->
- <template #toolbar_buttons>
- <XButton
- type="primary"
- preIcon="ep:zoom-in"
- :title="t('action.add')"
- v-hasPermi="['system:user:create']"
- @click="handleCreate()"
- />
- <XButton
- type="warning"
- preIcon="ep:upload"
- :title="t('action.import')"
- v-hasPermi="['system:user:import']"
- @click="importDialogVisible = true"
- />
- <XButton
- type="warning"
- preIcon="ep:download"
- :title="t('action.export')"
- v-hasPermi="['system:user:export']"
- @click="exportList('用户数据.xls')"
- />
- </template>
- <template #status="{ row }">
- <el-switch
- v-model="row.status"
- :active-value="0"
- :inactive-value="1"
- @change="handleStatusChange(row)"
- />
- </template>
- <template #actionbtns_default="{ row }">
- <XTextButton
- preIcon="ep:edit"
- :title="t('action.edit')"
- v-hasPermi="['system:user:update']"
- @click="handleUpdate(row.id)"
- />
- <!-- 操作:详情 -->
- <XTextButton
- preIcon="ep:view"
- :title="t('action.detail')"
- v-hasPermi="['system:user:update']"
- @click="handleDetail(row.id)"
- />
- <XTextButton
- preIcon="ep:key"
- title="重置密码"
- v-hasPermi="['system:user:update-password']"
- @click="handleResetPwd(row)"
- />
- <XTextButton
- preIcon="ep:key"
- title="分配角色"
- v-hasPermi="['system:permission:assign-user-role']"
- @click="handleRole(row)"
- />
- <!-- 操作:删除 -->
- <XTextButton
- preIcon="ep:delete"
- :title="t('action.del')"
- v-hasPermi="['system:user:delete']"
- @click="handleDelete(row.id)"
- />
- </template>
- </vxe-grid>
- </el-card>
- </div>
- <XModal v-model="dialogVisible" :title="dialogTitle">
- <!-- 对话框(添加 / 修改) -->
- <Form
- v-if="['create', 'update'].includes(actionType)"
- :rules="rules"
- :schema="allSchemas.formSchema"
- ref="formRef"
- >
- <template #deptId>
- <el-tree-select
- node-key="id"
- v-model="deptId"
- :props="defaultProps"
- :data="deptOptions"
- check-strictly
- />
- </template>
- <template #postIds>
- <el-select v-model="postIds" multiple :placeholder="t('common.selectText')">
- <el-option
- v-for="item in postOptions"
- :key="item.id"
- :label="item.name"
- :value="item.id"
- />
- </el-select>
- </template>
- </Form>
- <!-- 对话框(详情) -->
- <Descriptions
- v-if="actionType === 'detail'"
- :schema="allSchemas.detailSchema"
- :data="detailData"
- >
- <template #deptId="{ row }">
- <span>{{ row.dept?.name }}</span>
- </template>
- <template #postIds="{ row }">
- <el-tag v-for="(post, index) in row.postIds" :key="index" index="">
- <template v-for="postObj in postOptions">
- {{ post === postObj.id ? postObj.name : '' }}
- </template>
- </el-tag>
- </template>
- </Descriptions>
- <!-- 操作按钮 -->
- <template #footer>
- <!-- 按钮:保存 -->
- <XButton
- v-if="['create', 'update'].includes(actionType)"
- type="primary"
- :title="t('action.save')"
- :loading="loading"
- @click="submitForm()"
- />
- <!-- 按钮:关闭 -->
- <XButton :loading="loading" :title="t('dialog.close')" @click="dialogVisible = false" />
- </template>
- </XModal>
- <!-- 分配用户角色 -->
- <XModal v-model="roleDialogVisible" title="分配角色">
- <el-form :model="userRole" label-width="140px" :inline="true">
- <el-form-item label="用户名称">
- <el-tag>{{ userRole.username }}</el-tag>
- </el-form-item>
- <el-form-item label="用户昵称">
- <el-tag>{{ userRole.nickname }}</el-tag>
- </el-form-item>
- <el-form-item label="角色">
- <el-transfer
- v-model="userRole.roleIds"
- :titles="['角色列表', '已选择']"
- :props="{
- key: 'id',
- label: 'name'
- }"
- :data="roleOptions"
- />
- </el-form-item>
- </el-form>
- <!-- 操作按钮 -->
- <template #footer>
- <!-- 按钮:保存 -->
- <XButton type="primary" :title="t('action.save')" :loading="loading" @click="submitRole()" />
- <!-- 按钮:关闭 -->
- <XButton :title="t('dialog.close')" @click="roleDialogVisible = false" />
- </template>
- </XModal>
- <!-- 导入 -->
- <XModal v-model="importDialogVisible" :title="importDialogTitle">
- <el-form class="drawer-multiColumn-form" label-width="150px">
- <el-form-item label="模板下载 :">
- <el-button type="primary" @click="handleImportTemp">
- <Icon icon="ep:download" />
- 点击下载
- </el-button>
- </el-form-item>
- <el-form-item label="文件上传 :">
- <el-upload
- ref="uploadRef"
- :action="updateUrl + '?updateSupport=' + updateSupport"
- :headers="uploadHeaders"
- :drag="true"
- :limit="1"
- :multiple="true"
- :show-file-list="true"
- :disabled="uploadDisabled"
- :before-upload="beforeExcelUpload"
- :on-exceed="handleExceed"
- :on-success="handleFileSuccess"
- :on-error="excelUploadError"
- :auto-upload="false"
- accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- >
- <Icon icon="ep:upload-filled" />
- <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
- <template #tip>
- <div class="el-upload__tip">请上传 .xls , .xlsx 标准格式文件</div>
- </template>
- </el-upload>
- </el-form-item>
- <el-form-item label="是否更新已经存在的用户数据:">
- <el-checkbox v-model="updateSupport" />
- </el-form-item>
- </el-form>
- <template #footer>
- <!-- 按钮:保存 -->
- <XButton
- type="warning"
- preIcon="ep:upload-filled"
- :title="t('action.save')"
- @click="submitFileForm()"
- />
- <!-- 按钮:关闭 -->
- <XButton :title="t('dialog.close')" @click="importDialogVisible = false" />
- </template>
- </XModal>
- </template>
- <script setup lang="ts" name="User">
- import { nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
- import {
- ElTag,
- ElInput,
- ElCard,
- ElTree,
- ElTreeSelect,
- ElSelect,
- ElOption,
- ElTransfer,
- ElForm,
- ElFormItem,
- ElUpload,
- ElSwitch,
- ElCheckbox,
- UploadInstance,
- UploadRawFile
- } from 'element-plus'
- import { useRouter } from 'vue-router'
- import { VxeGridInstance } from 'vxe-table'
- import { handleTree } from '@/utils/tree'
- import download from '@/utils/download'
- import { CommonStatusEnum } from '@/utils/constants'
- import { getAccessToken, getTenantId } from '@/utils/auth'
- import { useI18n } from '@/hooks/web/useI18n'
- import { useMessage } from '@/hooks/web/useMessage'
- import { useVxeGrid } from '@/hooks/web/useVxeGrid'
- import { FormExpose } from '@/components/Form'
- import { rules, allSchemas } from './user.data'
- import * as UserApi from '@/api/system/user'
- import { listSimpleDeptApi } from '@/api/system/dept'
- import { listSimpleRolesApi } from '@/api/system/role'
- import { listSimplePostsApi, PostVO } from '@/api/system/post'
- import {
- aassignUserRoleApi,
- listUserRolesApi,
- PermissionAssignUserRoleReqVO
- } from '@/api/system/permission'
- const { t } = useI18n() // 国际化
- const message = useMessage() // 消息弹窗
- const defaultProps = {
- children: 'children',
- label: 'name',
- value: 'id'
- }
- const queryParams = reactive({
- deptId: null
- })
- // ========== 列表相关 ==========
- const tableTitle = ref('用户列表')
- // 列表相关的变量
- const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
- const { gridOptions, getList, deleteData, exportList } = useVxeGrid<UserApi.UserVO>({
- allSchemas: allSchemas,
- queryParams: queryParams,
- getListApi: UserApi.getUserPageApi,
- deleteApi: UserApi.deleteUserApi,
- exportListApi: UserApi.exportUserApi
- })
- // ========== 创建部门树结构 ==========
- const filterText = ref('')
- const deptOptions = ref<any[]>([]) // 树形结构
- const treeRef = ref<InstanceType<typeof ElTree>>()
- const getTree = async () => {
- const res = await listSimpleDeptApi()
- deptOptions.value.push(...handleTree(res))
- }
- const filterNode = (value: string, data: Tree) => {
- if (!value) return true
- return data.name.includes(value)
- }
- const handleDeptNodeClick = async (row: { [key: string]: any }) => {
- queryParams.deptId = row.id
- await getList(xGrid)
- }
- const { push } = useRouter()
- const handleDeptEdit = () => {
- push('/system/dept')
- }
- watch(filterText, (val) => {
- treeRef.value!.filter(val)
- })
- // ========== CRUD 相关 ==========
- const loading = ref(false) // 遮罩层
- const actionType = ref('') // 操作按钮的类型
- const dialogVisible = ref(false) // 是否显示弹出层
- const dialogTitle = ref('edit') // 弹出层标题
- const formRef = ref<FormExpose>() // 表单 Ref
- const deptId = ref() // 部门ID
- const postIds = ref<string[]>([]) // 岗位ID
- const postOptions = ref<PostVO[]>([]) //岗位列表
- // 获取岗位列表
- const getPostOptions = async () => {
- const res = await listSimplePostsApi()
- postOptions.value.push(...res)
- }
- // 设置标题
- const setDialogTile = async (type: string) => {
- dialogTitle.value = t('action.' + type)
- actionType.value = type
- dialogVisible.value = true
- }
- // 新增操作
- const handleCreate = async () => {
- setDialogTile('create')
- // 重置表单
- deptId.value = null
- postIds.value = []
- await nextTick()
- if (allSchemas.formSchema[0].field !== 'username') {
- unref(formRef)?.addSchema(
- {
- field: 'username',
- label: '用户账号',
- component: 'Input'
- },
- 0
- )
- unref(formRef)?.addSchema(
- {
- field: 'password',
- label: '用户密码',
- component: 'InputPassword'
- },
- 1
- )
- }
- }
- // 修改操作
- const handleUpdate = async (rowId: number) => {
- setDialogTile('update')
- await nextTick()
- unref(formRef)?.delSchema('username')
- unref(formRef)?.delSchema('password')
- // 设置数据
- const res = await UserApi.getUserApi(rowId)
- deptId.value = res.deptId
- postIds.value = res.postIds
- unref(formRef)?.setValues(res)
- }
- const detailData = ref()
- // 详情操作
- const handleDetail = async (rowId: number) => {
- // 设置数据
- const res = await UserApi.getUserApi(rowId)
- detailData.value = res
- await setDialogTile('detail')
- }
- // 删除操作
- const handleDelete = async (rowId: number) => {
- await deleteData(xGrid, rowId)
- }
- // 提交按钮
- const submitForm = async () => {
- loading.value = true
- // 提交请求
- try {
- const data = unref(formRef)?.formModel as UserApi.UserVO
- data.deptId = deptId.value
- data.postIds = postIds.value
- if (actionType.value === 'create') {
- await UserApi.createUserApi(data)
- message.success(t('common.createSuccess'))
- } else {
- await UserApi.updateUserApi(data)
- message.success(t('common.updateSuccess'))
- }
- dialogVisible.value = false
- } finally {
- // unref(formRef)?.setSchema(allSchemas.formSchema)
- // 刷新列表
- await getList(xGrid)
- loading.value = false
- }
- }
- // 改变用户状态操作
- const handleStatusChange = async (row: UserApi.UserVO) => {
- const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
- message
- .confirm('确认要"' + text + '""' + row.username + '"用户吗?', t('common.reminder'))
- .then(async () => {
- row.status =
- row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.ENABLE : CommonStatusEnum.DISABLE
- await UserApi.updateUserStatusApi(row.id, row.status)
- message.success(text + '成功')
- // 刷新列表
- await getList(xGrid)
- })
- .catch(() => {
- row.status =
- row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
- })
- }
- // 重置密码
- const handleResetPwd = (row: UserApi.UserVO) => {
- message.prompt('请输入"' + row.username + '"的新密码', t('common.reminder')).then(({ value }) => {
- UserApi.resetUserPwdApi(row.id, value).then(() => {
- message.success('修改成功,新密码是:' + value)
- })
- })
- }
- // 分配角色
- const roleDialogVisible = ref(false)
- const roleOptions = ref()
- const userRole = reactive({
- id: 0,
- username: '',
- nickname: '',
- roleIds: []
- })
- const handleRole = async (row: UserApi.UserVO) => {
- userRole.id = row.id
- userRole.username = row.username
- userRole.nickname = row.nickname
- // 获得角色拥有的权限集合
- const roles = await listUserRolesApi(row.id)
- userRole.roleIds = roles
- // 获取角色列表
- const roleOpt = await listSimpleRolesApi()
- roleOptions.value = roleOpt
- roleDialogVisible.value = true
- }
- // 提交
- const submitRole = async () => {
- const data = ref<PermissionAssignUserRoleReqVO>({
- userId: userRole.id,
- roleIds: userRole.roleIds
- })
- await aassignUserRoleApi(data.value)
- message.success(t('common.updateSuccess'))
- roleDialogVisible.value = false
- }
- // ========== 导入相关 ==========
- const importDialogVisible = ref(false)
- const uploadDisabled = ref(false)
- const importDialogTitle = ref('用户导入')
- const updateSupport = ref(0)
- let updateUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import'
- const uploadHeaders = ref()
- // 下载导入模版
- const handleImportTemp = async () => {
- const res = await UserApi.importUserTemplateApi()
- download.excel(res, '用户导入模版.xls')
- }
- // 文件上传之前判断
- const beforeExcelUpload = (file: UploadRawFile) => {
- const isExcel =
- file.type === 'application/vnd.ms-excel' ||
- file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- const isLt5M = file.size / 1024 / 1024 < 5
- if (!isExcel) message.error('上传文件只能是 xls / xlsx 格式!')
- if (!isLt5M) message.error('上传文件大小不能超过 5MB!')
- return isExcel && isLt5M
- }
- // 文件上传
- const uploadRef = ref<UploadInstance>()
- const submitFileForm = () => {
- uploadHeaders.value = {
- Authorization: 'Bearer ' + getAccessToken(),
- 'tenant-id': getTenantId()
- }
- uploadDisabled.value = true
- uploadRef.value!.submit()
- }
- // 文件上传成功
- const handleFileSuccess = async (response: any): Promise<void> => {
- if (response.code !== 0) {
- message.error(response.msg)
- return
- }
- importDialogVisible.value = false
- uploadDisabled.value = false
- const data = response.data
- let text = '上传成功数量:' + data.createUsernames.length + ';'
- for (let username of data.createUsernames) {
- text += '< ' + username + ' >'
- }
- text += '更新成功数量:' + data.updateUsernames.length + ';'
- for (const username of data.updateUsernames) {
- text += '< ' + username + ' >'
- }
- text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';'
- for (const username in data.failureUsernames) {
- text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
- }
- message.alert(text)
- await getList(xGrid)
- }
- // 文件数超出提示
- const handleExceed = (): void => {
- message.error('最多只能上传一个文件!')
- }
- // 上传错误提示
- const excelUploadError = (): void => {
- message.error('导入数据失败,请您重新上传!')
- }
- // ========== 初始化 ==========
- onMounted(async () => {
- await getPostOptions()
- await getTree()
- })
- </script>
- <style scoped>
- .user {
- height: 900px;
- max-height: 960px;
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- </style>
|