Преглед на файлове

feat(router): 增加基本router vol.4

gemercheung преди 3 години
родител
ревизия
f38cf70a67

+ 202 - 0
mock/demo/system.ts

@@ -0,0 +1,202 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultError, resultPageSuccess, resultSuccess } from '../_util';
+
+const accountList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 20; index++) {
+    result.push({
+      id: `${index}`,
+      account: '@first',
+      email: '@email',
+      nickname: '@cname()',
+      role: '@first',
+      createTime: '@datetime',
+      remark: '@cword(10,20)',
+      'status|1': ['0', '1'],
+    });
+  }
+  return result;
+})();
+
+const roleList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 4; index++) {
+    result.push({
+      id: index + 1,
+      orderNo: `${index + 1}`,
+      roleName: ['超级管理员', '管理员', '文章管理员', '普通用户'][index],
+      roleValue: '@first',
+      createTime: '@datetime',
+      remark: '@cword(10,20)',
+      menu: [['0', '1', '2'], ['0', '1'], ['0', '2'], ['2']][index],
+      'status|1': ['0', '1'],
+    });
+  }
+  return result;
+})();
+
+const deptList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 3; index++) {
+    result.push({
+      id: `${index}`,
+      deptName: ['华东分部', '华南分部', '西北分部'][index],
+      orderNo: index + 1,
+      createTime: '@datetime',
+      remark: '@cword(10,20)',
+      'status|1': ['0', '0', '1'],
+      children: (() => {
+        const children: any[] = [];
+        for (let j = 0; j < 4; j++) {
+          children.push({
+            id: `${index}-${j}`,
+            deptName: ['研发部', '市场部', '商务部', '财务部'][j],
+            orderNo: j + 1,
+            createTime: '@datetime',
+            remark: '@cword(10,20)',
+            'status|1': ['0', '1'],
+            parentDept: `${index}`,
+            children: undefined,
+          });
+        }
+        return children;
+      })(),
+    });
+  }
+  return result;
+})();
+
+const menuList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 3; index++) {
+    result.push({
+      id: `${index}`,
+      icon: ['ion:layers-outline', 'ion:git-compare-outline', 'ion:tv-outline'][index],
+      component: 'LAYOUT',
+      type: '0',
+      menuName: ['Dashboard', '权限管理', '功能'][index],
+      permission: '',
+      orderNo: index + 1,
+      createTime: '@datetime',
+      'status|1': ['0', '0', '1'],
+      children: (() => {
+        const children: any[] = [];
+        for (let j = 0; j < 4; j++) {
+          children.push({
+            id: `${index}-${j}`,
+            type: '1',
+            menuName: ['菜单1', '菜单2', '菜单3', '菜单4'][j],
+            icon: 'ion:document',
+            permission: ['menu1:view', 'menu2:add', 'menu3:update', 'menu4:del'][index],
+            component: [
+              '/dashboard/welcome/index',
+              '/dashboard/analysis/index',
+              '/dashboard/workbench/index',
+              '/dashboard/test/index',
+            ][j],
+            orderNo: j + 1,
+            createTime: '@datetime',
+            'status|1': ['0', '1'],
+            parentMenu: `${index}`,
+            children: (() => {
+              const children: any[] = [];
+              for (let k = 0; k < 4; k++) {
+                children.push({
+                  id: `${index}-${j}-${k}`,
+                  type: '2',
+                  menuName: '按钮' + (j + 1) + '-' + (k + 1),
+                  icon: '',
+                  permission:
+                    ['menu1:view', 'menu2:add', 'menu3:update', 'menu4:del'][index] +
+                    ':btn' +
+                    (k + 1),
+                  component: [
+                    '/dashboard/welcome/index',
+                    '/dashboard/analysis/index',
+                    '/dashboard/workbench/index',
+                    '/dashboard/test/index',
+                  ][j],
+                  orderNo: j + 1,
+                  createTime: '@datetime',
+                  'status|1': ['0', '1'],
+                  parentMenu: `${index}-${j}`,
+                  children: undefined,
+                });
+              }
+              return children;
+            })(),
+          });
+        }
+        return children;
+      })(),
+    });
+  }
+  return result;
+})();
+
+export default [
+  {
+    url: '/basic-api/system/getAccountList',
+    timeout: 100,
+    method: 'get',
+    response: ({ query }) => {
+      const { page = 1, pageSize = 20 } = query;
+      return resultPageSuccess(page, pageSize, accountList);
+    },
+  },
+  {
+    url: '/basic-api/system/getRoleListByPage',
+    timeout: 100,
+    method: 'get',
+    response: ({ query }) => {
+      const { page = 1, pageSize = 20 } = query;
+      return resultPageSuccess(page, pageSize, roleList);
+    },
+  },
+  {
+    url: '/basic-api/system/setRoleStatus',
+    timeout: 500,
+    method: 'post',
+    response: ({ query }) => {
+      const { id, status } = query;
+      return resultSuccess({ id, status });
+    },
+  },
+  {
+    url: '/basic-api/system/getAllRoleList',
+    timeout: 100,
+    method: 'get',
+    response: () => {
+      return resultSuccess(roleList);
+    },
+  },
+  {
+    url: '/basic-api/system/getDeptList',
+    timeout: 100,
+    method: 'get',
+    response: () => {
+      return resultSuccess(deptList);
+    },
+  },
+  {
+    url: '/basic-api/system/getMenuList',
+    timeout: 100,
+    method: 'get',
+    response: () => {
+      return resultSuccess(menuList);
+    },
+  },
+  {
+    url: '/basic-api/system/accountExist',
+    timeout: 500,
+    method: 'post',
+    response: ({ body }) => {
+      const { account } = body || {};
+      if (account && account.indexOf('admin') !== -1) {
+        return resultError('该字段不能包含admin');
+      } else {
+        return resultSuccess(`${account} can use`);
+      }
+    },
+  },
+] as MockMethod[];

