Procházet zdrojové kódy

对接 角色 & 用户 & 菜单

cc12458 před 1 rokem
rodič
revize
abea74c896

+ 1 - 0
@types/components.d.ts

@@ -63,5 +63,6 @@ declare module 'vue' {
     UserEdit: typeof import('./../src/components/UserEdit.vue')['default']
     UserPassword: typeof import('./../src/components/UserPassword.vue')['default']
     UserPreview: typeof import('./../src/components/UserPreview.vue')['default']
+    UserQRCode: typeof import('./../src/components/UserQRCode.vue')['default']
   }
 }

+ 46 - 10
src/components/RoleEdit.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import type { RoleModel }                           from '@/model/system.model';
-import { editRoleMethod, roleMethod }               from '@/request/api/system.api';
-import { useRequest }                               from 'alova/client';
-import { type VxeFormListeners, type VxeFormProps } from 'vxe-pc-ui';
+import type { RoleModel }                                                 from '@/model/system.model';
+import { editRoleMethod, getRoleMenusMethod, roleMethod }                 from '@/request/api/system.api';
+import { useRequest }                                                     from 'alova/client';
+import { type VxeFormListeners, type VxeFormProps, type VxeTreeInstance } from 'vxe-pc-ui';
 
 
 type FormModel = Partial<RoleModel>
@@ -14,6 +14,19 @@ const emits = defineEmits<{
   submit: [ data?: RoleModel ],
 }>();
 
