فهرست منبع

feat: 新增后台用户功能

lanjianrong 4 سال پیش
والد
کامیت
4c7ed4b03f

+ 44 - 0
src/api/sys/manager.ts

@@ -0,0 +1,44 @@
+import { defHttp } from '/@/utils/http/axios'
+
+import { ErrorMessageMode } from '/@/utils/http/axios/types'
+import { BasicPageParams } from '/@/api/model/baseModel'
+enum Api {
+  GetManagerList = '/backstage/manager/list',
+  GetManagerInfo = '/backstage/manager',
+  SaveManagerStatus = '/backstage/manager/enable'
+}
+import { ManagerListGetResultModel, ManagerInfoGetResultModel } from './model/managerModel'
+/**
+ * @description 获取后台用户列表
+ */
+export function getManagerList(params: BasicPageParams) {
+  return defHttp.get<ManagerListGetResultModel>({
+    url: Api.GetManagerList,
+    params
+  })
+}
+
+/**
+ * @description 获取后台用户详情
+ */
+export function getManagerInfoById(params: { id: string }) {
+  return defHttp.get<ManagerInfoGetResultModel>({
+    url: Api.GetManagerInfo,
+    params
+  })
+}
+
+/**
+ * @description 后台用户启用/禁用
+ */
+export function toggleManagerStatus(params: { id: string; canLogin: 0 | 1 }, mode: ErrorMessageMode = 'modal') {
+  return defHttp.post(
+    {
+      url: Api.SaveManagerStatus,
+      params
+    },
+    {
+      errorMessageMode: mode
+    }
+  )
+}

+ 26 - 0
src/api/sys/model/managerModel.ts

@@ -0,0 +1,26 @@
+import { BasicFetchResult } from '/@/api/model/baseModel'
+
+export type ManagerItem = {
+  id: string
+  username: string
+  lastLogin: string
+  groupId: number
+  realName: string
+  telephone: string
+  loginIp: string
+  canLogin: number
+  office: string
+  category: string
+  email: string
+  qq: string
+  fixedphone: string
+  position: string
+  avatar: string
+  createTime: string
+}
+
+/**
+ * @description: Request list return value
+ */
+export type ManagerListGetResultModel = BasicFetchResult<ManagerItem[]>
+export type ManagerInfoGetResultModel = ManagerItem

+ 12 - 0
src/api/sys/model/projectModel.ts

@@ -63,6 +63,18 @@ export type ProjectAccountItem = {
   qualityPermission: string
   safePermission: string
 }
+
+export type AddProjectAccountParams = {
+  projectId: string
+  account: string
+  password: string
+  role: string
+  name: string
+  company: string
+  position: string
+  mobile: string
+  telephone: string
+}
 /**
  * @description: Request list return value
  */

+ 11 - 2
src/api/sys/project.ts

@@ -5,14 +5,16 @@ import {
   ProjectListItem,
   ProjectUpdateOrCreateParams,
   ProjectAccountParams,
-  ProjectAccountListResultModal
+  ProjectAccountListResultModal,
+  AddProjectAccountParams
 } from './model/projectModel'
 enum Api {
   GetProjectList = '/backstage/project/list',
   GetProjectDetail = '/backstage/project',
   UpdateProjectDetail = '/backstage/project/save',
   AddProject = '/backstage/project/add',
-  GetProjectAccount = '/backstage/account'
+  GetProjectAccount = '/backstage/account',
+  AddProjectAccount = '/backstage/account/create'
 }
 
 /**
@@ -64,3 +66,10 @@ export function getProjectAccount(params: ProjectAccountParams) {
     params
   })
 }
+
+export function addProjectAccount(params: AddProjectAccountParams) {
+  return defHttp.post({
+    url: Api.AddProjectAccount,
+    params
+  })
+}

+ 17 - 0
src/router/menus/modules/manager.ts

@@ -0,0 +1,17 @@
+import type { MenuModule } from '/@/router/types'
+// import { t } from '/@/hooks/web/useI18n'
+
+const menu: MenuModule = {
+  orderNo: 11,
+  menu: {
+    name: '后台用户',
+    path: '/manager',
+    children: [
+      {
+        path: 'list',
+        name: '用户列表'
+      }
+    ]
+  }
+}
+export default menu

+ 20 - 20
src/router/routes/index.ts

@@ -1,40 +1,40 @@
-import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
+import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types'
 
-import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
+import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic'
 
-import { mainOutRoutes } from './mainOut';
-import { PageEnum } from '/@/enums/pageEnum';
-import { t } from '/@/hooks/web/useI18n';
+import { mainOutRoutes } from './mainOut'
+import { PageEnum } from '/@/enums/pageEnum'
+import { t } from '/@/hooks/web/useI18n'
 
-const modules = import.meta.globEager('./modules/**/*.ts');
+const modules = import.meta.globEager('./modules/**/*.ts')
 
