Browse Source

用户管理

CzRger 1 week ago
parent
commit
a83a77c015

+ 10 - 0
src/api/modules/center/user.ts

@@ -0,0 +1,10 @@
+import { get, post, put, del } from '@/api/request'
+
+// 用户管理-分页
+export const userPage = (params) => get(`/user`, params, {})
+// 用户管理-新增
+export const userAdd = (params) => post(`/user`, params, {})
+// 用户管理-编辑
+export const userEdit = (params) => put(`/user`, params, {})
+// 用户管理-详情
+export const userDetail = (id) => get(`/user/${id}`, {}, {})

+ 1 - 0
src/stores/index.ts

@@ -3,3 +3,4 @@ export * from './modules/dialog'
 export * from './modules/menu'
 export * from './modules/workflow'
 export * from './modules/dictionary'
+export * from './modules/regex'

+ 32 - 1
src/stores/modules/dictionary.ts

@@ -13,6 +13,7 @@ import FileTxt from '@/assets/images/file-txt.png'
 import FileUnknown from '@/assets/images/file-unknown.png'
 // @ts-ignore
 import FileWord from '@/assets/images/file-word.png'
+import { tenantsPage } from '@/api/modules/center/tenant'
 
 const listToMap = ({
   list,
@@ -53,6 +54,12 @@ export const useDictionaryStore = defineStore('dictionary', {
       map: new Map(),
       objMap: new Map(),
     },
+    tenants: {
+      waiting: false,
+      list: [],
+      map: new Map(),
+      objMap: new Map(),
+    },
     shareConditions: [
       { label: '不开放', value: 'NO_SHARE' },
       { label: '全部开放', value: 'OPEN_SHARE' },
@@ -74,7 +81,7 @@ export const useDictionaryStore = defineStore('dictionary', {
       { label: '草稿', value: 0 },
       { label: '发布', value: 1 },
     ],
-    tenantStatus: [
+    trueFalseStatus: [
       { label: '启用', value: true },
       { label: '停用', value: false },
     ],
@@ -201,6 +208,30 @@ export const useDictionaryStore = defineStore('dictionary', {
           })
       }
     },
+    initTenants() {
+      if (!this.tenants.waiting) {
+        this.tenants.waiting = true
+        tenantsPage({
+          page: 1,
+          size: 9999,
+          enabled: true,
+        })
+          .then(({ data }: any) => {
+            const arr: any = data.content.map((v) => {
+              v.label = v.name
+              v.value = v.id
+              return v
+            })
+            this.tenants.list = arr
+            this.tenants.map = listToMap({ list: arr })
+            this.tenants.objMap = listToMap({ list: arr, isObj: true })
+          })
+          .catch(() => {})
+          .finally(() => {
+            this.tenants.waiting = false
+          })
+      }
+    },
     getFileIcon(name) {
       if (name.toLowerCase().includes('.txt')) {
         return FileTxt

+ 22 - 0
src/stores/modules/regex.ts

@@ -0,0 +1,22 @@
+import { defineStore } from 'pinia'
+
+export const useRegexStore = defineStore('regex', {
+  state: () => ({
+    //  11位手机号 + 带区号座机号 + 不带区号座机 + 空格
+    phoneCN: /^(1[3-9]\d{9}|\d{3,4}-\d{7,8}|\d{7,8}|\s+)$/,
+    //  邮箱
+    email:
+      /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
+    //  密码级别低(0):请输入8~16位密码
+    password0: /^.{8,16}$/,
+    //  密码级别中(1):请输入8~16位密码,要求同时包含大小写字母、数字
+    password1: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,16}$/,
+    //  密码级别高(2):请输入8~16位密码,要求同时包含大小写字母、数字、特殊字符
+    password2:
+      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}[\]:;"'<>,.?/]).{8,16}$/,
+    // ip
+    ip: /^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$/,
+  }),
+  getters: {},
+  actions: {},
+})

+ 15 - 0
src/utils/czr-util.ts

@@ -116,6 +116,9 @@ export const formatTableHeadFilters = (
 }
 
 export const YMDHms = (date: any) => {
+  if (!date) {
+    return ''
+  }
   const _date = new Date(date)
   const Y = `${_date.getFullYear()}`
   const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`
@@ -128,18 +131,27 @@ export const YMDHms = (date: any) => {
 }
 
 export const YM = (date: any, format = false) => {
+  if (!date) {
+    return ''
+  }
   const _date = new Date(date)
   const Y = `${_date.getFullYear()}`
   const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`
   return format ? `${Y}年${M}月` : `${Y}-${M}`
 }
 export const M = (date: any, format = false) => {
+  if (!date) {
+    return ''
+  }
   const _date = new Date(date)
   const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`
   return format ? `${M}月` : `${M}`
 }
 
 export const YMD = (date: any, format = false) => {
+  if (!date) {
+    return ''
+  }
   const _date = new Date(date)
   const Y = `${_date.getFullYear()}`
   const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`
@@ -148,6 +160,9 @@ export const YMD = (date: any, format = false) => {
 }
 
 export const Hms = (date: any, format = false) => {
+  if (!date) {
+    return ''
+  }
   const _date = new Date(date)
   const H = `${_date.getHours() < 10 ? `0${_date.getHours()}` : _date.getHours()}`
   const m = `${_date.getMinutes() < 10 ? `0${_date.getMinutes()}` : _date.getMinutes()}`

+ 1 - 1
src/views/manage/center/tenant/detail.vue

@@ -87,7 +87,7 @@
       :show="state.invite.show"
       title="新增管理员"
       @onClose="state.invite.show = false"
-      width="40rem"
+      width="62.5rem"
       :show-submit="false"
       :show-close="false"
     >

+ 1 - 1
src/views/manage/center/tenant/index.vue

@@ -37,7 +37,7 @@
             label-width="0px"
             v-model:param="state.query.form.enabled"
             link="select"
-            :options="DictionaryStore.tenantStatus"
+            :options="DictionaryStore.trueFalseStatus"
             placeholder="租户状态"
           />
           <CzrFormColumn

+ 211 - 0
src/views/manage/center/user/detail.vue

@@ -0,0 +1,211 @@
+<template>
+  <CzrDialog
+    :show="show"
+    :title="titleCpt"
+    @onClose="$emit('update:show', false)"
+    @onSubmit="onSubmit"
+    width="62.5rem"
+    height="auto"
+    max-height="90%"
+    :loading="state.loading"
+  >
+    <div class="bm-form">
+      <CzrForm ref="ref_form" :form-view="isViewCpt">
+        <CzrFormColumn
+          required
+          :span="12"
+          label="账号"
+          v-model:param="state.form.loginId"
+          :disabled="!!state.form.id"
+          :min-length="2"
+          :max-length="50"
+        />
+        <CzrFormColumn
+          required
+          :span="12"
+          label="密码"
+          v-model:param="state.form.pass"
+          :rules="[
+            {
+              handle: (val) => RegexStore.password2.test(val),
+              message:
+                '请输入8~16位密码,要求同时包含大小写字母、数字、特殊字符',
+            },
+          ]"
+          :disabled="!!state.form.id"
+        />
+        <CzrFormColumn
+          required
+          :span="12"
+          label="用户名"
+          v-model:param="state.form.name"
+          :min-length="2"
+          :max-length="20"
+        />
+        <template v-if="state.form.id">
+          <CzrFormColumn
+            required
+            :span="12"
+            label="状态"
+            v-model:param="state.form.enabled"
+            link="switch"
+          />
+          <CzrFormColumn
+            :span="12"
+            label="角色"
+            v-model:param="state.form.roles"
+            link="select"
+            :options="[]"
+          />
+          <CzrFormColumn
+            :span="24"
+            label="备注"
+            v-model:param="state.form.remark"
+            type="textarea"
+            :rows="4"
+          />
+        </template>
+      </CzrForm>
+    </div>
+  </CzrDialog>
+</template>
+
+<script setup lang="ts">
+import {
+  computed,
+  getCurrentInstance,
+  nextTick,
+  reactive,
+  ref,
+  watch,
+} from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  useAppStore,
+  useDialogStore,
+  useDictionaryStore,
+  useRegexStore,
+} from '@/stores'
+import { useRouter } from 'vue-router'
+import { CopyDocument } from '@element-plus/icons-vue'
+import { copy } from '@/utils/czr-util'
+import { userAdd, userDetail } from '@/api/modules/center/user'
+
+const router = useRouter()
+const AppStore = useAppStore()
+const DictionaryStore = useDictionaryStore()
+const DialogStore = useDialogStore()
+const RegexStore = useRegexStore()
+const emit = defineEmits(['update:show', 'refresh'])
+const { proxy } = getCurrentInstance()
+const props = defineProps({
+  show: { default: false },
+  transfer: <any>{},
+})
+const state: any = reactive({
+  loading: false,
+  form: {},
+})
+const ref_form = ref()
+const titleCpt = computed(() => {
+  let t = '用户'
+  switch (props.transfer.mode) {
+    case 'add':
+      t = '新增账号'
+      break
+    case 'edit':
+      t = '编辑' + t
+      break
+    case 'view':
+      t = '查看' + t
+      break
+  }
+  return t
+})
+const isViewCpt = computed(() => props.transfer?.mode === 'view')
+watch(
+  () => props.show,
+  (n) => {
+    if (n) {
+      initDictionary()
+      state.form = {
+        enabled: true,
+      }
+      if (props.transfer.mode !== 'add') {
+        initData()
+      }
+      nextTick(() => {
+        ref_form.value.reset()
+      })
+    }
+  },
+)
+const initDictionary = () => {}
+const initData = () => {
+  state.loading = true
+  userDetail(props.transfer.id)
+    .then(({ data }: any) => {
+      state.form = data
+    })
+    .catch(() => {})
+    .finally(() => {
+      state.loading = false
+    })
+}
+const onSubmit = () => {
+  ref_form.value
+    .submit()
+    .then(() => {
+      DialogStore.confirm({
+        content: `请确认是否提交?`,
+        onSubmit: () => {
+          state.loading = true
+          if (props.transfer.mode === 'add') {
+            userAdd(state.form)
+              .then(({ data }: any) => {
+                ElMessage.success(`${titleCpt.value}成功!`)
+                emit('update:show', false)
+                emit('refresh')
+              })
+              .catch(() => {})
+              .finally(() => {
+                state.loading = false
+              })
+          } else {
+            // datasetsUpdate({
+            //   ...state.form,
+            //   ...ref_modelConfig.value.getData(),
+            //   tenantId: AppStore.tenantInfo?.id,
+            // })
+            //   .then(({ data }: any) => {
+            //     ElMessage.success(`${titleCpt.value}成功!`)
+            //     emit('update:show', false)
+            //     emit('refresh')
+            //   })
+            //   .catch(() => {})
+            //   .finally(() => {
+            //     state.loading = false
+            //   })
+          }
+        },
+      })
+    })
+    .catch((e) => {
+      ElMessage({
+        message: e[0].message,
+        grouping: true,
+        type: 'warning',
+      })
+    })
+}
+const onInvite = () => {
+  state.invite.url = 'https://cn.element-plus.org/zh-CN/component/input.html'
+  state.invite.show = true
+}
+const onCopy = (str) => {
+  copy(str)
+  ElMessage.success('复制成功!')
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 358 - 4
src/views/manage/center/user/index.vue

@@ -1,9 +1,363 @@
-<template>用户管理</template>
+<template>
+  <CzrContent
+    v-model:tableHead="state.query.head"
+    @handleReset="onReset"
+    @handleSearch="onSearch"
+  >
+    <template #tableTitle>
+      <div class="flex gap-2.5"></div>
+    </template>
+    <template #buttons>
+      <div class="flex items-center gap-2.5">
+        <CzrForm class="bm-filter" label-width="0px" @handleEnter="onSearch">
+          <CzrFormColumn
+            width="6.68rem"
+            class="__czr-table-form-column"
+            :span="24"
+            label-width="0px"
+            v-model:param="state.query.form.tenantId"
+            link="select"
+            :options="DictionaryStore.tenants.list"
+            placeholder="全部租戶"
+          />
+          <CzrFormColumn
+            width="19rem"
+            class="__czr-table-form-column"
+            :span="24"
+            label-width="0px"
+            v-model:param="state.query.form.loginTime"
+            link="date"
+            type="daterange"
+            placeholder="最后登录时间"
+          />
+          <CzrFormColumn
+            width="19rem"
+            class="__czr-table-form-column"
+            :span="24"
+            label-width="0px"
+            v-model:param="state.query.form.createTime"
+            link="date"
+            type="daterange"
+            placeholder="注册时间"
+          />
+          <CzrFormColumn
+            width="6.68rem"
+            class="__czr-table-form-column"
+            :span="24"
+            label-width="0px"
+            v-model:param="state.query.form.enabled"
+            link="select"
+            :options="DictionaryStore.trueFalseStatus"
+            placeholder="状态"
+          />
+          <CzrFormColumn
+            width="6.68rem"
+            class="__czr-table-form-column"
+            :span="24"
+            label-width="0px"
+            v-model:param="state.query.form.xxx"
+            link="select"
+            :options="[]"
+            placeholder="全部角色"
+          />
+          <CzrFormColumn
+            width="15.63rem"
+            class="__czr-table-form-column"
+            :span="24"
+            label-width="0px"
+            v-model:param="state.text"
+            placeholder="输入关键词以检索"
+            :prefix-icon="Search"
+          />
+          <CzrButton type="add" @click="onInvite" title="新增用户" />
+          <CzrButton type="add" @click="onAdd" title="新增账号" />
+        </CzrForm>
+      </div>
+    </template>
+    <template #table>
+      <CzrTable
+        v-loading="state.query.loading"
+        v-model:data="state.query.result.data"
+        :head="state.query.head"
+        :total="state.query.result.total"
+        :page="state.query.page.pageNum"
+        :pageSize="state.query.page.pageSize"
+        @handlePage="onPage"
+        @handleSort="onSort"
+        v-model:selected="state.query.selected"
+      >
+        <template #caozuo-column-value="{ scope }">
+          <div class="__czr-table-operations">
+            <CzrButton type="table" title="编辑" @click="onEdit(scope.row)" />
+            <CzrButton type="table-del" @click="onDel(scope.row)" />
+          </div>
+        </template>
+      </CzrTable>
+    </template>
+  </CzrContent>
+  <detailCom
+    v-model:show="state.detail.show"
+    :transfer="state.detail.transfer"
+    @refresh="onSearch"
+  />
+  <CzrDialog
+    :show="state.invite.show"
+    title="新增用户"
+    @onClose="state.invite.show = false"
+    width="40rem"
+    :show-submit="false"
+    :show-close="false"
+  >
+    <div class="bm-form">
+      <CzrFormColumn
+        :span="24"
+        label="角色"
+        v-model:param="state.invite.roles"
+        link="select"
+        :options="[]"
+        :multiple="true"
+      />
+      <div class="__czr-title_1 mb-2">
+        邀请链接
+        <div class="ml-2 text-xs font-normal text-[var(--czr-error-color)]">
+          链接24小时内有效
+        </div>
+      </div>
+      <CzrFormColumn
+        :span="24"
+        label="邀请链接"
+        v-model:param="state.invite.url"
+        :readonly="true"
+      >
+        <template #suffix>
+          <CopyDocument
+            class="w-4 cursor-pointer"
+            @click="onCopy(state.invite.url)"
+          />
+        </template>
+      </CzrFormColumn>
+    </div>
+  </CzrDialog>
+</template>
 
 <script setup lang="ts">
-import { reactive } from 'vue'
+import {
+  computed,
+  getCurrentInstance,
+  onMounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue'
+import { CopyDocument, Search } from '@element-plus/icons-vue'
+import { debounce } from 'lodash'
+import { useAppStore, useDialogStore, useDictionaryStore } from '@/stores'
+import { ElMessage } from 'element-plus'
+import detailCom from './detail.vue'
+import { userPage } from '@/api/modules/center/user'
+import { copy } from '@/utils/czr-util'
 
-const state: any = reactive({})
+const AppStore = useAppStore()
+const DialogStore = useDialogStore()
+const DictionaryStore = useDictionaryStore()
+const emit = defineEmits([])
+const props = defineProps({})
+const { proxy }: any = getCurrentInstance()
+const state: any = reactive({
+  text: '',
+  query: {
+    init: false,
+    loading: false,
+    head: [
+      { value: 'loginId', label: '账号', show: true },
+      { value: 'name', label: '用户名', show: true },
+      {
+        value: 'createTime',
+        label: '注册时间',
+        show: true,
+        width: 180,
+        datetime: true,
+      },
+      {
+        value: 'loginTime',
+        label: '最后登录时间',
+        show: true,
+        width: 180,
+        datetime: true,
+      },
+      {
+        value: 'enabled',
+        label: '状态',
+        show: true,
+        dictList: computed(() => DictionaryStore.trueFalseStatus),
+      },
+      {
+        value: 'caozuo',
+        label: '操作',
+        show: true,
+        width: 200,
+        fixed: 'right',
+        popover: false,
+      },
+    ],
+    page: {
+      pageNum: 1,
+      pageSize: 20,
+    },
+    form: {},
+    formReal: {},
+    sort: {},
+    result: {
+      total: 0,
+      data: [],
+    },
+    selected: [],
+  },
+  detail: {
+    show: false,
+    transfer: {},
+  },
+  invite: {
+    show: false,
+    url: '',
+    roles: [],
+  },
+})
+const setText = debounce((v) => {
+  state.query.form.name = v
+}, 1000)
+watch(
+  () => state.text,
+  (n) => {
+    setText(n)
+  },
+)
+watch(
+  () => state.query.form,
+  (n) => {
+    if (state.query.init) {
+      onSearch()
+    }
+  },
+  { deep: true },
+)
+const onSort = ({ key, value }) => {
+  state.query.sort[key] = value
+  onSearch()
+}
+const onPage = (pageNum, pageSize) => {
+  setTimeout(() => {
+    state.query.init = true
+  }, 100)
+  state.query.page = {
+    pageNum: pageNum,
+    pageSize: pageSize,
+  }
+  const params = {
+    page: state.query.page.pageNum,
+    size: state.query.page.pageSize,
+  }
+  //  添加表单参数
+  for (const [k, v] of Object.entries(state.query.formReal)) {
+    if (proxy.$czrUtil.isValue(v)) {
+      if (k === 'loginTime') {
+        params['loginTimeStart'] = v[0]
+        params['loginTimeEnd'] = v[1]
+      } else if (k === 'createTime') {
+        params['createTimeStart'] = v[0]
+        params['createTimeEnd'] = v[1]
+      } else {
+        params[k] = v
+      }
+    }
+  }
+
+  //  添加排序参数
+  for (const [k, v] of Object.entries(state.query.sort)) {
+  }
+  state.query.loading = true
+  userPage(params)
+    .then(({ data }: any) => {
+      state.query.result.total = data.totalElements
+      state.query.result.data = data.content
+    })
+    .catch(() => {})
+    .finally(() => {
+      state.query.loading = false
+    })
+}
+const onSearch = () => {
+  state.query.formReal = JSON.parse(JSON.stringify(state.query.form))
+  onPage(1, state.query.page.pageSize)
+}
+const onReset = () => {
+  state.query.page = {
+    pageNum: 1,
+    pageSize: 20,
+  }
+  state.query.form = {}
+  state.query.sort = {}
+  onSearch()
+}
+const onAdd = () => {
+  state.detail.transfer = {
+    mode: 'add',
+  }
+  state.detail.show = true
+}
+const onEdit = (row) => {
+  state.detail.transfer = {
+    mode: 'edit',
+    id: row.id,
+  }
+  state.detail.show = true
+}
+const onDel = (row: any) => {
+  DialogStore.confirm({
+    title: '删除确认',
+    content: `请确认是否删除`,
+    onSubmit: () => {
+      // appDel(row.id)
+      //   .then(() => {
+      //     ElMessage.success('删除成功!')
+      //   })
+      //   .catch(() => {})
+      //   .finally(() => {
+      //     onSearch()
+      //   })
+    },
+  })
+}
+
+const onInvite = () => {
+  state.invite.url = 'https://cn.element-plus.org/zh-CN/component/input.html'
+  state.invite.roles = []
+  state.invite.show = true
+}
+const onCopy = (str) => {
+  copy(str)
+  ElMessage.success('复制成功!')
+}
+onMounted(() => {
+  initDictionary()
+  onReset()
+})
+const initDictionary = () => {
+  DictionaryStore.initTenants()
+}
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.model {
+  width: 100%;
+  background-image: url('@/assets/images/model/model-icon-7.png');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  padding: 1rem;
+  border-radius: 10px;
+  box-shadow: 0rem 0.25rem 0.63rem 0rem rgba(40, 83, 247, 0.05);
+  border: var(--czr-border);
+  display: flex;
+  flex-direction: column;
+}
+</style>

+ 1 - 1
vite.config.ts

@@ -57,7 +57,7 @@ export default defineConfig(({ mode, command }) => {
       strictPort: false,
       proxy: {
         [env.VITE_BASE_API_PROXY]: {
-          target: 'http://8.130.72.63:18091',
+          target: 'http://1.95.78.201:18088/',
           changeOrigin: true,
           rewrite: (path) => path.replace(env.VITE_BASE_API_PROXY, ''),
         },