+ 7 - 0
src/api/demo/model/accountModel.ts

@@ -0,0 +1,7 @@
+export interface GetAccountInfoModel {
+  email: string;
+  name: string;
+  introduction: string;
+  phone: string;
+  address: string;
+}

+ 12 - 0
src/api/demo/model/areaModel.ts

@@ -0,0 +1,12 @@
+export interface AreaModel {
+  id: string;
+  code: string;
+  parentCode: string;
+  name: string;
+  levelType: number;
+  [key: string]: string | number;
+}
+
+export interface AreaParams {
+  parentCode: string;
+}

+ 15 - 0
src/api/demo/model/optionsModel.ts

@@ -0,0 +1,15 @@
+import { BasicFetchResult } from '/@/api/model/baseModel';
+
+export interface DemoOptionsItem {
+  label: string;
+  value: string;
+}
+
+export interface selectParams {
+  id: number | string;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type DemoOptionsGetResultModel = BasicFetchResult<DemoOptionsItem>;

+ 74 - 0
src/api/demo/model/systemModel.ts

@@ -0,0 +1,74 @@
+import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
+
+export type AccountParams = BasicPageParams & {
+  account?: string;
+  nickname?: string;
+};
+
+export type RoleParams = {
+  roleName?: string;
+  status?: string;
+};
+
+export type RolePageParams = BasicPageParams & RoleParams;
+
+export type DeptParams = {
+  deptName?: string;
+  status?: string;
+};
+
+export type MenuParams = {
+  menuName?: string;
+  status?: string;
+};
+
+export interface AccountListItem {
+  id: string;
+  account: string;
+  email: string;
+  nickname: string;
+  role: number;
+  createTime: string;
+  remark: string;
+  status: number;
+}
+
+export interface DeptListItem {
+  id: string;
+  orderNo: string;
+  createTime: string;
+  remark: string;
+  status: number;
+}
+
+export interface MenuListItem {
+  id: string;
+  orderNo: string;
+  createTime: string;
+  status: number;
+  icon: string;
+  component: string;
+  permission: string;
+}
+
+export interface RoleListItem {
+  id: string;
+  roleName: string;
+  roleValue: string;
+  status: number;
+  orderNo: string;
+  createTime: string;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type AccountListGetResultModel = BasicFetchResult<AccountListItem>;
+
+export type DeptListGetResultModel = BasicFetchResult<DeptListItem>;
+
+export type MenuListGetResultModel = BasicFetchResult<MenuListItem>;
+
+export type RolePageListGetResultModel = BasicFetchResult<RoleListItem>;
+
+export type RoleListGetResultModel = RoleListItem[];

+ 44 - 0
src/api/demo/system.ts

@@ -0,0 +1,44 @@
+import {
+  AccountParams,
+  DeptListItem,
+  MenuParams,
+  RoleParams,
+  RolePageParams,
+  MenuListGetResultModel,
+  DeptListGetResultModel,
+  AccountListGetResultModel,
+  RolePageListGetResultModel,
+  RoleListGetResultModel,
+} from './model/systemModel';
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  AccountList = '/system/getAccountList',
+  IsAccountExist = '/system/accountExist',
+  DeptList = '/system/getDeptList',
+  setRoleStatus = '/system/setRoleStatus',
+  MenuList = '/system/getMenuList',
+  RolePageList = '/system/getRoleListByPage',
+  GetAllRoleList = '/system/getAllRoleList',
+}
+
+export const getAccountList = (params: AccountParams) =>
+  defHttp.get<AccountListGetResultModel>({ url: Api.AccountList, params });
+
+export const getDeptList = (params?: DeptListItem) =>
+  defHttp.get<DeptListGetResultModel>({ url: Api.DeptList, params });
+
+export const getMenuList = (params?: MenuParams) =>
+  defHttp.get<MenuListGetResultModel>({ url: Api.MenuList, params });
+
+export const getRoleListByPage = (params?: RolePageParams) =>
+  defHttp.get<RolePageListGetResultModel>({ url: Api.RolePageList, params });
+
+export const getAllRoleList = (params?: RoleParams) =>
+  defHttp.get<RoleListGetResultModel>({ url: Api.GetAllRoleList, params });
+
+export const setRoleStatus = (id: number, status: string) =>
+  defHttp.post({ url: Api.setRoleStatus, params: { id, status } });
+
+export const isAccountExist = (account: string) =>
+  defHttp.post({ url: Api.IsAccountExist, params: { account } }, { errorMessageMode: 'none' });

+ 78 - 0
src/router/routes/modules/system.ts

@@ -0,0 +1,78 @@
+import type { AppRouteModule } from '/@/router/types';
+
+import { LAYOUT } from '/@/router/constant';
+import { t } from '/@/hooks/web/useI18n';
+
+const system: AppRouteModule = {
+  path: '/system',
+  name: 'System',
+  component: LAYOUT,
+  redirect: '/system/account',
+  meta: {
+    orderNo: 2000,
+    icon: 'ion:settings-outline',
+    title: t('routes.demo.system.moduleName'),
+  },
+  children: [
+    {
+      path: 'account',
+      name: 'AccountManagement',
+      meta: {
+        title: t('routes.demo.system.account'),
+        ignoreKeepAlive: false,
+      },
+      component: () => import('/@/views/dashboard/system/account/index.vue'),
+    },
+    {
+      path: 'account_detail/:id',
+      name: 'AccountDetail',
+      meta: {
+        hideMenu: true,
+        title: t('routes.demo.system.account_detail'),
+        ignoreKeepAlive: true,
+        showMenu: false,
+        currentActiveMenu: '/system/account',
+      },
+      component: () => import('/@/views/dashboard/system/account/AccountDetail.vue'),
+    },
+    {
+      path: 'role',
+      name: 'RoleManagement',
+      meta: {
+        title: t('routes.demo.system.role'),
+        ignoreKeepAlive: true,
+      },
+      component: () => import('/@/views/dashboard/system/role/index.vue'),
+    },
+
+    {
+      path: 'menu',
+      name: 'MenuManagement',
+      meta: {
+        title: t('routes.demo.system.menu'),
+        ignoreKeepAlive: true,
+      },
+      component: () => import('/@/views/dashboard/system/menu/index.vue'),
+    },
+    {
+      path: 'dept',
+      name: 'DeptManagement',
+      meta: {
+        title: t('routes.demo.system.dept'),
+        ignoreKeepAlive: true,
+      },
+      component: () => import('/@/views/dashboard/system/dept/index.vue'),
+    },
+    {
+      path: 'changePassword',
+      name: 'ChangePassword',
+      meta: {
+        title: t('routes.demo.system.password'),
+        ignoreKeepAlive: true,
+      },
+      component: () => import('/@/views/dashboard/system/password/index.vue'),
+    },
+  ],
+};
+
+export default system;

+ 1 - 1
src/views/dashboard/scenes/list.vue

@@ -1,5 +1,5 @@
 <template>
-  <div> 场景直播 </div>
+  <div> 场景列表</div>
 </template>
 
 <script lang="ts" setup></script>

+ 62 - 0
src/views/dashboard/system/account/AccountDetail.vue

@@ -0,0 +1,62 @@
+<template>
+  <PageWrapper
+    :title="`用户` + userId + `的资料`"
+    content="这是用户资料详情页面。本页面仅用于演示相同路由在tab中打开多个页面并且显示不同的数据"
+    contentBackground
+    @back="goBack"
+  >
+    <template #extra>
+      <a-button type="primary" danger> 禁用账号 </a-button>
+      <a-button type="primary"> 修改密码 </a-button>
+    </template>
+    <template #footer>
+      <a-tabs default-active-key="detail" v-model:activeKey="currentKey">
+        <a-tab-pane key="detail" tab="用户资料" />
+        <a-tab-pane key="logs" tab="操作日志" />
+      </a-tabs>
+    </template>
+    <div class="pt-4 m-4 desc-wrap">
+      <template v-if="currentKey == 'detail'">
+        <div v-for="i in 10" :key="i">这是用户{{ userId }}资料Tab</div>
+      </template>
+      <template v-if="currentKey == 'logs'">
+        <div v-for="i in 10" :key="i">这是用户{{ userId }}操作日志Tab</div>
+      </template>
+    </div>
+  </PageWrapper>
+</template>
+
+<script>
+  import { defineComponent, ref } from 'vue';
+  import { useRoute } from 'vue-router';
+  import { PageWrapper } from '/@/components/Page';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { useTabs } from '/@/hooks/web/useTabs';
+  import { Tabs } from 'ant-design-vue';
+  export default defineComponent({
+    name: 'AccountDetail',
+    components: { PageWrapper, ATabs: Tabs, ATabPane: Tabs.TabPane },
+    setup() {
+      const route = useRoute();
+      const go = useGo();
+      // 此处可以得到用户ID
+      const userId = ref(route.params?.id);
+      const currentKey = ref('detail');
+      const { setTitle } = useTabs();
+      // TODO
+      // 本页代码仅作演示,实际应当通过userId从接口获得用户的相关资料
+
+      // 设置Tab的标题(不会影响页面标题)
+      setTitle('详情:用户' + userId.value);
+
+      // 页面左侧点击返回链接时的操作
+      function goBack() {
+        // 本例的效果时点击返回始终跳转到账号列表页,实际应用时可返回上一页
+        go('/system/account');
+      }
+      return { userId, currentKey, goBack };
+    },
+  });
+</script>
+
+<style></style>

+ 73 - 0
src/views/dashboard/system/account/AccountModal.vue

@@ -0,0 +1,73 @@
+<template>
+  <BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
+    <BasicForm @register="registerForm" />
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, computed, unref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { accountFormSchema } from './account.data';
+  import { getDeptList } from '/@/api/demo/system';
+
+  export default defineComponent({
+    name: 'AccountModal',
+    components: { BasicModal, BasicForm },
+    emits: ['success', 'register'],
+    setup(_, { emit }) {
+      const isUpdate = ref(true);
+      const rowId = ref('');
+
+      const [registerForm, { setFieldsValue, updateSchema, resetFields, validate }] = useForm({
+        labelWidth: 100,
+        schemas: accountFormSchema,
+        showActionButtonGroup: false,
+        actionColOptions: {
+          span: 23,
+        },
+      });
+
+      const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+        resetFields();
+        setModalProps({ confirmLoading: false });
+        isUpdate.value = !!data?.isUpdate;
+
+        if (unref(isUpdate)) {
+          rowId.value = data.record.id;
+          setFieldsValue({
+            ...data.record,
+          });
+        }
+
+        const treeData = await getDeptList();
+        updateSchema([
+          {
+            field: 'pwd',
+            show: !unref(isUpdate),
+          },
+          {
+            field: 'dept',
+            componentProps: { treeData },
+          },
+        ]);
+      });
+
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增账号' : '编辑账号'));
+
+      async function handleSubmit() {
+        try {
+          const values = await validate();
+          setModalProps({ confirmLoading: true });
+          // TODO custom api
+          console.log(values);
+          closeModal();
+          emit('success', { isUpdate: unref(isUpdate), values: { ...values, id: rowId.value } });
+        } finally {
+          setModalProps({ confirmLoading: false });
+        }
+      }
+
+      return { registerModal, registerForm, getTitle, handleSubmit };
+    },
+  });
+</script>

