From ca1b6df5ca3a48ef17f347135c218f3d1e15c8bc Mon Sep 17 00:00:00 2001
From: fengjingtao <fessor@139.com>
Date: Wed, 29 Mar 2023 20:21:58 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86rev?=
 =?UTF-8?q?iew=E4=BB=A5=E5=90=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .vscode/settings.json            |  22 +-
 src/types/auto-components.d.ts   |  11 -
 src/views/system/user/index0.vue | 576 +++++++++++++++++++++++++++++++
 3 files changed, 597 insertions(+), 12 deletions(-)
 create mode 100644 src/views/system/user/index0.vue

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 38cc3052..3e9f1774 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -40,5 +40,25 @@
   "i18n-ally.displayLanguage": "zh-CN",
   "i18n-ally.enabledFrameworks": ["vue", "react"],
   "god.tsconfig": "./tsconfig.json",
-  "vue-i18n.i18nPaths": "src/locales"
+  "vue-i18n.i18nPaths": "src/locales",
+  "workbench.colorCustomizations": {
+    "activityBar.activeBackground": "#65c89b",
+    "activityBar.background": "#65c89b",
+    "activityBar.foreground": "#15202b",
+    "activityBar.inactiveForeground": "#15202b99",
+    "activityBarBadge.background": "#945bc4",
+    "activityBarBadge.foreground": "#e7e7e7",
+    "commandCenter.border": "#15202b99",
+    "sash.hoverBorder": "#65c89b",
+    "statusBar.background": "#42b883",
+    "statusBar.foreground": "#15202b",
+    "statusBarItem.hoverBackground": "#359268",
+    "statusBarItem.remoteBackground": "#42b883",
+    "statusBarItem.remoteForeground": "#15202b",
+    "titleBar.activeBackground": "#42b883",
+    "titleBar.activeForeground": "#15202b",
+    "titleBar.inactiveBackground": "#42b88399",
+    "titleBar.inactiveForeground": "#15202b99"
+  },
+  "peacock.color": "#42b883"
 }
