You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by le...@apache.org on 2022/01/21 08:05:32 UTC

[dolphinscheduler] branch dev updated: [Feature][UI Next] Security User Manage (#8133)

This is an automated email from the ASF dual-hosted git repository.

leonbao pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new e0dbf3e  [Feature][UI Next] Security User Manage (#8133)
e0dbf3e is described below

commit e0dbf3edc6200ed2abd1dffbcfaff8b53d7996f4
Author: lilyzhou <lj...@outlook.com>
AuthorDate: Fri Jan 21 16:04:05 2022 +0800

    [Feature][UI Next] Security User Manage (#8133)
---
 .../src/locales/modules/en_US.ts                   |  32 +++
 .../src/locales/modules/zh_CN.ts                   |  30 +++
 .../src/router/modules/security.ts                 |   4 +-
 .../src/service/modules/users/index.ts             |   8 +-
 dolphinscheduler-ui-next/src/utils/regex.ts        |   4 +-
 .../security/user-manage/components/use-modal.ts   | 277 +++++++++++++++++++++
 .../security/user-manage/components/user-modal.tsx | 133 ++++++++++
 .../src/views/security/user-manage/index.tsx       | 144 +++++++++++
 .../src/views/security/user-manage/use-table.tsx   | 212 ++++++++++++++++
 9 files changed, 837 insertions(+), 7 deletions(-)

diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
index f90db5d..ca0b64e 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
@@ -434,6 +434,38 @@ const security = {
     edit: 'Edit',
     delete: 'Delete',
     delete_confirm: 'Delete?'
+  },
+  user: {
+    user_manage: 'User Manage',
+    create_user: 'Create User',
+    update_user: 'Update User',
+    delete_user: 'Delete User',
+    delete_confirm: 'Are you sure to delete?',
+    delete_confirm_tip:
+      'Deleting user is a dangerous operation,please be careful',
+    index: 'Index',
+    username: 'Username',
+    username_exists: 'The username already exists',
+    username_rule_msg: 'Please enter username',
+    user_password: 'Please enter password',
+    user_password_rule_msg:
+      'Please enter a password containing letters and numbers with a length between 6 and 20',
+    user_type: 'User Type',
+    tenant_code: 'Tenant',
+    tenant_id_rule_msg: 'Please select tenant',
+    queue: 'Queue',
+    email: 'Email',
+    email_rule_msg: 'Please enter valid email',
+    phone: 'Phone',
+    phone_rule_msg: 'Please enter valid phone number',
+    state: 'State',
+    create_time: 'Create Time',
+    update_time: 'Update Time',
+    operation: 'Operation',
+    edit: 'Edit',
+    delete: 'Delete',
+    save_error_msg: 'Failed to save, please retry',
+    delete_error_msg: 'Failed to delete, please retry'
   }
 }
 
diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
index 1ee6f60..0b82718 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
@@ -433,6 +433,36 @@ const security = {
     edit: '编辑',
     delete: '删除',
     delete_confirm: '确定删除吗?'
+  },
+  user: {
+    user_manage: '用户管理',
+    create_user: '创建用户',
+    update_user: '更新用户',
+    delete_user: '删除用户',
+    delete_confirm: '确定删除吗?',
+    delete_confirm_tip: '删除用户属于危险操作,请谨慎操作!',
+    index: '序号',
+    username: '用户名',
+    username_exists: '用户名已存在',
+    username_rule_msg: '请输入用户名',
+    user_password: '密码',
+    user_password_rule_msg: '请输入包含字母和数字,长度在6~20之间的密码',
+    user_type: '用户类型',
+    tenant_code: '租户',
+    tenant_id_rule_msg: '请选择租户',
+    queue: '队列',
+    email: '邮件',
+    email_rule_msg: '请输入正确的邮箱',
+    phone: '手机',
+    phone_rule_msg: '请输入正确的手机号',
+    state: '状态',
+    create_time: '创建时间',
+    update_time: '更新时间',
+    operation: '操作',
+    edit: '编辑',
+    delete: '删除',
+    save_error_msg: '保存失败,请重试',
+    delete_error_msg: '删除失败,请重试'
   }
 }
 
diff --git a/dolphinscheduler-ui-next/src/router/modules/security.ts b/dolphinscheduler-ui-next/src/router/modules/security.ts
index 44774e4..b50e3fb 100644
--- a/dolphinscheduler-ui-next/src/router/modules/security.ts
+++ b/dolphinscheduler-ui-next/src/router/modules/security.ts
@@ -39,9 +39,9 @@ export default {
       }
     },
     {
-      path: '/security/users',
+      path: '/security/user-manage',
       name: 'users-manage',
-      component: components['home'],
+      component: components['user-manage'],
       meta: {
         title: '用户管理',
         showSide: true
diff --git a/dolphinscheduler-ui-next/src/service/modules/users/index.ts b/dolphinscheduler-ui-next/src/service/modules/users/index.ts
index 1e91571..7d56619 100644
--- a/dolphinscheduler-ui-next/src/service/modules/users/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/users/index.ts
@@ -65,7 +65,7 @@ export function createUser(data: UserReq): any {
   })
 }
 