-const routeModuleList: AppRouteModule[] = [];
+const routeModuleList: AppRouteModule[] = []
 
-Object.keys(modules).forEach((key) => {
-  const mod = modules[key].default || {};
-  const modList = Array.isArray(mod) ? [...mod] : [mod];
-  routeModuleList.push(...modList);
-});
+Object.keys(modules).forEach(key => {
+  const mod = modules[key].default || {}
+  const modList = Array.isArray(mod) ? [...mod] : [mod]
+  routeModuleList.push(...modList)
+})
 
-export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
+export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList]
 
 export const RootRoute: AppRouteRecordRaw = {
   path: '/',
   name: 'Root',
   redirect: PageEnum.BASE_HOME,
   meta: {
-    title: 'Root',
-  },
-};
+    title: 'Root'
+  }
+}
 
 export const LoginRoute: AppRouteRecordRaw = {
   path: '/login',
   name: 'Login',
   component: () => import('/@/views/sys/login/Login.vue'),
   meta: {
-    title: t('routes.basic.login'),
-  },
-};
+    title: t('routes.basic.login')
+  }
+}
 
 // Basic routing without permission
-export const basicRoutes = [LoginRoute, RootRoute, ...mainOutRoutes, REDIRECT_ROUTE];
+export const basicRoutes = [LoginRoute, RootRoute, ...mainOutRoutes, REDIRECT_ROUTE]

+ 34 - 0
src/router/routes/modules/manager.ts

@@ -0,0 +1,34 @@
+import type { AppRouteModule } from '/@/router/types'
+
+import { LAYOUT } from '/@/router/constant'
+
+const manager: AppRouteModule = {
+  path: '/manager',
+  name: 'Manager',
+  component: LAYOUT,
+  redirect: '/manager/list',
+  meta: {
+    title: '后台用户',
+    icon: 'fa-solid:user-cog'
+  },
+  children: [
+    {
+      path: 'list',
+      name: 'ManagerList',
+      component: () => import('/@/views/manager/list/index.vue'),
+      meta: {
+        title: '用户列表'
+      }
+    },
+    {
+      path: 'detail/:id',
+      name: 'ManagerDetail',
+      component: () => import('/@/views/manager/detail/index.vue'),
+      meta: {
+        title: '用户详情'
+      }
+    }
+  ]
+}
+
+export default manager

+ 69 - 11
src/views/dashboard/project-detail/components/account.vue

@@ -1,30 +1,48 @@
 <template>
   <BasicTable @register="registerTable">
     <template #toolbar>
-      <div class="w-2/5 flex">
+      <div class="w-2/5 flex justify-end">
         <!-- <Search @search="value => handleSearch('search', value)" placeholder="项目名称/项目编号" /> -->
         <a-button @click="showModalFn" class="ml-3"><PlusOutlined />新增账号</a-button>
       </div>
     </template>
   </BasicTable>
   <BasicModal @register="registerModal" @ok="submitModal">
-    <BasicForm @register="registerForm" ref="formElRef" />
+    <BasicForm @register="registerForm" ref="formElRef">
+      <template #role="{ model, field }">
+        <span v-if="hasAdmin" class="text-red">已存在管理员</span>
+        <a-radio-group v-else v-model:value="model[field]">
+          <a-radio value="1">设为<Icon icon="clarity:administrator-solid" />管理员</a-radio>
+        </a-radio-group>
+      </template>
+    </BasicForm>
   </BasicModal>
 </template>
 <script lang="ts">
   import { computed, defineComponent, PropType, ref, watch } from 'vue'
   import { BasicTable, useTable } from '/@/components/Table'
   import { BasicModal, useModal } from '/@/components/Modal'