+ 42 - 0
src/views/dashboard/system/account/DeptTree.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="m-4 mr-0 overflow-hidden bg-white">
+    <BasicTree
+      title="部门列表"
+      toolbar
+      search
+      :clickRowToExpand="false"
+      :treeData="treeData"
+      :fieldNames="{ key: 'id', title: 'deptName' }"
+      @select="handleSelect"
+    />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, onMounted, ref } from 'vue';
+
+  import { BasicTree, TreeItem } from '/@/components/Tree';
+  import { getDeptList } from '/@/api/demo/system';
+
+  export default defineComponent({
+    name: 'DeptTree',
+    components: { BasicTree },
+
+    emits: ['select'],
+    setup(_, { emit }) {
+      const treeData = ref<TreeItem[]>([]);
+
+      async function fetch() {
+        treeData.value = (await getDeptList()) as unknown as TreeItem[];
+      }
+
+      function handleSelect(keys) {
+        emit('select', keys[0]);
+      }
+
+      onMounted(() => {
+        fetch();
+      });
+      return { treeData, handleSelect };
+    },
+  });
+</script>

+ 127 - 0
src/views/dashboard/system/account/account.data.ts

@@ -0,0 +1,127 @@
+import { getAllRoleList, isAccountExist } from '/@/api/demo/system';
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+
+export const columns: BasicColumn[] = [
+  {
+    title: '用户名',
+    dataIndex: 'account',
+    width: 120,
+  },
+  {
+    title: '昵称',
+    dataIndex: 'nickname',
+    width: 120,
+  },
+  {
+    title: '邮箱',
+    dataIndex: 'email',
+    width: 120,
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 180,
+  },
+  {
+    title: '角色',
+    dataIndex: 'role',
+    width: 200,
+  },
+  {
+    title: '备注',
+    dataIndex: 'remark',
+  },
+];
+
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'account',
+    label: '用户名',
+    component: 'Input',
+    colProps: { span: 8 },
+  },
+  {
+    field: 'nickname',
+    label: '昵称',
+    component: 'Input',
+    colProps: { span: 8 },
+  },
+];
+
+export const accountFormSchema: FormSchema[] = [
+  {
+    field: 'account',
+    label: '用户名',
+    component: 'Input',
+    helpMessage: ['本字段演示异步验证', '不能输入带有admin的用户名'],
+    rules: [
+      {
+        required: true,
+        message: '请输入用户名',
+      },
+      {
+        validator(_, value) {
+          return new Promise((resolve, reject) => {
+            isAccountExist(value)
+              .then(() => resolve())
+              .catch((err) => {
+                reject(err.message || '验证失败');
+              });
+          });
+        },
+      },
+    ],
+  },
+  {
+    field: 'pwd',
+    label: '密码',
+    component: 'InputPassword',
+    required: true,
+    ifShow: false,
+  },
+  {
+    label: '角色',
+    field: 'role',
+    component: 'ApiSelect',
+    componentProps: {
+      api: getAllRoleList,
+      labelField: 'roleName',
+      valueField: 'roleValue',
+    },
+    required: true,
+  },
+  {
+    field: 'dept',
+    label: '所属部门',
+    component: 'TreeSelect',
+    componentProps: {
+      fieldNames: {
+        label: 'deptName',
+        key: 'id',
+        value: 'id',
+      },
+      getPopupContainer: () => document.body,
+    },
+    required: true,
+  },
+  {
+    field: 'nickname',
+    label: '昵称',
+    component: 'Input',
+    required: true,
+  },
+
+  {
+    label: '邮箱',
+    field: 'email',
+    component: 'Input',
+    required: true,
+  },
+
+  {
+    label: '备注',
+    field: 'remark',
+    component: 'InputTextArea',
+  },
+];

