feat:增加 menu 菜单的列表(初始化)
							parent
							
								
									5ab0eb163a
								
							
						
					
					
						commit
						09d0cfa87e
					
				| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { requestClient } from '#/api/request';
 | 
			
		|||
 | 
			
		||||
export namespace SystemMenuApi {
 | 
			
		||||
  /** 菜单信息 */
 | 
			
		||||
  export interface MenuVO {
 | 
			
		||||
  export interface SystemMenu {
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    permission: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -23,26 +23,26 @@ export namespace SystemMenuApi {
 | 
			
		|||
 | 
			
		||||
/** 查询菜单(精简)列表 */
 | 
			
		||||
export async function getSimpleMenusList() {
 | 
			
		||||
  return requestClient.get('/system/menu/simple-list');
 | 
			
		||||
  return requestClient.get<SystemMenuApi.SystemMenu[]>('/system/menu/simple-list');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 查询菜单列表 */
 | 
			
		||||
export async function getMenuList(params?: Record<string, any>) {
 | 
			
		||||
  return requestClient.get('/system/menu/list', { params });
 | 
			
		||||
  return requestClient.get<SystemMenuApi.SystemMenu[]>('/system/menu/list', { params });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 获取菜单详情 */
 | 
			
		||||
export async function getMenu(id: number) {
 | 
			
		||||
  return requestClient.get(`/system/menu/get?id=${id}`);
 | 
			
		||||
  return requestClient.get<SystemMenuApi.SystemMenu>(`/system/menu/get?id=${id}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 新增菜单 */
 | 
			
		||||
export async function createMenu(data: SystemMenuApi.MenuVO) {
 | 
			
		||||
export async function createMenu(data: SystemMenuApi.SystemMenu) {
 | 
			
		||||
  return requestClient.post('/system/menu/create', data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 修改菜单 */
 | 
			
		||||
export async function updateMenu(data: SystemMenuApi.MenuVO) {
 | 
			
		||||
export async function updateMenu(data: SystemMenuApi.SystemMenu) {
 | 
			
		||||
  return requestClient.put('/system/menu/update', data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,104 @@
 | 
			
		|||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
 | 
			
		||||
// import type { SystemMenuApi } from '#/api/system/menu';
 | 
			
		||||
 | 
			
		||||
import { $t } from '#/locales';
 | 
			
		||||
 | 
			
		||||
export function getMenuTypeOptions() {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      color: 'processing',
 | 
			
		||||
      label: $t('system.menu.typeCatalog'),
 | 
			
		||||
      value: 'catalog',
 | 
			
		||||
    },
 | 
			
		||||
    { color: 'default', label: $t('system.menu.typeMenu'), value: 'menu' },
 | 
			
		||||
    { color: 'error', label: $t('system.menu.typeButton'), value: 'button' },
 | 
			
		||||
    {
 | 
			
		||||
      color: 'success',
 | 
			
		||||
      label: $t('system.menu.typeEmbedded'),
 | 
			
		||||
      value: 'embedded',
 | 
			
		||||
    },
 | 
			
		||||
    { color: 'warning', label: $t('system.menu.typeLink'), value: 'link' },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useGridColumns(
 | 
			
		||||
  onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>,
 | 
			
		||||
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['columns'] {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      align: 'left',
 | 
			
		||||
      field: 'meta.title',
 | 
			
		||||
      fixed: 'left',
 | 
			
		||||
      slots: { default: 'title' },
 | 
			
		||||
      title: $t('system.menu.menuTitle'),
 | 
			
		||||
      treeNode: true,
 | 
			
		||||
      width: 250,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      align: 'center',
 | 
			
		||||
      cellRender: { name: 'CellTag', options: getMenuTypeOptions() },
 | 
			
		||||
      field: 'type',
 | 
			
		||||
      title: $t('system.menu.type'),
 | 
			
		||||
      width: 100,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      align: 'left',
 | 
			
		||||
      field: 'path',
 | 
			
		||||
      title: $t('system.menu.path'),
 | 
			
		||||
      width: 200,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      align: 'left',
 | 
			
		||||
      field: 'component',
 | 
			
		||||
      formatter: ({ row }) => {
 | 
			
		||||
        switch (row.type) {
 | 
			
		||||
          case 'catalog':
 | 
			
		||||
          case 'menu': {
 | 
			
		||||
            return row.component ?? '';
 | 
			
		||||
          }
 | 
			
		||||
          case 'embedded': {
 | 
			
		||||
            return row.meta?.iframeSrc ?? '';
 | 
			
		||||
          }
 | 
			
		||||
          case 'link': {
 | 
			
		||||
            return row.meta?.link ?? '';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return '';
 | 
			
		||||
      },
 | 
			
		||||
      minWidth: 200,
 | 
			
		||||
      title: $t('system.menu.component'),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      cellRender: { name: 'CellTag' },
 | 
			
		||||
      field: 'status',
 | 
			
		||||
      title: $t('system.menu.status'),
 | 
			
		||||
      width: 100,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      align: 'right',
 | 
			
		||||
      cellRender: {
 | 
			
		||||
        attrs: {
 | 
			
		||||
          nameField: 'name',
 | 
			
		||||
          onClick: onActionClick,
 | 
			
		||||
        },
 | 
			
		||||
        name: 'CellOperation',
 | 
			
		||||
        options: [
 | 
			
		||||
          {
 | 
			
		||||
            code: 'append',
 | 
			
		||||
            text: '新增下级',
 | 
			
		||||
          },
 | 
			
		||||
          'edit', // 默认的编辑按钮
 | 
			
		||||
          'delete', // 默认的删除按钮
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      field: 'operation',
 | 
			
		||||
      fixed: 'right',
 | 
			
		||||
      headerAlign: 'center',
 | 
			
		||||
      showOverflow: false,
 | 
			
		||||
      title: $t('system.menu.operation'),
 | 
			
		||||
      width: 200,
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,156 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import type {
 | 
			
		||||
  OnActionClickParams,
 | 
			
		||||
  VxeTableGridOptions,
 | 
			
		||||
} from '#/adapter/vxe-table';
 | 
			
		||||
import type { SystemMenuApi } from '#/api/system/menu';
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { $t } from '#/locales';
 | 
			
		||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
 | 
			
		||||
import { getMenuList, deleteMenu } from '#/api/system/menu';
 | 
			
		||||
import { SystemMenuTypeEnum } from '#/utils/constants';
 | 
			
		||||
 | 
			
		||||
import { Page, useVbenModal } from '@vben/common-ui';
 | 
			
		||||
import { Button, message } from 'ant-design-vue';
 | 
			
		||||
import { IconifyIcon, Plus } from '@vben/icons';
 | 
			
		||||
 | 
			
		||||
import { useGridColumns } from './data';
 | 
			
		||||
import Form from './modules/form.vue';
 | 
			
		||||
 | 
			
		||||
const [FormModal, formModalApi] = useVbenModal({
 | 
			
		||||
  connectedComponent: Form,
 | 
			
		||||
  destroyOnClose: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/** 编辑菜单 */
 | 
			
		||||
function onEdit(row: SystemMenuApi.SystemMenu) {
 | 
			
		||||
  formModalApi.setData(row).open();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 添加下级菜单 */
 | 
			
		||||
function onAppend(row: SystemMenuApi.SystemMenu) {
 | 
			
		||||
  formModalApi.setData({ pid: row.id }).open();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 创建菜单 */
 | 
			
		||||
function onCreate() {
 | 
			
		||||
  formModalApi.setData({}).open();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 删除菜单 */
 | 
			
		||||
async function onDelete(row: SystemMenuApi.SystemMenu) {
 | 
			
		||||
  const hideLoading = message.loading({
 | 
			
		||||
    content: $t('ui.actionMessage.deleting', [row.name]),
 | 
			
		||||
    duration: 0,
 | 
			
		||||
    key: 'action_process_msg',
 | 
			
		||||
  });
 | 
			
		||||
  try {
 | 
			
		||||
    await deleteMenu(row.id as number);
 | 
			
		||||
    message.success({
 | 
			
		||||
      content: $t('ui.actionMessage.deleteSuccess', [row.name]),
 | 
			
		||||
      key: 'action_process_msg',
 | 
			
		||||
    });
 | 
			
		||||
    onRefresh();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    hideLoading();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 表格操作按钮的回调函数 */
 | 
			
		||||
function onActionClick({
 | 
			
		||||
  code,
 | 
			
		||||
  row,
 | 
			
		||||
}: OnActionClickParams<SystemMenuApi.SystemMenu>) {
 | 
			
		||||
  switch (code) {
 | 
			
		||||
    case 'append': {
 | 
			
		||||
      onAppend(row);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'delete': {
 | 
			
		||||
      onDelete(row);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 'edit': {
 | 
			
		||||
      onEdit(row);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const [Grid, gridApi] = useVbenVxeGrid({
 | 
			
		||||
  gridOptions: {
 | 
			
		||||
    columns: useGridColumns(onActionClick),
 | 
			
		||||
    height: 'auto',
 | 
			
		||||
    keepSource: true,
 | 
			
		||||
    pagerConfig: {
 | 
			
		||||
      enabled: false,
 | 
			
		||||
    },
 | 
			
		||||
    proxyConfig: {
 | 
			
		||||
      ajax: {
 | 
			
		||||
        query: async (_params) => {
 | 
			
		||||
          return await getMenuList();
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    rowConfig: {
 | 
			
		||||
      keyField: 'id',
 | 
			
		||||
    },
 | 
			
		||||
    toolbarConfig: {
 | 
			
		||||
      refresh: { code: 'query' },
 | 
			
		||||
    },
 | 
			
		||||
    treeConfig: {
 | 
			
		||||
      parentField: 'parentId',
 | 
			
		||||
      rowField: 'id',
 | 
			
		||||
      transform: true,
 | 
			
		||||
      reserve: true,
 | 
			
		||||
    },
 | 
			
		||||
  } as VxeTableGridOptions,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/** 刷新表格 */
 | 
			
		||||
function onRefresh() {
 | 
			
		||||
  gridApi.query();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 切换树形展开/收缩状态 */
 | 
			
		||||
const isExpanded = ref(false);
 | 
			
		||||
function toggleExpand() {
 | 
			
		||||
  isExpanded.value = !isExpanded.value;
 | 
			
		||||
  gridApi.grid.setAllTreeExpand(isExpanded.value);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <Page auto-content-height>
 | 
			
		||||
    <FormModal @success="onRefresh" />
 | 
			
		||||
    <Grid>
 | 
			
		||||
      <template #toolbar-tools>
 | 
			
		||||
        <Button type="primary" @click="onCreate">
 | 
			
		||||
          <Plus class="size-5" />
 | 
			
		||||
          {{ $t('ui.actionTitle.create', ['菜单']) }}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button class="ml-2" @click="toggleExpand">
 | 
			
		||||
          {{ isExpanded ? '收缩' : '展开' }}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template #title="{ row }">
 | 
			
		||||
        <div class="flex w-full items-center gap-1">
 | 
			
		||||
          <div class="size-5 flex-shrink-0">
 | 
			
		||||
            <IconifyIcon
 | 
			
		||||
              v-if="row.type === SystemMenuTypeEnum.BUTTON"
 | 
			
		||||
              icon="carbon:square-outline"
 | 
			
		||||
              class="size-full"
 | 
			
		||||
            />
 | 
			
		||||
            <IconifyIcon
 | 
			
		||||
              v-else-if="row.icon"
 | 
			
		||||
              :icon="row.icon || 'carbon:circle-dash'"
 | 
			
		||||
              class="size-full"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <span class="flex-auto">{{ $t(row.name) }}</span>
 | 
			
		||||
          <div class="items-center justify-end"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  </Page>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,473 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import type { ChangeEvent } from 'ant-design-vue/es/_util/EventInterface';
 | 
			
		||||
 | 
			
		||||
import type { Recordable } from '@vben/types';
 | 
			
		||||
 | 
			
		||||
import type { VbenFormSchema } from '#/adapter/form';
 | 
			
		||||
 | 
			
		||||
import { computed, h, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { useVbenDrawer } from '@vben/common-ui';
 | 
			
		||||
import { IconifyIcon } from '@vben/icons';
 | 
			
		||||
import { $te } from '@vben/locales';
 | 
			
		||||
import { getPopupContainer } from '@vben/utils';
 | 
			
		||||
 | 
			
		||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
 | 
			
		||||
 | 
			
		||||
import { useVbenForm, z } from '#/adapter/form';
 | 
			
		||||
// import {
 | 
			
		||||
//   createMenu,
 | 
			
		||||
//   getMenuList,
 | 
			
		||||
//   isMenuNameExists,
 | 
			
		||||
//   isMenuPathExists,
 | 
			
		||||
//   SystemMenuApi,
 | 
			
		||||
//   updateMenu,
 | 
			
		||||
// } from '#/api/system/menu';
 | 
			
		||||
import { $t } from '#/locales';
 | 
			
		||||
// import { componentKeys } from '#/router/routes'; // TODO @芋艿:后续搞
 | 
			
		||||
 | 
			
		||||
import { getMenuTypeOptions } from '../data';
 | 
			
		||||
import { getMenuList } from '#/api/system/menu';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  success: [];
 | 
			
		||||
}>();
 | 
			
		||||
const formData = ref<SystemMenuApi.SystemMenu>();
 | 
			
		||||
const titleSuffix = ref<string>();
 | 
			
		||||
const schema: VbenFormSchema[] = [
 | 
			
		||||
  {
 | 
			
		||||
    component: 'RadioGroup',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      buttonStyle: 'solid',
 | 
			
		||||
      options: getMenuTypeOptions(),
 | 
			
		||||
      optionType: 'button',
 | 
			
		||||
    },
 | 
			
		||||
    defaultValue: 'menu',
 | 
			
		||||
    fieldName: 'type',
 | 
			
		||||
    formItemClass: 'col-span-2 md:col-span-2',
 | 
			
		||||
    label: $t('system.menu.type'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Input',
 | 
			
		||||
    fieldName: 'name',
 | 
			
		||||
    label: $t('system.menu.menuName'),
 | 
			
		||||
    rules: z
 | 
			
		||||
      .string()
 | 
			
		||||
      .min(2, $t('ui.formRules.minLength', [$t('system.menu.menuName'), 2]))
 | 
			
		||||
      .max(30, $t('ui.formRules.maxLength', [$t('system.menu.menuName'), 30]))
 | 
			
		||||
      .refine(
 | 
			
		||||
        async (value: string) => {
 | 
			
		||||
          return !(await isMenuNameExists(value, formData.value?.id));
 | 
			
		||||
        },
 | 
			
		||||
        (value) => ({
 | 
			
		||||
          message: $t('ui.formRules.alreadyExists', [
 | 
			
		||||
            $t('system.menu.menuName'),
 | 
			
		||||
            value,
 | 
			
		||||
          ]),
 | 
			
		||||
        }),
 | 
			
		||||
      ),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'ApiTreeSelect',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      api: getMenuList,
 | 
			
		||||
      class: 'w-full',
 | 
			
		||||
      filterTreeNode(input: string, node: Recordable<any>) {
 | 
			
		||||
        if (!input || input.length === 0) {
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
        const title: string = node.meta?.title ?? '';
 | 
			
		||||
        if (!title) return false;
 | 
			
		||||
        return title.includes(input) || $t(title).includes(input);
 | 
			
		||||
      },
 | 
			
		||||
      getPopupContainer,
 | 
			
		||||
      labelField: 'meta.title',
 | 
			
		||||
      showSearch: true,
 | 
			
		||||
      treeDefaultExpandAll: true,
 | 
			
		||||
      valueField: 'id',
 | 
			
		||||
      childrenField: 'children',
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'pid',
 | 
			
		||||
    label: $t('system.menu.parent'),
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        title({ label, meta }: { label: string; meta: Recordable<any> }) {
 | 
			
		||||
          const coms = [];
 | 
			
		||||
          if (!label) return '';
 | 
			
		||||
          if (meta?.icon) {
 | 
			
		||||
            coms.push(h(IconifyIcon, { class: 'size-4', icon: meta.icon }));
 | 
			
		||||
          }
 | 
			
		||||
          coms.push(h('span', { class: '' }, $t(label || '')));
 | 
			
		||||
          return h('div', { class: 'flex items-center gap-1' }, coms);
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Input',
 | 
			
		||||
    componentProps() {
 | 
			
		||||
      // 不需要处理多语言时就无需这么做
 | 
			
		||||
      return {
 | 
			
		||||
        addonAfter: titleSuffix.value,
 | 
			
		||||
        onChange({ target: { value } }: ChangeEvent) {
 | 
			
		||||
          titleSuffix.value = value && $te(value) ? $t(value) : undefined;
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.title',
 | 
			
		||||
    label: $t('system.menu.menuTitle'),
 | 
			
		||||
    rules: 'required',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Input',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['catalog', 'embedded', 'menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'path',
 | 
			
		||||
    label: $t('system.menu.path'),
 | 
			
		||||
    rules: z
 | 
			
		||||
      .string()
 | 
			
		||||
      .min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
 | 
			
		||||
      .max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
 | 
			
		||||
      .refine(
 | 
			
		||||
        (value: string) => {
 | 
			
		||||
          return value.startsWith('/');
 | 
			
		||||
        },
 | 
			
		||||
        $t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
 | 
			
		||||
      )
 | 
			
		||||
      .refine(
 | 
			
		||||
        async (value: string) => {
 | 
			
		||||
          return !(await isMenuPathExists(value, formData.value?.id));
 | 
			
		||||
        },
 | 
			
		||||
        (value) => ({
 | 
			
		||||
          message: $t('ui.formRules.alreadyExists', [
 | 
			
		||||
            $t('system.menu.path'),
 | 
			
		||||
            value,
 | 
			
		||||
          ]),
 | 
			
		||||
        }),
 | 
			
		||||
      ),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Input',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['embedded', 'menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'activePath',
 | 
			
		||||
    help: $t('system.menu.activePathHelp'),
 | 
			
		||||
    label: $t('system.menu.activePath'),
 | 
			
		||||
    rules: z
 | 
			
		||||
      .string()
 | 
			
		||||
      .min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
 | 
			
		||||
      .max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
 | 
			
		||||
      .refine(
 | 
			
		||||
        (value: string) => {
 | 
			
		||||
          return value.startsWith('/');
 | 
			
		||||
        },
 | 
			
		||||
        $t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
 | 
			
		||||
      )
 | 
			
		||||
      .refine(async (value: string) => {
 | 
			
		||||
        return await isMenuPathExists(value, formData.value?.id);
 | 
			
		||||
      }, $t('system.menu.activePathMustExist'))
 | 
			
		||||
      .optional(),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'IconPicker',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      prefix: 'carbon',
 | 
			
		||||
    },
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['catalog', 'embedded', 'link', 'menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.icon',
 | 
			
		||||
    label: $t('system.menu.icon'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'IconPicker',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      prefix: 'carbon',
 | 
			
		||||
    },
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['catalog', 'embedded', 'menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.activeIcon',
 | 
			
		||||
    label: $t('system.menu.activeIcon'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'AutoComplete',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      allowClear: true,
 | 
			
		||||
      class: 'w-full',
 | 
			
		||||
      filterOption(input: string, option: { value: string }) {
 | 
			
		||||
        return option.value.toLowerCase().includes(input.toLowerCase());
 | 
			
		||||
      },
 | 
			
		||||
      // options: componentKeys.map((v) => ({ value: v })), // TODO @芋艿:后续完善
 | 
			
		||||
    },
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      rules: (values) => {
 | 
			
		||||
        return values.type === 'menu' ? 'required' : null;
 | 
			
		||||
      },
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return values.type === 'menu';
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'component',
 | 
			
		||||
    label: $t('system.menu.component'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Input',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['embedded', 'link'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'linkSrc',
 | 
			
		||||
    label: $t('system.menu.linkSrc'),
 | 
			
		||||
    rules: z.string().url($t('ui.formRules.invalidURL')),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'RadioGroup',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      buttonStyle: 'solid',
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: $t('common.enabled'), value: 1 },
 | 
			
		||||
        { label: $t('common.disabled'), value: 0 },
 | 
			
		||||
      ],
 | 
			
		||||
      optionType: 'button',
 | 
			
		||||
    },
 | 
			
		||||
    defaultValue: 1,
 | 
			
		||||
    fieldName: 'status',
 | 
			
		||||
    label: $t('system.menu.status'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Select',
 | 
			
		||||
    componentProps: {
 | 
			
		||||
      allowClear: true,
 | 
			
		||||
      class: 'w-full',
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: $t('system.menu.badgeType.dot'), value: 'dot' },
 | 
			
		||||
        { label: $t('system.menu.badgeType.normal'), value: 'normal' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return values.type !== 'button';
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.badgeType',
 | 
			
		||||
    label: $t('system.menu.badgeType.title'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Input',
 | 
			
		||||
    componentProps: (values) => {
 | 
			
		||||
      return {
 | 
			
		||||
        allowClear: true,
 | 
			
		||||
        class: 'w-full',
 | 
			
		||||
        disabled: values.meta?.badgeType !== 'normal',
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return values.type !== 'button';
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.badge',
 | 
			
		||||
    label: $t('system.menu.badge'),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Divider',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return !['button', 'link'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'divider1',
 | 
			
		||||
    formItemClass: 'col-span-2 md:col-span-2 pb-0',
 | 
			
		||||
    hideLabel: true,
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.advancedSettings'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Checkbox',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.keepAlive',
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.keepAlive'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Checkbox',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['embedded', 'menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.affixTab',
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.affixTab'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Checkbox',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return !['button'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.hideInMenu',
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.hideInMenu'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Checkbox',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return ['catalog', 'menu'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.hideChildrenInMenu',
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.hideChildrenInMenu'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Checkbox',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return !['button', 'link'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.hideInBreadcrumb',
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.hideInBreadcrumb'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    component: 'Checkbox',
 | 
			
		||||
    dependencies: {
 | 
			
		||||
      show: (values) => {
 | 
			
		||||
        return !['button', 'link'].includes(values.type);
 | 
			
		||||
      },
 | 
			
		||||
      triggerFields: ['type'],
 | 
			
		||||
    },
 | 
			
		||||
    fieldName: 'meta.hideInTab',
 | 
			
		||||
    renderComponentContent() {
 | 
			
		||||
      return {
 | 
			
		||||
        default: () => $t('system.menu.hideInTab'),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const breakpoints = useBreakpoints(breakpointsTailwind);
 | 
			
		||||
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
 | 
			
		||||
 | 
			
		||||
const [Form, formApi] = useVbenForm({
 | 
			
		||||
  commonConfig: {
 | 
			
		||||
    colon: true,
 | 
			
		||||
    formItemClass: 'col-span-2 md:col-span-1',
 | 
			
		||||
  },
 | 
			
		||||
  schema,
 | 
			
		||||
  showDefaultActions: false,
 | 
			
		||||
  wrapperClass: 'grid-cols-2 gap-x-4',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [Drawer, drawerApi] = useVbenDrawer({
 | 
			
		||||
  onConfirm: onSubmit,
 | 
			
		||||
  onOpenChange(isOpen) {
 | 
			
		||||
    if (isOpen) {
 | 
			
		||||
      const data = drawerApi.getData<SystemMenuApi.SystemMenu>();
 | 
			
		||||
      if (data?.type === 'link') {
 | 
			
		||||
        data.linkSrc = data.meta?.link;
 | 
			
		||||
      } else if (data?.type === 'embedded') {
 | 
			
		||||
        data.linkSrc = data.meta?.iframeSrc;
 | 
			
		||||
      }
 | 
			
		||||
      if (data) {
 | 
			
		||||
        formData.value = data;
 | 
			
		||||
        formApi.setValues(formData.value);
 | 
			
		||||
        titleSuffix.value = formData.value.meta?.title
 | 
			
		||||
          ? $t(formData.value.meta.title)
 | 
			
		||||
          : '';
 | 
			
		||||
      } else {
 | 
			
		||||
        formApi.resetForm();
 | 
			
		||||
        titleSuffix.value = '';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function onSubmit() {
 | 
			
		||||
  const { valid } = await formApi.validate();
 | 
			
		||||
  if (valid) {
 | 
			
		||||
    drawerApi.lock();
 | 
			
		||||
    const data =
 | 
			
		||||
      await formApi.getValues<
 | 
			
		||||
        Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>
 | 
			
		||||
      >();
 | 
			
		||||
    if (data.type === 'link') {
 | 
			
		||||
      data.meta = { ...data.meta, link: data.linkSrc };
 | 
			
		||||
    } else if (data.type === 'embedded') {
 | 
			
		||||
      data.meta = { ...data.meta, iframeSrc: data.linkSrc };
 | 
			
		||||
    }
 | 
			
		||||
    delete data.linkSrc;
 | 
			
		||||
    try {
 | 
			
		||||
      await (formData.value?.id
 | 
			
		||||
        ? updateMenu(formData.value.id, data)
 | 
			
		||||
        : createMenu(data));
 | 
			
		||||
      drawerApi.close();
 | 
			
		||||
      emit('success');
 | 
			
		||||
    } finally {
 | 
			
		||||
      drawerApi.unlock();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
const getDrawerTitle = computed(() =>
 | 
			
		||||
  formData.value?.id
 | 
			
		||||
    ? $t('ui.actionTitle.edit', ['菜单'])
 | 
			
		||||
    : $t('ui.actionTitle.create', ['菜单']),
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <Drawer class="w-full max-w-[800px]" :title="getDrawerTitle">
 | 
			
		||||
    <Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
 | 
			
		||||
  </Drawer>
 | 
			
		||||
</template>
 | 
			
		||||
		Loading…
	
		Reference in New Issue