+const menusRef = ref<VxeTreeInstance>();
+const menus = shallowRef<any[]>([]);
+const selected = ref<string[]>([]);
+// @ts-ignore
+const { loading: menusLoading } = useRequest(() => getRoleMenusMethod(props.data), { initialData: [] }).onSuccess(
+  ({ data }) => {
+    menus.value = data.menus;
+    nextTick(() => {
+      menusRef.value?.setAllExpandNode(true);
+      selected.value = [ ...data.selected ];
+    });
+
+  });
 const { loading, send: load } = useRequest(roleMethod, { immediate: false, initialData: props.data ?? defaultModel })
   .onSuccess(({ data }) => {
     formProps.data = { ...data };
@@ -29,16 +42,25 @@ const formProps = reactive<VxeFormProps<FormModel>>({
   data: { ...props.data },
   items: [
     { field: 'roleName', title: '角色名', span: 24, itemRender: { name: 'VxeInput' } },
+    {
+      field: 'dataScope', title: '数据权限', span: 24, itemRender: {
+        name: 'VxeRadioGroup',
+        options: [
+          { label: '本人', value: '5' },
+          { label: '本系统', value: '1' },
+        ],
+      },
+    },
+    { field: 'menuIds', title: '菜单权限', span: 24, slots: { default: 'menus' } },
     { align: 'center', span: 24, slots: { default: 'active' } },
   ],
   rules: {
-    roleName: [
-      { required: true, message: '请输入角色名' },
-    ],
+    roleName: [ { required: true, message: '请输入角色名' } ],
+    dataScope: [ { required: true, message: '请选择数据权限' } ],
   },
 });
 const formEmits: VxeFormListeners<FormModel> = {
-  submit({ data }) { submit(data); },
+  submit({ data }) { submit({ roleSort: 0, status: 0, ...data, menuIds: selected.value }); },
   reset() { formProps.data = { ...props.data }; },
 };
 
@@ -47,11 +69,25 @@ onBeforeMount(() => {
 });
 </script>
 <template>
-  <vxe-form v-bind="formProps" v-on="formEmits" :loading>
+  <vxe-form class-name="role-edit-form" v-bind="formProps" v-on="formEmits" :loading>
+    <template #menus>
+      <vxe-tree
+        ref="menusRef" :data="menus" :loading="menusLoading"
+        show-checkbox v-model:checkNodeKeys="selected"
+        title-field="label" value-field="id" key-field="id"
+      />
+    </template>
     <template #active>
       <vxe-button type="submit" status="primary" content="提交" :loading="submitting"></vxe-button>
       <vxe-button type="reset" content="重置" :disabled="submitting"></vxe-button>
     </template>
   </vxe-form>
 </template>
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.role-edit-form {
+
+  :deep(.vxe-tree) {
+    max-height: 400px;
+  }
+}
+</style>

+ 14 - 7
src/components/UserEdit.vue

@@ -20,9 +20,10 @@ const { loading, send: load } = useRequest(userMethod, { immediate: false, initi
   .onSuccess(({ data }) => {
     formProps.data = { ...data };
   });
-const { loading: submitting, send: submit } = useRequest(editUserMethod, { immediate: false }).onSuccess(({ data }) => {
-  emits('submit');
-});
+const { loading: submitting, send: submit } = useRequest(editUserMethod, { immediate: false })
+  .onSuccess(({ data }) => {
+    emits('submit');
+  });
 
 const formProps = reactive<VxeFormProps<FormModel>>({
   titleWidth: 100,
@@ -32,11 +33,17 @@ const formProps = reactive<VxeFormProps<FormModel>>({
   data: { ...props.data },
   items: [
     { field: 'userName', title: '系统账号', span: 24, itemRender: { name: 'VxeInput' } },
-    { field: 'password', title: '密码', span: 24, itemRender: { name: 'VxeInput', props: { type: 'password' } }, visible: !props.data?.userId },
     {
-      field: 'roles', title: '角色', span: 24, itemRender: {
+      field: 'password',
+      title: '密码',
+      span: 24,
+      itemRender: { name: 'VxeInput', props: { type: 'password' } },
+      visible: !props.data?.userId,
+    },
+    {
+      field: 'roleIds', title: '角色', span: 24, itemRender: {
         name: 'VxeSelect', props: {
-          multiple: true,
+          multiple: true, clearable: true,
           loading: computed(() => rolesLoading.value),
           options: computed(() => roles.value),
           optionProps: { label: 'roleName', value: 'roleId' },
@@ -54,7 +61,7 @@ const formProps = reactive<VxeFormProps<FormModel>>({
         },
       },
     },
-    { field: '工号', title: '工号', span: 24, itemRender: { name: 'VxeInput' } },
+    { field: 'jobnumber', title: '工号', span: 24, itemRender: { name: 'VxeInput' } },
     { field: 'phonenumber', title: '手机号码', span: 24, itemRender: { name: 'VxeInput', props: { maxlength: 11 } } },
     { align: 'center', span: 24, slots: { default: 'active' } },
   ],

+ 2 - 2
src/components/UserPreview.vue

@@ -12,9 +12,9 @@ const { data, loading } = useRequest(() => userMethod(props.data), { initialData
   <a-spin :spinning="loading">
     <a-descriptions bordered>
       <a-descriptions-item label="系统账号">{{ data.userId }}</a-descriptions-item>
-      <a-descriptions-item label="角色" :span="2">{{ data.roles?.join('/') }}</a-descriptions-item>
+      <a-descriptions-item label="角色" :span="2">{{ data.roles?.map(t => t.roleName)?.join(', ') }}</a-descriptions-item>
       <a-descriptions-item label="医院 / 科室" :span="2">{{ data['dept']?.deptName }}</a-descriptions-item>
-      <a-descriptions-item label="工号">{{ data.工号 }}</a-descriptions-item>
+      <a-descriptions-item label="工号">{{ data.jobnumber }}</a-descriptions-item>
       <a-descriptions-item label="姓名">{{ data.nickName }}</a-descriptions-item>
       <a-descriptions-item label="手机号码">{{ data.phonenumber }}</a-descriptions-item>
     </a-descriptions>

+ 12 - 0
src/components/UserQRCode.vue

@@ -0,0 +1,12 @@
+<script setup lang="ts">
+import type { UserModel } from '@/model/system.model';
+
+
+const props = defineProps<{ dataset: UserModel }>();
+</script>
+<template>
+  <div class="text-center" style="padding: 12px">
+    <a-image :src="props.dataset?.appletImg" :width="256" :height="256" :preview="false"></a-image>
+  </div>
+</template>
+<style scoped lang="scss"></style>

+ 3 - 1
src/model/system.model.ts

@@ -18,10 +18,11 @@ export interface UserModel {
   userId: string;
   userName: string;
   phonenumber?: string;
+  jobnumber?: string;
 
   roles?: any[];
 
-  工号?: string;
+  appletImg: string;
 }
 
 export type UserQuery = Partial<UserModel> & { branch?: string };
@@ -35,6 +36,7 @@ export interface TagQuery {
 export interface TagModel {
   id: string;
   name: string;
+  status: '0' | '1';
 
   parentId?: string;
   parentName?: string;

+ 2 - 18
src/pages/index.vue

@@ -3,32 +3,16 @@ import { useAccountStore } from '@/stores';
 import { LogoutOutlined }  from '@ant-design/icons-vue';
 import { useElementSize }  from '@vueuse/core';
 
-import type { MenuProps } from 'ant-design-vue';
-
 import { storeToRefs } from 'pinia';
 
 
 const title = import.meta.env.SIX_TITLE;
 const Account = useAccountStore();
-const { local } = storeToRefs(Account);
+const { local, menus } = storeToRefs(Account);
 
 const titleRef = ref();
 const { width } = useElementSize(titleRef, void 0, { box: 'border-box' });
 
-const menus: MenuProps['items'] = [
-  { label: '中医调理 ', title: ' 中医调理', key: '/patient/room' },
-  { label: '患者管理 ', title: ' 患者管理', key: '/patient/history' },
-  {
-    label: '系统设置 ', title: ' 系统设置', key: '/system',
-    children: [
-      { label: '角色管理 ', key: '/system/role' },
-      { label: '用户管理 ', key: '/system/user' },
-      { label: '标签管理 ', key: '/system/tag' },
-
-    ],
-  },
-];
-
 const router = useRouter();
 const selectedKeys = computed(() => {
   return [ router.currentRoute.value.path ];
@@ -63,7 +47,6 @@ const current = ref<string[]>([]);
         />
       </div>
       <div class="account">
-        {{ current }}
         <a-avatar size="large" :src="local?.avatar">{{ local?.name?.slice(0, 1) }}</a-avatar>
         <span class="m-x-2">{{ local?.name }}</span>
         <LogoutOutlined class="cursor-pointer" @click="handleLogout" />
@@ -120,6 +103,7 @@ const current = ref<string[]>([]);
 
       .ant-menu-submenu {
         padding: 0;
+
         .ant-menu-submenu-title {
           padding: 0 16px;
         }

+ 2 - 0
src/pages/index/system/role.vue

@@ -184,6 +184,8 @@ function editRole(model?: RoleModel, index?: number) {
     id: `role-edit-modal`,
     remember: true,
     storage: true,
+    width: 500,
+    minHeight: 600,
     slots: {
       default() {
         return h(RoleEdit, <any> {

+ 30 - 6
src/pages/index/system/user.vue

@@ -2,6 +2,7 @@
 import UserEdit                           from '@/components/UserEdit.vue';
 import UserPassword                       from '@/components/UserPassword.vue';
 import UserPreview                        from '@/components/UserPreview.vue';
+import UserQRCode                         from '@/components/UserQRCode.vue';
 import { type UserModel, type UserQuery } from '@/model/system.model';
 
 import { branchMethod, deleteUserMethod, usersMethod } from '@/request/api/system.api';
@@ -30,7 +31,7 @@ const searchFormProps = reactive<VxeFormProps<UserQuery>>({
   items: [
     { field: 'userName', title: '系统账号', span: 6, itemRender: { name: 'VxeInput' } },
     { field: 'nickName', title: '姓名', span: 6, itemRender: { name: 'VxeInput' } },
-    { field: '工号', title: '工号', span: 6, itemRender: { name: 'VxeInput' } },
+    { field: 'jobnumber', title: '工号', span: 6, itemRender: { name: 'VxeInput' } },
     { field: 'phonenumber', title: '手机号码', span: 6, itemRender: { name: 'VxeInput' } },
     {
       field: 'deptId', title: '医院 / 科室', span: 6, itemRender: {
@@ -84,12 +85,11 @@ const gridOptions = reactive<VxeGridProps<UserModel>>({
     { field: 'userName', title: '系统账号', minWidth: 160 },
     { field: 'nickName', title: '姓名', minWidth: 160 },
     { field: 'dept.deptName', title: '医院 / 科室', minWidth: 160 },
-    { field: '工号', title: '工号', minWidth: 160 },
+    { field: 'jobnumber', title: '工号', minWidth: 160 },
     { field: 'phonenumber', title: '手机号码', minWidth: 160 },
     {
-      field: 'roles', title: '角色', minWidth: 160, formatter: ({ cellValue, row }) => {
-        return row.roles?.join('/') ?? '';
-      },
+      field: 'roles', title: '角色', minWidth: 160,
+      formatter: ({ cellValue, row }) => row.roles?.map?.(t => t.roleName)?.join(', ') ?? '',
     },
     {
       title: '操作',
@@ -100,13 +100,16 @@ const gridOptions = reactive<VxeGridProps<UserModel>>({
           mode: 'text',
         },
         options: [
+          { content: '小程序码', name: 'QRCode' },
           { content: '修改', name: 'editUser' },
           { content: '删除', status: 'error', name: 'deleteUser' },
         ],
         events: {
           click({ row, rowIndex }, { name }) {
             let method;
-            if ( name === 'editUser' ) { method = editUser; } else if ( name === 'deleteUser' ) { method = deleteUser; }
+            if ( name === 'editUser' ) { method = editUser; }
+            else if ( name === 'deleteUser' ) { method = deleteUser; }
+            else if ( name === 'QRCode' ) { method = QRCode; }
             method?.(row, rowIndex);
           },
         },
@@ -226,6 +229,27 @@ function updateUserPassword(model: UserModel, index?: number) {
     },
   });
 }
+
+function QRCode(model: UserModel) {
+  const { userName } = model;
+  VxeUI.modal.open({
+    title: `${ userName } 专属小程序码`,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `user-preview-qr-code`,
+    remember: true,
+    storage: true,
+    padding: false,
+    width: 256 + 12 * 2,
+    slots: {
+      default() {
+        return h(UserQRCode, <any> {
+          dataset: model,
+        });
+      },
+    },
+  })
+}
 </script>
 <template>
   <div class="page-container flex flex-col">

+ 11 - 10
src/pages/login.vue

@@ -1,12 +1,12 @@
 <script setup lang="ts">
-import type { AccountFormState }      from '@/model';
-import { accountMethod, loginMethod } from '@/request/api/account.api';
-import router                         from '@/router';
-import { useAccountStore }            from '@/stores';
-import { LockOutlined, UserOutlined } from '@ant-design/icons-vue';
-import { useSerialRequest }           from 'alova/client';
-import { Form }                       from 'ant-design-vue';
-import { h }                          from 'vue';
+import type { AccountFormState }                      from '@/model';
+import { accountMethod, getMenusMethod, loginMethod } from '@/request/api/account.api';
+import router                                         from '@/router';
+import { useAccountStore }                            from '@/stores';
+import { LockOutlined, UserOutlined }                 from '@ant-design/icons-vue';
+import { useSerialRequest }                           from 'alova/client';
+import { Form }                                       from 'ant-design-vue';
+import { h }                                          from 'vue';
 
 
 const formState = reactive<AccountFormState>({
@@ -26,10 +26,11 @@ const formWrapper = ref<HTMLElement>();
 const { loading, error, send } = useSerialRequest([
   () => loginMethod(formState),
   token => accountMethod(token),
+  data => getMenusMethod(data),
 ], { immediate: false }).onSuccess(
   ({ data }) => {
-    useAccountStore().$init(data);
-    router.replace({ path: '/' });
+    const path = useAccountStore().$init(data);
+    nextTick(() => router.replace({ path }));
   },
 );
 

+ 51 - 3
src/request/api/account.api.ts

@@ -1,5 +1,20 @@
-import { type AccountFormState, transformAccount } from '@/model';
-import request                                     from '@/request/alova';
+import { type AccountFormState, type PeopleModel, transformAccount } from '@/model';
+import request                                                       from '@/request/alova';
+import router                                                        from '@/router';
+
+
+interface Menu {
+  label: string;
+  title: string;
+  key: string;
+  children?: Menu[];
+}
+
+export interface AccountModel {
+  token: string;
+  local: PeopleModel;
+  menus?: Menu[];
+}
 
 
 export function loginMethod(data: AccountFormState) {
@@ -11,7 +26,7 @@ export function loginMethod(data: AccountFormState) {
 }
 
 export function accountMethod(token: string) {
-  return request.Get(`/prod-api/system/user/getInfo`, {
+  return request.Get<AccountModel>(`/prod-api/system/user/getInfo`, {
     headers: { Authorization: token },
     transform(data: any, headers) {
       console.log('accountMethod', data);
@@ -23,3 +38,36 @@ export function accountMethod(token: string) {
     meta: { unconvert: true },
   });
 }
+
+
+export function getMenusMethod(account: AccountModel) {
+  const routes = new Set(router.getRoutes().map(route => route.path));
+
+  const transformMenus = (data: any[], prefix?: string) => {
+    const menus: Menu[] = [];
+    if ( !Array.isArray(data) ) return menus;
+    for ( const menu of data ) {
+      const path = [ prefix, menu?.path ].filter(v => v != null).join('/');
+      const title = menu?.meta?.title ?? path;
+      const children = transformMenus(menu.children, path === '/' ? '' : path);
+      if ( children.length > 1 ) {
+        menus.push({ key: path, title, label: title, children });
+      } else if ( children.length === 1 ) {
+        menus.push(children[ 0 ]);
+      } else if ( routes.has(path) ) {
+        menus.push({ key: path, title, label: title });
+      }
+    }
+    return menus;
+  };
+
+  return request.Get<AccountModel, any[]>(`/prod-api/system/menu/getRouters`, {
+    headers: { Authorization: account.token },
+    transform(data) {
+      console.log('菜单-->', data, transformMenus(data), [ ...routes ]);
+
+      return { ...account, menus: transformMenus(data) };
+    },
+  });
+}
+

+ 65 - 13
src/request/api/system.api.ts

@@ -10,20 +10,33 @@ export function branchMethod() {
 
 export function usersMethod(page: number, size: number, query?: UserQuery) {
   return request.Get<List<RoleModel>>(`/prod-api/system/user/list`, {
+    hitSource: /user$/,
     params: { pageNum: page, pageSize: size, ...query },
   });
 }
 
 export function userMethod(data: Partial<UserModel>) {
-  return request.Get<UserModel>(`/prod-api/system/user/${ data.userId }`);
+  return request.Get<UserModel, any>(
+    `/prod-api/system/user/${ data.userId }`,
+    {
+      hitSource: 'edit-user',
+      transform(data) {
+        let roleIds = data?.roleIds ?? [];
+        if ( !roleIds.length ) roleIds = data?.roles?.map?.((t: any) => t.roleId);
+        return { ...data, roleIds };
+      },
+    },
+  );
 }
 
 export function editUserMethod(data: Partial<UserModel>) {
-  return data.userId ? request.Put(`/prod-api/system/user`, data) : request.Post(`/prod-api/system/user`, data);
+  return data.userId
+         ? request.Put(`/prod-api/system/user`, data, { name: 'edit-user' })
+         : request.Post(`/prod-api/system/user`, data, { name: 'edit-user' });
 }
 
 export function deleteUserMethod(data: Partial<UserModel>) {
-  return request.Delete(`/prod-api/system/user/${ data.userId }`);
+  return request.Delete(`/prod-api/system/user/${ data.userId }`, { name: 'delete-user' });
 }
 
 export function updateUserPasswordMethod(data: Partial<UserModel>) {
@@ -37,35 +50,69 @@ export function rolesAllMethod() {
 
 
 export function rolesMethod(page: number, size: number, query?: RoleQuery) {
-  return request.Get<List<RoleModel>>(`/prod-api/system/role/list`, {
-    params: { pageNum: page, pageSize: size, ...query },
-  });
+  return request.Get<List<RoleModel>>(
+    `/prod-api/system/role/list`,
+    {
+      hitSource: /role$/,
+      params: { pageNum: page, pageSize: size, ...query },
+    },
+  );
 }
 
 export function roleMethod(data: Partial<RoleModel>) {
-  return request.Get<RoleModel>(`/prod-api/system/role/${ data.roleId }`);
+  return request.Get<RoleModel>(
+    `/prod-api/system/role/${ data.roleId }`,
+    {
+      hitSource: 'edit-role',
+    },
+  );
 }
 
 export function editRoleMethod(data: Partial<RoleModel>) {
-  return data.roleId ? request.Put(`/prod-api/system/role`, data, {}) : request.Post(`/prod-api/system/role`, data);
+  return data.roleId
+         ? request.Put(`/prod-api/system/role`, data, { name: 'edit-role' })
+         : request.Post(`/prod-api/system/role`, data, { name: 'edit-role' });
 }
 
 export function deleteRoleMethod(data: Partial<RoleModel>) {
-  return request.Delete(`/prod-api/system/role/${ data.roleId }`);
+  return request.Delete(`/prod-api/system/role/${ data.roleId }`, { name: 'delete-role' });
 }
 
 export function updateRoleStatusMethod(data: Partial<RoleModel>) {
-  return request.Put(`/prod-api/system/role/changeStatus`, { roleId: data.roleId, status: data.status });
+  return request.Put(`/prod-api/system/role/changeStatus`, { roleId: data.roleId, status: data.status }, { name: 'update-role' });
+}
+
+type RoleMenu = { id: string; label: string; children?: RoleMenu[] };
+
+export function getRoleMenusMethod(data?: Partial<RoleModel>) {
+  return data?.roleId ? request.Get<{ selected: string[], menus: RoleMenu[] }, { menus: RoleMenu[], checkedKeys: string[] }>(
+    `/prod-api/system/menu/roleMenuTreeselect/${ data.roleId }`,
+    {
+      hitSource: 'edit-role',
+      meta: { unconvert: true },
+      transform(data) {
+        return { menus: data.menus, selected: Array.isArray(data.checkedKeys) ? data.checkedKeys : [] };
+      },
+    },
+  ) : request.Get<{ selected: string[], menus: RoleMenu[] }, RoleMenu[]>(`/prod-api/system/menu/treeselect`, {
+      transform(data) {
+        return { menus: data, selected: [] };
+      },
+    },
+  );
+
 }
 
 export function tagsMethod(page: number, size: number, query?: TagQuery) {
   return request.Post<List<TagModel>>(`/fdhb-pc/tagManage/pageTag`, query ?? {}, {
+    hitSource: /tag$/,
     params: { pageNum: page, pageSize: size },
   });
 }
 
 export function tagsSearchMethod(query?: TagQuery) {
   return request.Post<List<TagModel>, TagModel[]>(`/fdhb-pc/tagManage/selectTag`, query ?? {}, {
+    hitSource: /tag$/,
     transform(data) {
       return { total: data.length, data };
     },
@@ -73,18 +120,22 @@ export function tagsSearchMethod(query?: TagQuery) {
 }
 
 export function tagMethod(data: Partial<TagModel>) {
-  return request.Get<TagModel>(`/prod-api/system/role/${ data.roleId }`);
+  return request.Get<TagModel>(`/fdhb-pc/tagManage/getTag`, {
+    hitSource: 'edit-tag',
+    params: { tagId: data.id },
+  });
 }
 
 
 export function tagEditMethod(data: Partial<TagModel>) {
   return data.id
-         ? request.Post(`/fdhb-pc/tagManage/updateTag`, { ...data, tagId: data.id })
-         : request.Post(`/fdhb-pc/tagManage/addTag`, data);
+         ? request.Post(`/fdhb-pc/tagManage/updateTag`, { ...data, tagId: data.id }, { name: 'edit-tag' })
+         : request.Post(`/fdhb-pc/tagManage/addTag`, data, { name: 'edit-tag' });
 }
 
 export function tagDeleteMethod(data: Partial<TagModel>) {
   return request.Get(`/fdhb-pc/tagManage/deleteTag`, {
+    name: 'delete-tag',
     params: { tagId: data.id },
     cacheFor: null,
   });
@@ -93,6 +144,7 @@ export function tagDeleteMethod(data: Partial<TagModel>) {
 
 export function tagUpdateStatusMethod(data: Partial<TagModel>) {
   return request.Get(`/fdhb-pc/tagManage/updateStatus`, {
+    name: 'update-tag',
     params: { tagId: data.id, status: data.status },
     cacheFor: null,
   });

+ 15 - 5
src/stores/account.store.ts

@@ -1,20 +1,30 @@
-import type { PeopleModel } from '@/model';
-import { defineStore }      from 'pinia';
+import type { AccountModel } from '@/request/api/account.api';
+import { defineStore }       from 'pinia';
 
 
 export const useAccountStore = defineStore('account', () => {
   const token = ref<string>();
-  const local = ref<PeopleModel | null>();
+  const local = ref<AccountModel['local'] | null>();
+  const menus = ref<AccountModel['menus']>([]);
 
-  const $init = (data: { token: string; local: PeopleModel | null }) => {
+  const $init = (data: AccountModel) => {
     token.value = data.token;
     local.value = data.local;
+    menus.value = data.menus;
+    return getFirstMenu(menus.value) ?? '/';
   };
 
   const $reset = () => {
     token.value = '';
     local.value = null;
+    menus.value = [];
   };
 
-  return { token, local, $init, $reset };
+  return { token, local, menus, $init, $reset };
 });
+
+function getFirstMenu(menus?: AccountModel['menus']): string | void {
+  if ( !menus?.length ) return void 0;
+  const { key, children = [] } = menus?.[ 0 ] ?? {};
+  return getFirstMenu(children) ?? key;
+}