+ 134 - 0
src/views/dashboard/system/account/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <PageWrapper dense contentFullHeight fixedHeight contentClass="flex">
+    <DeptTree class="w-1/4 xl:w-1/5" @select="handleSelect" />
+    <BasicTable @register="registerTable" class="w-3/4 xl:w-4/5" :searchInfo="searchInfo">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate">新增账号</a-button>
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              icon: 'clarity:info-standard-line',
+              tooltip: '查看用户详情',
+              onClick: handleView.bind(null, record),
+            },
+            {
+              icon: 'clarity:note-edit-line',
+              tooltip: '编辑用户资料',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              icon: 'ant-design:delete-outlined',
+              color: 'error',
+              tooltip: '删除此账号',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <AccountModal @register="registerModal" @success="handleSuccess" />
+  </PageWrapper>
+</template>
+<script lang="ts">
+  import { defineComponent, reactive } from 'vue';
+
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { getAccountList } from '/@/api/demo/system';
+  import { PageWrapper } from '/@/components/Page';
+  import DeptTree from './DeptTree.vue';
+
+  import { useModal } from '/@/components/Modal';
+  import AccountModal from './AccountModal.vue';
+
+  import { columns, searchFormSchema } from './account.data';
+  import { useGo } from '/@/hooks/web/usePage';
+
+  export default defineComponent({
+    name: 'AccountManagement',
+    components: { BasicTable, PageWrapper, DeptTree, AccountModal, TableAction },
+    setup() {
+      const go = useGo();
+      const [registerModal, { openModal }] = useModal();
+      const searchInfo = reactive<Recordable>({});
+      const [registerTable, { reload, updateTableDataRecord }] = useTable({
+        title: '账号列表',
+        api: getAccountList,
+        rowKey: 'id',
+        columns,
+        formConfig: {
+          labelWidth: 120,
+          schemas: searchFormSchema,
+          autoSubmitOnEnter: true,
+        },
+        useSearchForm: true,
+        showTableSetting: true,
+        bordered: true,
+        handleSearchInfoFn(info) {
+          console.log('handleSearchInfoFn', info);
+          return info;
+        },
+        actionColumn: {
+          width: 120,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+        },
+      });
+
+      function handleCreate() {
+        openModal(true, {
+          isUpdate: false,
+        });
+      }
+
+      function handleEdit(record: Recordable) {
+        console.log(record);
+        openModal(true, {
+          record,
+          isUpdate: true,
+        });
+      }
+
+      function handleDelete(record: Recordable) {
+        console.log(record);
+      }
+
+      function handleSuccess({ isUpdate, values }) {
+        if (isUpdate) {
+          // 演示不刷新表格直接更新内部数据。
+          // 注意:updateTableDataRecord要求表格的rowKey属性为string并且存在于每一行的record的keys中
+          const result = updateTableDataRecord(values.id, values);
+          console.log(result);
+        } else {
+          reload();
+        }
+      }
+
+      function handleSelect(deptId = '') {
+        searchInfo.deptId = deptId;
+        reload();
+      }
+
+      function handleView(record: Recordable) {
+        go('/system/account_detail/' + record.id);
+      }
+
+      return {
+        registerTable,
+        registerModal,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        handleSuccess,
+        handleSelect,
+        handleView,
+        searchInfo,
+      };
+    },
+  });
+</script>

+ 61 - 0
src/views/dashboard/system/dept/DeptModal.vue

@@ -0,0 +1,61 @@
+<template>
+  <BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit">
+    <BasicForm @register="registerForm" />
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, computed, unref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { formSchema } from './dept.data';
+
+  import { getDeptList } from '/@/api/demo/system';
+  export default defineComponent({
+    name: 'DeptModal',
+    components: { BasicModal, BasicForm },
+    emits: ['success', 'register'],
+    setup(_, { emit }) {
+      const isUpdate = ref(true);
+
+      const [registerForm, { resetFields, setFieldsValue, updateSchema, validate }] = useForm({
+        labelWidth: 100,
+        schemas: formSchema,
+        showActionButtonGroup: false,
+      });
+
+      const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+        resetFields();
+        setModalProps({ confirmLoading: false });
+        isUpdate.value = !!data?.isUpdate;
+
+        if (unref(isUpdate)) {
+          setFieldsValue({
+            ...data.record,
+          });
+        }
+        const treeData = await getDeptList();
+        updateSchema({
+          field: 'parentDept',
+          componentProps: { treeData },
+        });
+      });
+
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增部门' : '编辑部门'));
+
+      async function handleSubmit() {
+        try {
+          const values = await validate();
+          setModalProps({ confirmLoading: true });
+          // TODO custom api
+          console.log(values);
+          closeModal();
+          emit('success');
+        } finally {
+          setModalProps({ confirmLoading: false });
+        }
+      }
+
+      return { registerModal, registerForm, getTitle, handleSubmit };
+    },
+  });
+</script>