diff --git a/src/types/auto-components.d.ts b/src/types/auto-components.d.ts
index be71517c..3607ce04 100644
--- a/src/types/auto-components.d.ts
+++ b/src/types/auto-components.d.ts
@@ -23,8 +23,6 @@ declare module '@vue/runtime-core' {
     DictTag: typeof import('./../components/DictTag/src/DictTag.vue')['default']
     Echart: typeof import('./../components/Echart/src/Echart.vue')['default']
     Editor: typeof import('./../components/Editor/src/Editor.vue')['default']
-    ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer']
-    ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
@@ -54,31 +52,22 @@ declare module '@vue/runtime-core' {
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
-    ElImage: typeof import('element-plus/es')['ElImage']
     ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
     ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
-    ElRadio: typeof import('element-plus/es')['ElRadio']
-    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
-    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
-    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
-    ElTableV2: typeof import('element-plus/es')['ElTableV2']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
-    ElTimeline: typeof import('element-plus/es')['ElTimeline']
-    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTransfer: typeof import('element-plus/es')['ElTransfer']
     ElTree: typeof import('element-plus/es')['ElTree']
diff --git a/src/views/system/user/index0.vue b/src/views/system/user/index0.vue
new file mode 100644
index 00000000..2f9ba9b0
--- /dev/null
+++ b/src/views/system/user/index0.vue
@@ -0,0 +1,576 @@
+<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-scrollbar height="650">
+        <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-scrollbar>
+    </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>
+      <!-- 列表 -->
+      <XTable @register="registerTable">
+        <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_default="{ 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)"
+          />
+          <el-dropdown
+            class="p-0.5"
+            v-hasPermi="[
+              'system:user:update-password',
+              'system:permission:assign-user-role',
+              'system:user:delete'
+            ]"
+          >
+            <XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item>
+                  <!-- 操作:重置密码 -->
+                  <XTextButton
+                    preIcon="ep:key"
+                    title="重置密码"
+                    v-hasPermi="['system:user:update-password']"
+                    @click="handleResetPwd(row)"
+                  />
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <!-- 操作:分配角色 -->
+                  <XTextButton
+                    preIcon="ep:key"
+                    title="分配角色"
+                    v-hasPermi="['system:permission:assign-user-role']"
+                    @click="handleRole(row)"
+                  />
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <!-- 操作:删除 -->
+                  <XTextButton
+                    preIcon="ep:delete"
+                    :title="t('action.del')"
+                    v-hasPermi="['system:user:delete']"
+                    @click="deleteData(row.id)"
+                  />
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </XTable>
+    </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="form">
+        <el-tree-select
+          node-key="id"
+          v-model="form['deptId']"
+          :props="defaultProps"
+          :data="deptOptions"
+          check-strictly
+        />
+      </template>
+      <template #postIds="form">
+        <el-select v-model="form['postIds']" multiple :placeholder="t('common.selectText')">
+          <el-option
+            v-for="item in postOptions"
+            :key="item.id"
+            :label="item.name"
+            :value="(item.id as unknown as number)"
+          />
+        </el-select>
+      </template>
+    </Form>
+    <!-- 对话框(详情) -->
+    <Descriptions
+      v-if="actionType === 'detail'"
+      :schema="allSchemas.detailSchema"
+      :data="detailData"
+    >
+      <template #deptId="{ row }">
+        <el-tag>{{ dataFormater(row.deptId) }}</el-tag>
+      </template>
+      <template #postIds="{ row }">
+        <template v-if="row.postIds !== ''">
+          <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>
+        <template v-else> </template>
+      </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="模板下载 :">
+        <XButton type="primary" prefix="ep:download" title="点击下载" @click="handleImportTemp()" />
+      </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 type { ElTree, UploadRawFile, UploadInstance } from 'element-plus'
+import { handleTree, defaultProps } from '@/utils/tree'
+import download from '@/utils/download'
+import { CommonStatusEnum } from '@/utils/constants'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import type { 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 queryParams = reactive({
+  deptId: null
+})
+// ========== 列表相关 ==========
+const tableTitle = ref('用户列表')
+// 列表相关的变量
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
+  allSchemas: allSchemas,
+  params: queryParams,
+  getListApi: UserApi.getUserPageApi,
+  deleteApi: UserApi.deleteUserApi,
+  exportListApi: UserApi.exportUserApi
+})
+// ========== 创建部门树结构 ==========
+const filterText = ref('')
+const deptOptions = ref<Tree[]>([]) // 树形结构
+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 reload()
+}
+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 postOptions = ref<PostVO[]>([]) //岗位列表
+
+// 获取岗位列表
+const getPostOptions = async () => {
+  const res = await listSimplePostsApi()
+  postOptions.value.push(...res)
+}
+const dataFormater = (val) => {
+  return deptFormater(deptOptions.value, val)
+}
+//部门回显
+const deptFormater = (ary, val: any) => {
+  var o = ''
+  if (ary && val) {
+    for (const v of ary) {
+      if (v.id == val) {
+        o = v.name
+        if (o) return o
+      } else if (v.children?.length) {
+        o = deptFormater(v.children, val)
+        if (o) return o
+      }
+    }
+    return o
+  } else {
+    return val
+  }
+}
+
+// 设置标题
+const setDialogTile = async (type: string) => {
+  dialogTitle.value = t('action.' + type)
+  actionType.value = type
+  dialogVisible.value = true
+}
+
+// 新增操作
+const handleCreate = async () => {
+  setDialogTile('create')
+  // 重置表单
+  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)
+  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 submitForm = async () => {
+  const elForm = unref(formRef)?.getElFormRef()
+  if (!elForm) return
+  elForm.validate(async (valid) => {
+    if (valid) {
+      // 提交请求
+      try {
+        const data = unref(formRef)?.formModel as UserApi.UserVO
+        if (actionType.value === 'create') {
+          loading.value = true
+          await UserApi.createUserApi(data)
+          message.success(t('common.createSuccess'))
+        } else {
+          loading.value = true
+          await UserApi.updateUserApi(data)
+          message.success(t('common.updateSuccess'))
+        }
+        dialogVisible.value = false
+      } finally {
+        // unref(formRef)?.setSchema(allSchemas.formSchema)
+        // 刷新列表
+        await reload()
+        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 reload()
+    })
+    .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
+}
+// ========== 导入相关 ==========
+// TODO @星语:这个要不要把导入用户,封装成一个小组件?可选哈
+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 reload()
+}
+// 文件数超出提示
+const handleExceed = (): void => {
+  message.error('最多只能上传一个文件!')
+}
+// 上传错误提示
+const excelUploadError = (): void => {
+  message.error('导入数据失败,请您重新上传!')
+}
+// ========== 初始化 ==========
+onMounted(async () => {
+  await getPostOptions()
+  await getTree()
+})
+</script>
+
+<style scoped>
+.user {
+  height: 780px;
+  max-height: 800px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>