-export function delUserById(data: IdReq): any {
+export function delUserById(data: IdReq) {
   return axios({
     url: '/users/delete',
     method: 'post',
@@ -135,7 +135,7 @@ export function listAll(params?: ListAllReq): any {
   })
 }
 
-export function queryUserList(params: ListReq): any {
+export function queryUserList(params: ListReq) {
   return axios({
     url: '/users/list-paging',
     method: 'get',
@@ -167,7 +167,7 @@ export function unauthorizedUser(params: AlertGroupIdReq): any {
   })
 }
 
-export function updateUser(data: IdReq & UserReq): any {
+export function updateUser(data: IdReq & UserReq) {
   return axios({
     url: '/users/update',
     method: 'post',
@@ -175,7 +175,7 @@ export function updateUser(data: IdReq & UserReq): any {
   })
 }
 
-export function verifyUserName(params: UserNameReq): any {
+export function verifyUserName(params: UserNameReq) {
   return axios({
     url: '/users/verify-user-name',
     method: 'get',
diff --git a/dolphinscheduler-ui-next/src/utils/regex.ts b/dolphinscheduler-ui-next/src/utils/regex.ts
index c660e8d..aa8715e 100644
--- a/dolphinscheduler-ui-next/src/utils/regex.ts
+++ b/dolphinscheduler-ui-next/src/utils/regex.ts
@@ -16,7 +16,9 @@
  */
 
 const regex = {
-  email: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ // support Chinese mailbox
+  email: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/, // support Chinese mailbox
+  phone: /^1\d{10}$/,
+  password: /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$/
 }
 
 export default regex
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts
new file mode 100644
index 0000000..e5f50ae
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ref, watch, computed, InjectionKey } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useMessage } from 'naive-ui'
+import { queryTenantList } from '@/service/modules/tenants'
+import { queryList } from '@/service/modules/queues'
+import {
+  createUser,
+  updateUser,
+  delUserById,
+  verifyUserName
+} from '@/service/modules/users'
+import regexUtils from '@/utils/regex'
+export type Mode = 'add' | 'edit' | 'delete'
+
+export type UserModalSharedStateType = ReturnType<
+  typeof useSharedUserModalState
+> & {
+  onSuccess?: (mode: Mode) => void
+}
+
+export const UserModalSharedStateKey: InjectionKey<UserModalSharedStateType> =
+  Symbol()
+
+export function useSharedUserModalState() {
+  return {
+    show: ref(false),
+    mode: ref<Mode>('add'),
+    user: ref()
+  }
+}
+
+export function useModal({
+  onSuccess,
+  show,
+  mode,
+  user
+}: UserModalSharedStateType) {
+  const message = useMessage()
+  const { t } = useI18n()
+  const formRef = ref()
+  const formValues = ref({
+    userName: '',
+    userPassword: '',
+    tenantId: 0,
+    email: '',
+    queue: '',
+    phone: '',
+    state: 1
+  })
+  const tenants = ref<any[]>([])
+  const queues = ref<any[]>([])
+  const optionsLoading = ref(false)
+  const confirmLoading = ref(false)
+
+  const formRules = computed(() => {
+    return {
+      userName: {
+        required: true,
+        message: t('security.user.username_rule_msg'),
+        trigger: 'blur'
+      },
+      userPassword: {
+        required: mode.value === 'add',
+        validator(rule: any, value?: string) {
+          if (mode.value !== 'add' && !value) {
+            return true
+          }
+          const msg = t('security.user.user_password_rule_msg')
+          if (!value || !regexUtils.password.test(value)) {
+            return new Error(msg)
+          }
+          return true
+        },
+        trigger: ['blur', 'input']
+      },
+      tenantId: {
+        required: true,
+        validator(rule: any, value?: number) {
+          const msg = t('security.user.tenant_id_rule_msg')
+          if (typeof value === 'number') {
+            return true
+          }
+          return new Error(msg)
+        },
+        trigger: 'blur'
+      },
+      email: {
+        required: true,
+        validator(rule: any, value?: string) {
+          const msg = t('security.user.email_rule_msg')
+          if (!value || !regexUtils.email.test(value)) {
+            return new Error(msg)
+          }
+          return true
+        },
+        trigger: ['blur', 'input']
+      },
+      phone: {
+        validator(rule: any, value?: string) {
+          const msg = t('security.user.phone_rule_msg')
+          if (value && !regexUtils.phone.test(value)) {
+            return new Error(msg)
+          }
+          return true
+        },
+        trigger: ['blur', 'input']
+      }
+    }
+  })
+
+  const titleMap: Record<Mode, string> = {
+    add: t('security.user.create_user'),
+    edit: t('security.user.update_user'),
+    delete: t('security.user.delete_user')
+  }
+
+  const setFormValues = () => {
+    const defaultValues = {
+      userName: '',
+      userPassword: '',
+      tenantId: tenants.value[0]?.value,
+      email: '',
+      queue: queues.value[0]?.value,
+      phone: '',
+      state: 1
+    }
+    if (!user.value) {
+      formValues.value = defaultValues
+    } else {
+      const v: any = {}
+      Object.keys(defaultValues).map((k) => {
+        v[k] = user.value[k]
+      })
+      v.userPassword = ''
+      formValues.value = v
+    }
+  }
+
+  const prepareOptions = async () => {
+    optionsLoading.value = true
+    Promise.all([queryTenantList(), queryList()])
+      .then((res) => {
+        tenants.value =
+          res[0]?.map((d: any) => ({
+            label: d.tenantCode,
+            value: d.id
+          })) || []
+        queues.value =
+          res[1]?.map((d: any) => ({
+            label: d.queueName,
+            value: d.queue
+          })) || []
+      })
+      .finally(() => {
+        optionsLoading.value = false
+      })
+  }
+
+  const onDelete = () => {
+    confirmLoading.value = true
+    delUserById({ id: user.value.id })
+      .then(
+        () => {
+          onSuccess?.(mode.value)
+          onModalCancel()
+        },
+        () => {
+          message.error(t('security.user.delete_error_msg'))
+        }
+      )
+      .finally(() => {
+        confirmLoading.value = false
+      })
+  }
+
+  const onCreateUser = () => {
+    confirmLoading.value = true
+    verifyUserName({ userName: formValues.value.userName })
+      .then(
+        () => createUser(formValues.value),
+        (error) => {
+          if (`${error.message}`.includes('exists')) {
+            message.error(t('security.user.username_exists'))
+          }
+          return false
+        }
+      )
+      .then(
+        (res) => {
+          if (res) {
+            onSuccess?.(mode.value)
+            onModalCancel()
+          }
+        },
+        () => {
+          message.error(t('security.user.save_error_msg'))
+        }
+      )
+      .finally(() => {
+        confirmLoading.value = false
+      })
+  }
+
+  const onUpdateUser = () => {
+    confirmLoading.value = true
+    updateUser({ id: user.value.id, ...formValues.value })
+      .then(
+        () => {
+          onSuccess?.(mode.value)
+          onModalCancel()
+        },
+        () => {
+          message.error(t('security.user.save_error_msg'))
+        }
+      )
+      .finally(() => {
+        confirmLoading.value = false
+      })
+  }
+
+  const onConfirm = () => {
+    if (mode.value === 'delete') {
+      onDelete()
+    } else {
+      formRef.value.validate((errors: any) => {
+        if (!errors) {
+          user.value ? onUpdateUser() : onCreateUser()
+        }
+      })
+    }
+  }
+
+  const onModalCancel = () => {
+    show.value = false
+  }
+
+  watch([show, mode], () => {
+    show.value && mode.value !== 'delete' && prepareOptions()
+  })
+
+  watch([queues, tenants, user], () => {
+    setFormValues()
+  })
+
+  return {
+    show,
+    mode,
+    user,
+    titleMap,
+    onModalCancel,
+    formRef,
+    formValues,
+    formRules,
+    tenants,
+    queues,
+    optionsLoading,
+    onConfirm,
+    confirmLoading
+  }
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx
new file mode 100644
index 0000000..d23a1e2
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { defineComponent, inject } from 'vue'
+import { useI18n } from 'vue-i18n'
+import {
+  NInput,
+  NForm,
+  NFormItem,
+  NSelect,
+  NRadio,
+  NRadioGroup,
+  NSpace,
+  NAlert
+} from 'naive-ui'
+
+import Modal from '@/components/modal'
+import {
+  useModal,
+  useSharedUserModalState,
+  UserModalSharedStateKey
+} from './use-modal'
+
+export const UserModal = defineComponent({
+  name: 'user-modal',
+  setup() {
+    const { t } = useI18n()
+    const sharedState =
+      inject(UserModalSharedStateKey) || useSharedUserModalState()
+    const modalState = useModal(sharedState)
+
+    return {
+      t,
+      ...modalState
+    }
+  },
+  render() {
+    const { t } = this
+    return (
+      <Modal
+        show={this.show}
+        title={this.titleMap?.[this.mode || 'add']}
+        onCancel={this.onModalCancel}
+        confirmLoading={this.confirmLoading}
+        onConfirm={this.onConfirm}
+      >
+        {{
+          default: () => {
+            if (this.mode === 'delete') {
+              return (
+                <NAlert type='error' title={t('security.user.delete_confirm')}>
+                  {t('security.user.delete_confirm_tip')}
+                </NAlert>
+              )
+            }
+            return (
+              <NForm
+                ref='formRef'
+                model={this.formValues}
+                rules={this.formRules}
+                labelPlacement='left'
+                labelAlign='left'
+                labelWidth={80}
+              >
+                <NFormItem label={t('security.user.username')} path='userName'>
+                  <NInput
+                    inputProps={{ autocomplete: 'off' }}
+                    v-model:value={this.formValues.userName}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('security.user.user_password')}
+                  path='userPassword'
+                >
+                  <NInput
+                    inputProps={{ autocomplete: 'off' }}
+                    type='password'
+                    v-model:value={this.formValues.userPassword}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('security.user.tenant_code')}
+                  path='tenantId'
+                >
+                  <NSelect
+                    options={this.tenants}
+                    v-model:value={this.formValues.tenantId}
+                  />
+                </NFormItem>
+                <NFormItem label={t('security.user.queue')} path='queue'>
+                  <NSelect
+                    options={this.queues}
+                    v-model:value={this.formValues.queue}
+                  />
+                </NFormItem>
+                <NFormItem label={t('security.user.email')} path='email'>
+                  <NInput v-model:value={this.formValues.email} />
+                </NFormItem>
+                <NFormItem label={t('security.user.phone')} path='phone'>
+                  <NInput v-model:value={this.formValues.phone} />
+                </NFormItem>
+                <NFormItem label={t('security.user.state')} path='state'>
+                  <NRadioGroup v-model:value={this.formValues.state}>
+                    <NSpace>
+                      <NRadio value={1}>启用</NRadio>
+                      <NRadio value={0}>停用</NRadio>
+                    </NSpace>
+                  </NRadioGroup>
+                </NFormItem>
+              </NForm>
+            )
+          }
+        }}
+      </Modal>
+    )
+  }
+})
+
+export default UserModal
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
new file mode 100644
index 0000000..018c77f
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { defineComponent, provide } from 'vue'
+import {
+  NCard,
+  NButton,
+  NInputGroup,
+  NInput,
+  NIcon,
+  NSpace,
+  NGrid,
+  NGridItem,
+  NDataTable,
+  NPagination,
+  NSkeleton
+} from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import { SearchOutlined } from '@vicons/antd'
+import { useTable } from './use-table'
+import UserModal from './components/user-modal'
+import {
+  useSharedUserModalState,
+  UserModalSharedStateKey,
+  Mode
+} from './components/use-modal'
+
+const UsersManage = defineComponent({
+  name: 'user-manage',
+  setup() {
+    const { t } = useI18n()
+    const { show, mode, user } = useSharedUserModalState()
+    const tableState = useTable({
+      onEdit: (u) => {
+        show.value = true
+        mode.value = 'edit'
+        user.value = u
+      },
+      onDelete: (u) => {
+        show.value = true
+        mode.value = 'delete'
+        user.value = u
+      }
+    })
+
+    const onSuccess = (mode: Mode) => {
+      if (mode === 'add') {
+        tableState.resetPage()
+      }
+      tableState.getUserList()
+    }
+
+    const onAddUser = () => {
+      show.value = true
+      mode.value = 'add'
+      user.value = undefined
+    }
+
+    provide(UserModalSharedStateKey, { show, mode, user, onSuccess })
+
+    return {
+      t,
+      onAddUser,
+      ...tableState
+    }
+  },
+  render() {
+    const { t, onSearchValOk, onSearchValClear, userListLoading } = this
+    return (
+      <>
+        <NGrid cols={1} yGap={16}>
+          <NGridItem>
+            <NCard>
+              <NSpace justify='space-between'>
+                <NButton onClick={this.onAddUser} type='primary'>
+                  {t('security.user.create_user')}
+                </NButton>
+                <NInputGroup>
+                  <NInput
+                    v-model:value={this.searchInputVal}
+                    clearable
+                    onClear={onSearchValClear}
+                    onKeyup={(e) => {
+                      if (e.key === 'Enter') {
+                        onSearchValOk()
+                      }
+                    }}
+                  />
+                  <NButton type='primary' onClick={onSearchValOk}>
+                    <NIcon>
+                      <SearchOutlined />
+                    </NIcon>
+                  </NButton>
+                </NInputGroup>
+              </NSpace>
+            </NCard>
+          </NGridItem>
+          <NGridItem>
+            <NCard>
+              {userListLoading ? (
+                <NSkeleton text repeat={6}></NSkeleton>
+              ) : (
+                <NSpace v-show={!userListLoading} vertical size={20}>
+                  <NDataTable
+                    columns={this.columns}
+                    data={this.userList}
+                    scrollX={this.scrollX}
+                    bordered={false}
+                  />
+                  <NSpace justify='center'>
+                    <NPagination
+                      v-model:page={this.page}
+                      v-model:page-size={this.pageSize}
+                      pageCount={this.pageCount}
+                      pageSizes={this.pageSizes}
+                      showSizePicker
+                    />
+                  </NSpace>
+                </NSpace>
+              )}
+            </NCard>
+          </NGridItem>
+        </NGrid>
+        <UserModal />
+      </>
+    )
+  }
+})
+
+export default UsersManage
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx
new file mode 100644
index 0000000..10b334b
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ref, watch, onBeforeMount } from 'vue'
+import { NSpace, NTooltip, NButton, NIcon, NTag } from 'naive-ui'
+import { EditOutlined, DeleteOutlined } from '@vicons/antd'
+import { queryUserList } from '@/service/modules/users'
+import { useI18n } from 'vue-i18n'
+
+type UseTableProps = {
+  onEdit: (user: any) => void
+  onDelete: (user: any) => void
+}
+
+function useColumns({ onEdit, onDelete }: UseTableProps) {
+  const { t } = useI18n()
+  const columns: any[] = [
+    {
+      title: t('security.user.index'),
+      key: 'index',
+      width: 80,
+      render: (rowData: any, rowIndex: number) => rowIndex + 1
+    },
+    {
+      title: t('security.user.username'),
+      key: 'userName'
+    },
+    {
+      title: t('security.user.tenant_code'),
+      key: 'tenantCode'
+    },
+    {
+      title: t('security.user.queue'),
+      key: 'queue'
+    },
+    {
+      title: t('security.user.email'),
+      key: 'email'
+    },
+    {
+      title: t('security.user.phone'),
+      key: 'phone'
+    },
+    {
+      title: t('security.user.state'),
+      key: 'state',
+      render: (rowData: any, rowIndex: number) => {
+        return rowData.state === 1 ? (
+          <NTag type='success'>启用</NTag>
+        ) : (
+          <NTag type='error'>停用</NTag>
+        )
+      }
+    },
+    {
+      title: t('security.user.create_time'),
+      key: 'createTime',
+      width: 200
+    },
+    {
+      title: t('security.user.update_time'),
+      key: 'updateTime',
+      width: 200
+    },
+    {
+      title: t('security.user.operation'),
+      key: 'operation',
+      fixed: 'right',
+      width: 120,
+      render: (rowData: any, rowIndex: number) => {
+        return (
+          <NSpace>
+            <NTooltip trigger='hover'>
+              {{
+                trigger: () => (
+                  <NButton
+                    circle
+                    type='info'
+                    size='small'
+                    onClick={() => {
+                      onEdit(rowData)
+                    }}
+                  >
+                    {{
+                      icon: () => (
+                        <NIcon>
+                          <EditOutlined />
+                        </NIcon>
+                      )
+                    }}
+                  </NButton>
+                ),
+                default: () => t('security.user.edit')
+              }}
+            </NTooltip>
+            <NTooltip trigger='hover'>
+              {{
+                trigger: () => (
+                  <NButton
+                    circle
+                    type='error'
+                    size='small'
+                    onClick={() => {
+                      onDelete(rowData)
+                    }}
+                  >
+                    {{
+                      icon: () => (
+                        <NIcon>
+                          <DeleteOutlined />
+                        </NIcon>
+                      )
+                    }}
+                  </NButton>
+                ),
+                default: () => t('security.user.delete')
+              }}
+            </NTooltip>
+          </NSpace>
+        )
+      }
+    }
+  ].map((d: any) => ({ ...d, width: d.width || 160 }))
+
+  const scrollX = columns.reduce((p, c) => p + c.width, 0)
+
+  return {
+    columns,
+    scrollX
+  }
+}
+
+export function useTable(props: UseTableProps) {
+  const page = ref(1)
+  const pageCount = ref(0)
+  const pageSize = ref(10)
+  const searchInputVal = ref()
+  const searchVal = ref('')
+  const pageSizes = [10, 30, 50]
+  const userListLoading = ref(false)
+  const userList = ref([])
+  const { columns, scrollX } = useColumns(props)
+
+  const getUserList = () => {
+    userListLoading.value = true
+    queryUserList({
+      pageNo: page.value,
+      pageSize: pageSize.value,
+      searchVal: searchVal.value
+    })
+      .then((res: any) => {
+        userList.value = res?.totalList || []
+        pageCount.value = res?.totalPage || 0
+      })
+      .finally(() => {
+        userListLoading.value = false
+      })
+  }
+
+  const resetPage = () => {
+    page.value = 1
+  }
+
+  const onSearchValOk = () => {
+    resetPage()
+    searchVal.value = searchInputVal.value
+  }
+
+  const onSearchValClear = () => {
+    resetPage()
+    searchVal.value = ''
+  }
+
+  onBeforeMount(() => {
+    getUserList()
+  })
+
+  watch([page, pageSize, searchVal], () => {
+    getUserList()
+  })
+
+  return {
+    userList,
+    userListLoading,
+    getUserList,
+    page,
+    pageCount,
+    pageSize,
+    searchVal,
+    searchInputVal,
+    pageSizes,
+    columns,
+    scrollX,
+    onSearchValOk,
+    onSearchValClear,
+    resetPage
+  }
+}