+ 108 - 0
src/views/dashboard/system/dept/dept.data.ts

@@ -0,0 +1,108 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { Tag } from 'ant-design-vue';
+
+export const columns: BasicColumn[] = [
+  {
+    title: '部门名称',
+    dataIndex: 'deptName',
+    width: 160,
+    align: 'left',
+  },
+  {
+    title: '排序',
+    dataIndex: 'orderNo',
+    width: 50,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 80,
+    customRender: ({ record }) => {
+      const status = record.status;
+      const enable = ~~status === 0;
+      const color = enable ? 'green' : 'red';
+      const text = enable ? '启用' : '停用';
+      return h(Tag, { color: color }, () => text);
+    },
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 180,
+  },
+  {
+    title: '备注',
+    dataIndex: 'remark',
+  },
+];
+
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'deptName',
+    label: '部门名称',
+    component: 'Input',
+    colProps: { span: 8 },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'Select',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '停用', value: '1' },
+      ],
+    },
+    colProps: { span: 8 },
+  },
+];
+
+export const formSchema: FormSchema[] = [
+  {
+    field: 'deptName',
+    label: '部门名称',
+    component: 'Input',
+    required: true,
+  },
+  {
+    field: 'parentDept',
+    label: '上级部门',
+    component: 'TreeSelect',
+
+    componentProps: {
+      fieldNames: {
+        label: 'deptName',
+        key: 'id',
+        value: 'id',
+      },
+      getPopupContainer: () => document.body,
+    },
+    required: true,
+  },
+  {
+    field: 'orderNo',
+    label: '排序',
+    component: 'InputNumber',
+    required: true,
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '停用', value: '1' },
+      ],
+    },
+    required: true,
+  },
+  {
+    label: '备注',
+    field: 'remark',
+    component: 'InputTextArea',
+  },
+];

+ 100 - 0
src/views/dashboard/system/dept/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate"> 新增部门 </a-button>
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              icon: 'clarity:note-edit-line',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              icon: 'ant-design:delete-outlined',
+              color: 'error',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <DeptModal @register="registerModal" @success="handleSuccess" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { getDeptList } from '/@/api/demo/system';
+
+  import { useModal } from '/@/components/Modal';
+  import DeptModal from './DeptModal.vue';
+
+  import { columns, searchFormSchema } from './dept.data';
+
+  export default defineComponent({
+    name: 'DeptManagement',
+    components: { BasicTable, DeptModal, TableAction },
+    setup() {
+      const [registerModal, { openModal }] = useModal();
+      const [registerTable, { reload }] = useTable({
+        title: '部门列表',
+        api: getDeptList,
+        columns,
+        formConfig: {
+          labelWidth: 120,
+          schemas: searchFormSchema,
+        },
+        pagination: false,
+        striped: false,
+        useSearchForm: true,
+        showTableSetting: true,
+        bordered: true,
+        showIndexColumn: false,
+        canResize: false,
+        actionColumn: {
+          width: 80,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+          fixed: undefined,
+        },
+      });
+
+      function handleCreate() {
+        openModal(true, {
+          isUpdate: false,
+        });
+      }
+
+      function handleEdit(record: Recordable) {
+        openModal(true, {
+          record,
+          isUpdate: true,
+        });
+      }
+
+      function handleDelete(record: Recordable) {
+        console.log(record);
+      }
+
+      function handleSuccess() {
+        reload();
+      }
+
+      return {
+        registerTable,
+        registerModal,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        handleSuccess,
+      };
+    },
+  });
+</script>