+  import { PlusOutlined } from '@ant-design/icons-vue'
   import { BasicForm, FormActionType, FormSchema, useForm } from '/@/components/Form/index'
   import { getTableColumns } from './tableData'
-  import { getProjectAccount } from '/@/api/sys/project'
+  import { getProjectAccount, addProjectAccount } from '/@/api/sys/project'
   import { useGo } from '/@/hooks/web/usePage'
+  import { propTypes } from '/@/utils/propTypes'
+  import { Radio } from 'ant-design-vue'
+  import { Icon } from '/@/components/Icon/index'
+  import { AddProjectAccountParams } from '/@/api/sys/model/projectModel'
+  import { useMessage } from '/@/hooks/web/useMessage'
+
   export default defineComponent({
     name: 'ProjectDetailAccount',
     components: {
       BasicTable,
       BasicModal,
-      BasicForm
+      BasicForm,
+      PlusOutlined,
+      ARadio: Radio,
+      ARadioGroup: Radio.Group,
+      Icon
     },
     props: {
       id: {
@@ -34,14 +52,16 @@
       activeId: {
         type: String as PropType<string>,
         default: '1'
-      }
+      },
+      hasAdmin: propTypes.bool
     },
     setup(props) {
       const formElRef = ref<Nullable<FormActionType>>(null)
-
+      const { createMessage } = useMessage()
       const go = useGo()
-      const [registerTable, { setTableData }] = useTable({
+      const [registerTable, { setTableData, reload }] = useTable({
         columns: getTableColumns(() => go('/acount/detail')),
+        showTableSetting: true,
         canResize: true
       })
       const fetchTableData = async (projectId: string) => {
@@ -78,6 +98,34 @@
           component: 'Input',
           label: '姓名',
           required: true
+        },
+        {
+          field: 'company',
+          component: 'Input',
+          label: '单位名称',
+          required: true
+        },
+        {
+          field: 'position',
+          component: 'Input',
+          label: '角色/职位',
+          required: true
+        },
+        {
+          field: 'mobile',
+          component: 'Input',
+          label: '手机'
+        },
+        {
+          field: 'telephone',
+          component: 'Input',
+          label: '电话'
+        },
+        {
+          field: 'role',
+          label: '特殊账号',
+          component: 'RadioGroup',
+          slot: 'role'
         }
       ])
       const [registerForm] = useForm({
@@ -89,18 +137,28 @@
       async function showModalFn() {
         openModal()
         setModalProps({
-          title: '新增项目',
+          title: '新增账号',
           okText: '确认添加'
         })
       }
 
       function submitModal() {
-        formElRef.value?.validate().then(() => {
-          // createProject(values)
+        formElRef.value?.validate().then((values: any) => {
+          try {
+            createProjectAccount({ ...values, projectId: props.id })
+          } finally {
+            reload()
+            openModal(false)
+            createMessage.success('新建账号成功')
+          }
         })
       }
 
-      return { registerTable, registerModal, registerForm, showModalFn, submitModal }
+      async function createProjectAccount(values: AddProjectAccountParams) {
+        await addProjectAccount(values)
+      }
+
+      return { formElRef, registerTable, registerModal, registerForm, showModalFn, submitModal }
     }
   })
 </script>

+ 1 - 1
src/views/dashboard/project-detail/components/tableData.tsx

@@ -7,7 +7,7 @@ export function getTableColumns(fn: (id: string) => void): BasicColumn[] {
       title: '账号',
       customRender: ({ text, record }) => (
         <div>
-          {record.isAdmin ? <Icon type="clarity:administrator-solid" /> : null}
+          {record.isAdmin ? <Icon icon="clarity:administrator-solid" /> : null}
           {text}
         </div>
       )

+ 13 - 3
src/views/dashboard/project-detail/index.vue

@@ -18,7 +18,7 @@
           <ProjectInfo v-if="projectInfo" :info="projectInfo" @updateInfo:info="updateInfo" />
         </a-tab-pane>
         <a-tab-pane key="2" tab="项目账号">
-          <ProjectAccount :active-id="activeKey" :id="projectInfo?.id" />
+          <ProjectAccount :active-id="activeKey" :id="projectInfo?.id" :has-admin="hasAdmin" />
         </a-tab-pane>
         <a-tab-pane key="3" tab="项目标段" />
         <a-tab-pane key="4" tab="办事处共享" />
@@ -28,7 +28,7 @@
   </PageWrapper>
 </template>
 <script lang="ts">
-  import { defineComponent, ref, watch } from 'vue'
+  import { computed, defineComponent, ref, watch } from 'vue'
   import { useRoute } from 'vue-router'
   import { Tabs } from 'ant-design-vue'
   import { PageWrapper } from '/@/components/Page'
@@ -51,6 +51,7 @@
       const activeKey = ref('1')
       const { prefixCls } = useDesign('project')
       const projectInfo = ref<Nullable<ProjectListItem>>(null)
+      const hasAdmin = computed(() => projectInfo.value?.userId !== 'xCi4xUL6uur0h7fVI--NeA')
       const route = useRoute()
       async function fetchProjectInfo(id: string) {
         const project = await getProjectById({ id })
@@ -73,7 +74,16 @@
         activeKey.value = key
       }
 
-      return { activeKey, projectInfo, projectStatusBgColorMap, projectStatusTextMap, prefixCls, updateInfo, tabChange }
+      return {
+        activeKey,
+        projectInfo,
+        projectStatusBgColorMap,
+        projectStatusTextMap,
+        prefixCls,
+        updateInfo,
+        tabChange,
+        hasAdmin
+      }
     }
   })
 </script>

+ 101 - 0
src/views/manager/detail/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <PageWrapper content-class="bg-white p-5">
+    <div class="m-3 flex">
+      <div class="w-170px text-right w-min-170px">用户名 (CLD):</div>
+      <div class="ml-4">{{ info?.username }}</div>
+    </div>
+    <div class="m-3" flex>
+      <div class="w-170px text-right w-min-170px">手机:</div>
+      <div class="ml-4">{{ info?.telephone }}</div>
+    </div>
+    <div class="m-3 flex">
+      <div class="w-170px text-right w-min-170px">电话:</div>
+      <div class="ml-4">{{ info?.fixedphone }}</div>
+    </div>
+    <div class="m-3 flex">
+      <div class="w-170px text-right w-min-170px">邮箱:</div>
+      <div class="ml-4">{{ info?.email }}</div>
+    </div>
+    <div class="m-3 flex">
+      <div class="w-170px text-right w-min-170px">QQ:</div>
+      <div class="ml-4">{{ info?.qq }}</div>
+    </div>
+    <div class="m-3 flex">
+      <div class="w-170px text-right w-min-170px">办事处:</div>
+      <div class="ml-4">{{ info?.category }}</div>
+    </div>
+    <div class="m-3 flex">
+      <div class="w-170px text-right w-min-170pxt">职位:</div>
+      <div class="ml-4">{{ info?.position }}</div>
+    </div>
+    <div class="m-3 flex" v-if="info">
+      <div class="w-170px text-right w-min-170px">登录使用:</div>
+      <a-radio-group v-model:value="info.canLogin" @change="onChange" class="ml-4">
+        <a-radio-button :value="1">{{ loginPassText }}</a-radio-button>
+        <a-radio-button :value="0">{{ loginForbidText }}</a-radio-button>
+      </a-radio-group>
+    </div>
+  </PageWrapper>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, ref, watch } from 'vue'
+  import { useRoute } from 'vue-router'
+  import { getManagerInfoById, toggleManagerStatus } from '/@/api/sys/manager'
+  import { PageWrapper } from '/@/components/Page'
+  import { ManagerInfoGetResultModel } from '/@/api/sys/model/managerModel'
+  import { Radio } from 'ant-design-vue'
+  import { RadioChangeEvent } from 'ant-design-vue/lib/radio'
+  import { useMessage } from '/@/hooks/web/useMessage'
+  export default defineComponent({
+    name: 'UserDetail',
+    components: {
+      PageWrapper,
+      ARadioButton: Radio.Button,
+      ARadioGroup: Radio.Group
+    },
+    props: {
+      id: {
+        type: String as PropType<string>,
+        default: ''
+      }
+    },
+    setup() {
+      const info = ref<Nullable<ManagerInfoGetResultModel>>(null)
+      const route = useRoute()
+      const { createMessage } = useMessage()
+      const loginPassText = computed(() => (info.value?.canLogin === 1 ? '已开启' : '开启'))
+      const loginForbidText = computed(() => (info.value?.canLogin === 0 ? '已静止' : '静止'))
+      async function fetchManagerInfo(id: string) {
+        const data = await getManagerInfoById({ id })
+        info.value = data
+      }
+      watch(
+        () => route.params.id,
+        async (newId: string) => {
+          if (newId) {
+            await fetchManagerInfo(newId)
+          }
+        },
+        {
+          immediate: true
+        }
+      )
+
+      async function submitManagerStatus(id: string, status: 0 | 1) {
+        await toggleManagerStatus({ id, canLogin: status })
+      }
+
+      watch(info, newVal => {
+        console.log(newVal)
+      })
+      function onChange(e: RadioChangeEvent) {
+        try {
+          submitManagerStatus(info.value?.id || '', e.target?.value || 0)
+        } finally {
+          createMessage.success(`已${e.target?.value === 1 ? '允许' : '禁止'} ${info.value?.username} 登录`)
+        }
+      }
+      return { info, onChange, loginPassText, loginForbidText }
+    }
+  })
+</script>

+ 25 - 0
src/views/manager/list/index.vue

@@ -0,0 +1,25 @@
+<template>
+  <BasicTable @register="registerTable" />
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue'
+  import { BasicTable, useTable } from '/@/components/Table'
+  import { getTableColumns } from './tableColumns'
+  import { useGo } from '/@/hooks/web/usePage'
+  import { getManagerList } from '/@/api/sys/manager'
+  export default defineComponent({
+    name: 'ManagerList',
+    components: {
+      BasicTable
+    },
+    setup() {
+      const go = useGo()
+      const [registerTable] = useTable({
+        columns: getTableColumns((id: string) => go(`/manager/detail/${id}`)),
+        canResize: false,
+        api: getManagerList
+      })
+      return { registerTable }
+    }
+  })
+</script>

+ 43 - 0
src/views/manager/list/tableColumns.tsx

@@ -0,0 +1,43 @@
+import Icon from '/@/components/Icon'
+import { BasicColumn } from '/@/components/Table/src/types/table'
+import { formatToDateTime } from '/@/utils/dateUtil'
+export function getTableColumns(fn: (id: string) => void): BasicColumn[] {
+  return [
+    // {
+    //   dataIndex: 'id',
+    //   title: 'Id'
+    // },
+    {
+      dataIndex: 'username',
+      title: '用户名 (CLD)'
+    },
+    {
+      dataIndex: 'category',
+      title: '办事处'
+    },
+    {
+      dataIndex: 'lastLogin',
+      title: '最后登录时间',
+      sortOrder: 'ascend',
+      customRender: ({ text }) => <span>{text ? formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss') : null}</span>
+    },
+    {
+      dataIndex: 'loginIp',
+      title: '最近登录IP'
+    },
+    {
+      dataIndex: 'groupId',
+      title: '用户组',
+      customRender: ({ text }) => <span>{text === 1 ? '超级管理员' : '管理员改'}</span>
+    },
+    {
+      dataIndex: 'opreate',
+      title: '操作',
+      customRender: ({ record }) => (
+        <span class="text-[#007BFF] hover:text-[#005BB8] cursor-pointer">
+          <Icon icon="fluent:edit-32-filled" onClick={() => fn(record.id)}></Icon>
+        </span>
+      )
+    }
+  ]
+}