+ 70 - 0
src/views/dashboard/system/menu/MenuDrawer.vue

@@ -0,0 +1,70 @@
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    @register="registerDrawer"
+    showFooter
+    :title="getTitle"
+    width="50%"
+    @ok="handleSubmit"
+  >
+    <BasicForm @register="registerForm" />
+  </BasicDrawer>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, computed, unref } from 'vue';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { formSchema } from './menu.data';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+
+  import { getMenuList } from '/@/api/demo/system';
+
+  export default defineComponent({
+    name: 'MenuDrawer',
+    components: { BasicDrawer, BasicForm },
+    emits: ['success', 'register'],
+    setup(_, { emit }) {
+      const isUpdate = ref(true);
+
+      const [registerForm, { resetFields, setFieldsValue, updateSchema, validate }] = useForm({
+        labelWidth: 100,
+        schemas: formSchema,
+        showActionButtonGroup: false,
+        baseColProps: { lg: 12, md: 24 },
+      });
+
+      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+        resetFields();
+        setDrawerProps({ confirmLoading: false });
+        isUpdate.value = !!data?.isUpdate;
+
+        if (unref(isUpdate)) {
+          setFieldsValue({
+            ...data.record,
+          });
+        }
+        const treeData = await getMenuList();
+        updateSchema({
+          field: 'parentMenu',
+          componentProps: { treeData },
+        });
+      });
+
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增菜单' : '编辑菜单'));
+
+      async function handleSubmit() {
+        try {
+          const values = await validate();
+          setDrawerProps({ confirmLoading: true });
+          // TODO custom api
+          console.log(values);
+          closeDrawer();
+          emit('success');
+        } finally {
+          setDrawerProps({ confirmLoading: false });
+        }
+      }
+
+      return { registerDrawer, registerForm, getTitle, handleSubmit };
+    },
+  });
+</script>

+ 107 - 0
src/views/dashboard/system/menu/index.vue

@@ -0,0 +1,107 @@
+<template>
+  <div>
+    <BasicTable @register="registerTable" @fetch-success="onFetchSuccess">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate"> 新增菜单 </a-button>
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              icon: 'clarity:note-edit-line',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              icon: 'ant-design:delete-outlined',
+              color: 'error',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <MenuDrawer @register="registerDrawer" @success="handleSuccess" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, nextTick } from 'vue';
+
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { getMenuList } from '/@/api/demo/system';
+
+  import { useDrawer } from '/@/components/Drawer';
+  import MenuDrawer from './MenuDrawer.vue';
+
+  import { columns, searchFormSchema } from './menu.data';
+
+  export default defineComponent({
+    name: 'MenuManagement',
+    components: { BasicTable, MenuDrawer, TableAction },
+    setup() {
+      const [registerDrawer, { openDrawer }] = useDrawer();
+      const [registerTable, { reload, expandAll }] = useTable({
+        title: '菜单列表',
+        api: getMenuList,
+        columns,
+        formConfig: {
+          labelWidth: 120,
+          schemas: searchFormSchema,
+        },
+        isTreeTable: true,
+        pagination: false,
+        striped: false,
+        useSearchForm: true,
+        showTableSetting: true,
+        bordered: true,
+        showIndexColumn: false,
+        canResize: false,
+        actionColumn: {
+          width: 80,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+          fixed: undefined,
+        },
+      });
+
+      function handleCreate() {
+        openDrawer(true, {
+          isUpdate: false,
+        });
+      }
+
+      function handleEdit(record: Recordable) {
+        openDrawer(true, {
+          record,
+          isUpdate: true,
+        });
+      }
+
+      function handleDelete(record: Recordable) {
+        console.log(record);
+      }
+
+      function handleSuccess() {
+        reload();
+      }
+
+      function onFetchSuccess() {
+        // 演示默认展开所有表项
+        nextTick(expandAll);
+      }
+
+      return {
+        registerTable,
+        registerDrawer,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        handleSuccess,
+        onFetchSuccess,
+      };
+    },
+  });
+</script>

+ 202 - 0
src/views/dashboard/system/menu/menu.data.ts

@@ -0,0 +1,202 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { Tag } from 'ant-design-vue';
+import { Icon } from '/@/components/Icon';
+
+export const columns: BasicColumn[] = [
+  {
+    title: '菜单名称',
+    dataIndex: 'menuName',
+    width: 200,
+    align: 'left',
+  },
+  {
+    title: '图标',
+    dataIndex: 'icon',
+    width: 50,
+    customRender: ({ record }) => {
+      return h(Icon, { icon: record.icon });
+    },
+  },
+  {
+    title: '权限标识',
+    dataIndex: 'permission',
+    width: 180,
+  },
+  {
+    title: '组件',
+    dataIndex: 'component',
+  },
+  {
+    title: '排序',
+    dataIndex: 'orderNo',
+    width: 50,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 80,
+    customRender: ({ record }) => {
+      const status = record.status;
+      const enable = ~~status === 0;
+      const color = enable ? 'green' : 'red';
+      const text = enable ? '启用' : '停用';
+      return h(Tag, { color: color }, () => text);
+    },
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 180,
+  },
+];
+
+const isDir = (type: string) => type === '0';
+const isMenu = (type: string) => type === '1';
+const isButton = (type: string) => type === '2';
+
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'menuName',
+    label: '菜单名称',
+    component: 'Input',
+    colProps: { span: 8 },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'Select',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '停用', value: '1' },
+      ],
+    },
+    colProps: { span: 8 },
+  },
+];
+
+export const formSchema: FormSchema[] = [
+  {
+    field: 'type',
+    label: '菜单类型',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '目录', value: '0' },
+        { label: '菜单', value: '1' },
+        { label: '按钮', value: '2' },
+      ],
+    },
+    colProps: { lg: 24, md: 24 },
+  },
+  {
+    field: 'menuName',
+    label: '菜单名称',
+    component: 'Input',
+    required: true,
+  },
+
+  {
+    field: 'parentMenu',
+    label: '上级菜单',
+    component: 'TreeSelect',
+    componentProps: {
+      fieldNames: {
+        label: 'menuName',
+        key: 'id',
+        value: 'id',
+      },
+      getPopupContainer: () => document.body,
+    },
+  },
+
+  {
+    field: 'orderNo',
+    label: '排序',
+    component: 'InputNumber',
+    required: true,
+  },
+  {
+    field: 'icon',
+    label: '图标',
+    component: 'IconPicker',
+    required: true,
+    ifShow: ({ values }) => !isButton(values.type),
+  },
+
+  {
+    field: 'routePath',
+    label: '路由地址',
+    component: 'Input',
+    required: true,
+    ifShow: ({ values }) => !isButton(values.type),
+  },
+  {
+    field: 'component',
+    label: '组件路径',
+    component: 'Input',
+    ifShow: ({ values }) => isMenu(values.type),
+  },
+  {
+    field: 'permission',
+    label: '权限标识',
+    component: 'Input',
+    ifShow: ({ values }) => !isDir(values.type),
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '禁用', value: '1' },
+      ],
+    },
+  },
+  {
+    field: 'isExt',
+    label: '是否外链',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '否', value: '0' },
+        { label: '是', value: '1' },
+      ],
+    },
+    ifShow: ({ values }) => !isButton(values.type),
+  },
+
+  {
+    field: 'keepalive',
+    label: '是否缓存',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '否', value: '0' },
+        { label: '是', value: '1' },
+      ],
+    },
+    ifShow: ({ values }) => isMenu(values.type),
+  },
+
+  {
+    field: 'show',
+    label: '是否显示',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '是', value: '0' },
+        { label: '否', value: '1' },
+      ],
+    },
+    ifShow: ({ values }) => !isButton(values.type),
+  },
+];

+ 44 - 0
src/views/dashboard/system/password/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <PageWrapper title="修改当前用户密码" content="修改成功后会自动退出当前登录!">
+    <div class="py-8 bg-white flex flex-col justify-center items-center">
+      <BasicForm @register="register" />
+      <div class="flex justify-center">
+        <a-button @click="resetFields"> 重置 </a-button>
+        <a-button class="!ml-4" type="primary" @click="handleSubmit"> 确认 </a-button>
+      </div>
+    </div>
+  </PageWrapper>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { PageWrapper } from '/@/components/Page';
+  import { BasicForm, useForm } from '/@/components/Form';
+
+  import { formSchema } from './pwd.data';
+  export default defineComponent({
+    name: 'ChangePassword',
+    components: { BasicForm, PageWrapper },
+    setup() {
+      const [register, { validate, resetFields }] = useForm({
+        size: 'large',
+        labelWidth: 100,
+        showActionButtonGroup: false,
+        schemas: formSchema,
+      });
+
+      async function handleSubmit() {
+        try {
+          const values = await validate();
+          const { passwordOld, passwordNew } = values;
+
+          // TODO custom api
+          console.log(passwordOld, passwordNew);
+          // const { router } = useRouter();
+          // router.push(pageEnum.BASE_LOGIN);
+        } catch (error) {}
+      }
+
+      return { register, resetFields, handleSubmit };
+    },
+  });
+</script>

+ 46 - 0
src/views/dashboard/system/password/pwd.data.ts

@@ -0,0 +1,46 @@
+import { FormSchema } from '/@/components/Form';
+
+export const formSchema: FormSchema[] = [
+  {
+    field: 'passwordOld',
+    label: '当前密码',
+    component: 'InputPassword',
+    required: true,
+  },
+  {
+    field: 'passwordNew',
+    label: '新密码',
+    component: 'StrengthMeter',
+    componentProps: {
+      placeholder: '新密码',
+    },
+    rules: [
+      {
+        required: true,
+        message: '请输入新密码',
+      },
+    ],
+  },
+  {
+    field: 'confirmPassword',
+    label: '确认密码',
+    component: 'InputPassword',
+
+    dynamicRules: ({ values }) => {
+      return [
+        {
+          required: true,
+          validator: (_, value) => {
+            if (!value) {
+              return Promise.reject('不能为空');
+            }
+            if (value !== values.passwordNew) {
+              return Promise.reject('两次输入的密码不一致!');
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
+    },
+  },
+];

+ 87 - 0
src/views/dashboard/system/role/RoleDrawer.vue

@@ -0,0 +1,87 @@
+<template>
+  <BasicDrawer
+    v-bind="$attrs"
+    @register="registerDrawer"
+    showFooter
+    :title="getTitle"
+    width="500px"
+    @ok="handleSubmit"
+  >
+    <BasicForm @register="registerForm">
+      <template #menu="{ model, field }">
+        <BasicTree
+          v-model:value="model[field]"
+          :treeData="treeData"
+          :fieldNames="{ title: 'menuName', key: 'id' }"
+          checkable
+          toolbar
+          title="菜单分配"
+        />
+      </template>
+    </BasicForm>
+  </BasicDrawer>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, computed, unref } from 'vue';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { formSchema } from './role.data';
+  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { BasicTree, TreeItem } from '/@/components/Tree';
+
+  import { getMenuList } from '/@/api/demo/system';
+
+  export default defineComponent({
+    name: 'RoleDrawer',
+    components: { BasicDrawer, BasicForm, BasicTree },
+    emits: ['success', 'register'],
+    setup(_, { emit }) {
+      const isUpdate = ref(true);
+      const treeData = ref<TreeItem[]>([]);
+
+      const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
+        labelWidth: 90,
+        schemas: formSchema,
+        showActionButtonGroup: false,
+      });
+
+      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+        resetFields();
+        setDrawerProps({ confirmLoading: false });
+        // 需要在setFieldsValue之前先填充treeData,否则Tree组件可能会报key not exist警告
+        if (unref(treeData).length === 0) {
+          treeData.value = (await getMenuList()) as any as TreeItem[];
+        }
+        isUpdate.value = !!data?.isUpdate;
+
+        if (unref(isUpdate)) {
+          setFieldsValue({
+            ...data.record,
+          });
+        }
+      });
+
+      const getTitle = computed(() => (!unref(isUpdate) ? '新增角色' : '编辑角色'));
+
+      async function handleSubmit() {
+        try {
+          const values = await validate();
+          setDrawerProps({ confirmLoading: true });
+          // TODO custom api
+          console.log(values);
+          closeDrawer();
+          emit('success');
+        } finally {
+          setDrawerProps({ confirmLoading: false });
+        }
+      }
+
+      return {
+        registerDrawer,
+        registerForm,
+        getTitle,
+        handleSubmit,
+        treeData,
+      };
+    },
+  });
+</script>

+ 97 - 0
src/views/dashboard/system/role/index.vue

@@ -0,0 +1,97 @@
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #toolbar>
+        <a-button type="primary" @click="handleCreate"> 新增角色 </a-button>
+      </template>
+      <template #action="{ record }">
+        <TableAction
+          :actions="[
+            {
+              icon: 'clarity:note-edit-line',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              icon: 'ant-design:delete-outlined',
+              color: 'error',
+              popConfirm: {
+                title: '是否确认删除',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+          ]"
+        />
+      </template>
+    </BasicTable>
+    <RoleDrawer @register="registerDrawer" @success="handleSuccess" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { getRoleListByPage } from '/@/api/demo/system';
+
+  import { useDrawer } from '/@/components/Drawer';
+  import RoleDrawer from './RoleDrawer.vue';
+
+  import { columns, searchFormSchema } from './role.data';
+
+  export default defineComponent({
+    name: 'RoleManagement',
+    components: { BasicTable, RoleDrawer, TableAction },
+    setup() {
+      const [registerDrawer, { openDrawer }] = useDrawer();
+      const [registerTable, { reload }] = useTable({
+        title: '角色列表',
+        api: getRoleListByPage,
+        columns,
+        formConfig: {
+          labelWidth: 120,
+          schemas: searchFormSchema,
+        },
+        useSearchForm: true,
+        showTableSetting: true,
+        bordered: true,
+        showIndexColumn: false,
+        actionColumn: {
+          width: 80,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+          fixed: undefined,
+        },
+      });
+
+      function handleCreate() {
+        openDrawer(true, {
+          isUpdate: false,
+        });
+      }
+
+      function handleEdit(record: Recordable) {
+        openDrawer(true, {
+          record,
+          isUpdate: true,
+        });
+      }
+
+      function handleDelete(record: Recordable) {
+        console.log(record);
+      }
+
+      function handleSuccess() {
+        reload();
+      }
+
+      return {
+        registerTable,
+        registerDrawer,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        handleSuccess,
+      };
+    },
+  });
+</script>

+ 124 - 0
src/views/dashboard/system/role/role.data.ts

@@ -0,0 +1,124 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { Switch } from 'ant-design-vue';
+import { setRoleStatus } from '/@/api/demo/system';
+import { useMessage } from '/@/hooks/web/useMessage';
+
+export const columns: BasicColumn[] = [
+  {
+    title: '角色名称',
+    dataIndex: 'roleName',
+    width: 200,
+  },
+  {
+    title: '角色值',
+    dataIndex: 'roleValue',
+    width: 180,
+  },
+  {
+    title: '排序',
+    dataIndex: 'orderNo',
+    width: 50,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 120,
+    customRender: ({ record }) => {
+      if (!Reflect.has(record, 'pendingStatus')) {
+        record.pendingStatus = false;
+      }
+      return h(Switch, {
+        checked: record.status === '1',
+        checkedChildren: '已启用',
+        unCheckedChildren: '已禁用',
+        loading: record.pendingStatus,
+        onChange(checked: boolean) {
+          record.pendingStatus = true;
+          const newStatus = checked ? '1' : '0';
+          const { createMessage } = useMessage();
+          setRoleStatus(record.id, newStatus)
+            .then(() => {
+              record.status = newStatus;
+              createMessage.success(`已成功修改角色状态`);
+            })
+            .catch(() => {
+              createMessage.error('修改角色状态失败');
+            })
+            .finally(() => {
+              record.pendingStatus = false;
+            });
+        },
+      });
+    },
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 180,
+  },
+  {
+    title: '备注',
+    dataIndex: 'remark',
+  },
+];
+
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'roleNme',
+    label: '角色名称',
+    component: 'Input',
+    colProps: { span: 8 },
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'Select',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '停用', value: '1' },
+      ],
+    },
+    colProps: { span: 8 },
+  },
+];
+
+export const formSchema: FormSchema[] = [
+  {
+    field: 'roleName',
+    label: '角色名称',
+    required: true,
+    component: 'Input',
+  },
+  {
+    field: 'roleValue',
+    label: '角色值',
+    required: true,
+    component: 'Input',
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '停用', value: '1' },
+      ],
+    },
+  },
+  {
+    label: '备注',
+    field: 'remark',
+    component: 'InputTextArea',
+  },
+  {
+    label: ' ',
+    field: 'menu',
+    slot: 'menu',
+    component: 'Input',
+  },
+];