Browse Source

Merge pull request #1303 from colinin/enhance-favorite-menu

feat(vben5-platform): Enhance favorite menu
pull/1312/head
yx lin 1 year ago
committed by GitHub
parent
commit
c8c3d1ea9c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      apps/vben5/apps/app-antd/package.json
  2. 8
      apps/vben5/apps/app-antd/src/adapter/component/index.ts
  3. 1
      apps/vben5/packages/@abp/platform/package.json
  4. 34
      apps/vben5/packages/@abp/platform/src/api/useMyFavoriteMenusApi.ts
  5. 106
      apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNav.vue
  6. 137
      apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNavModal.vue
  7. 29
      apps/vben5/packages/@abp/platform/src/components/workbench/index.vue
  8. 9
      apps/vben5/packages/@abp/platform/src/locales/langs/en-US/workbench.json
  9. 9
      apps/vben5/packages/@abp/platform/src/locales/langs/zh-CN/workbench.json
  10. 1
      apps/vben5/pnpm-workspace.yaml

3
apps/vben5/apps/app-antd/package.json

@ -66,6 +66,7 @@
"dayjs": "catalog:", "dayjs": "catalog:",
"pinia": "catalog:", "pinia": "catalog:",
"vue": "catalog:", "vue": "catalog:",
"vue-router": "catalog:" "vue-router": "catalog:",
"vue3-colorpicker": "catalog:"
} }
} }

8
apps/vben5/apps/app-antd/src/adapter/component/index.ts

@ -31,6 +31,12 @@ const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent( const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'), () => import('ant-design-vue/es/checkbox'),
); );
const ColorPicker = defineAsyncComponent(() =>
import('vue3-colorpicker').then((res) => {
import('vue3-colorpicker/style.css');
return res.ColorPicker;
}),
);
const CheckboxGroup = defineAsyncComponent(() => const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup), import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
); );
@ -117,6 +123,7 @@ export type ComponentType =
| 'AutoComplete' | 'AutoComplete'
| 'Checkbox' | 'Checkbox'
| 'CheckboxGroup' | 'CheckboxGroup'
| 'ColorPicker'
| 'DatePicker' | 'DatePicker'
| 'DefaultButton' | 'DefaultButton'
| 'Divider' | 'Divider'
@ -182,6 +189,7 @@ async function initComponentAdapter() {
AutoComplete, AutoComplete,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,
ColorPicker,
DatePicker, DatePicker,
// 自定义默认按钮 // 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => { DefaultButton: (props, { attrs, slots }) => {

1
apps/vben5/packages/@abp/platform/package.json

@ -39,6 +39,7 @@
"ant-design-vue": "catalog:", "ant-design-vue": "catalog:",
"lodash.clonedeep": "catalog:", "lodash.clonedeep": "catalog:",
"vue": "catalog:*", "vue": "catalog:*",
"vue3-colorpicker": "catalog:",
"vxe-table": "catalog:" "vxe-table": "catalog:"
}, },
"devDependencies": { "devDependencies": {

34
apps/vben5/packages/@abp/platform/src/api/useMyFavoriteMenusApi.ts

@ -1,12 +1,42 @@
import type { ListResultDto } from '@abp/core'; import type { ListResultDto } from '@abp/core';
import type { UserFavoriteMenuDto } from '../types/favorites'; import type {
UserFavoriteMenuCreateDto,
UserFavoriteMenuDto,
} from '../types/favorites';
import { useRequest } from '@abp/request'; import { useRequest } from '@abp/request';
export function useMyFavoriteMenusApi() { export function useMyFavoriteMenusApi() {
const { cancel, request } = useRequest(); const { cancel, request } = useRequest();
/**
* 新增常用菜单
* @param input 参数
* @returns 常用菜单
*/
function createApi(
input: UserFavoriteMenuCreateDto,
): Promise<UserFavoriteMenuDto> {
return request<UserFavoriteMenuDto>(
`/api/platform/menus/favorites/my-favorite-menus`,
{
data: input,
method: 'POST',
},
);
}
/**
* 删除常用菜单
* @param id 菜单Id
*/
function deleteApi(id: string): Promise<void> {
return request(`/api/platform/menus/favorites/my-favorite-menus/${id}`, {
method: 'DELETE',
});
}
/** /**
* 获取常用菜单列表 * 获取常用菜单列表
* @param framework ui框架 * @param framework ui框架
@ -25,6 +55,8 @@ export function useMyFavoriteMenusApi() {
return { return {
cancel, cancel,
createApi,
deleteApi,
getListApi, getListApi,
}; };
} }

106
apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNav.vue

@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FavoriteMenu } from '../types'; import type { FavoriteMenu } from '../types';
import { computed, h } from 'vue';
import { $t } from '@vben/locales';
import { import {
Card, Card,
CardContent, CardContent,
@ -9,6 +13,9 @@ import {
VbenIcon, VbenIcon,
} from '@vben-core/shadcn-ui'; } from '@vben-core/shadcn-ui';
import { DeleteOutlined } from '@ant-design/icons-vue';
import { Dropdown, Menu, Modal } from 'ant-design-vue';
interface Props { interface Props {
items?: FavoriteMenu[]; items?: FavoriteMenu[];
title: string; title: string;
@ -18,13 +25,53 @@ defineOptions({
name: 'WorkbenchQuickNav', name: 'WorkbenchQuickNav',
}); });
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
items: () => [], items: () => [],
}); });
defineEmits<{ const emits = defineEmits<{
(event: 'click', menu: FavoriteMenu): void; (event: 'click', menu: FavoriteMenu): void;
(event: 'delete', menu: FavoriteMenu): void;
(event: 'add'): void;
}>(); }>();
const MenuItem = Menu.Item;
const getFavoriteMenus = computed(() => {
const addMenu: FavoriteMenu = {
id: 'addMenu',
displayName: $t('workbench.content.favoriteMenu.create'),
icon: 'ion:add-outline',
color: '#00bfff',
isDefault: true,
};
return [...props.items, addMenu];
});
function onClick(menu: FavoriteMenu) {
if (menu.id === 'addMenu') {
emits('add');
return;
}
emits('click', menu);
}
function onMenuClick(key: string, menu: FavoriteMenu) {
switch (key) {
case 'delete': {
Modal.confirm({
centered: true,
iconType: 'warning',
title: $t('AbpUi.AreYouSure'),
content: $t('AbpUi.ItemWillBeDeletedMessage'),
okCancel: true,
onOk: () => {
emits('delete', menu);
},
});
}
}
}
</script> </script>
<template> <template>
@ -33,25 +80,42 @@ defineEmits<{
<CardTitle class="text-lg">{{ title }}</CardTitle> <CardTitle class="text-lg">{{ title }}</CardTitle>
</CardHeader> </CardHeader>
<CardContent class="flex flex-wrap p-0"> <CardContent class="flex flex-wrap p-0">
<template v-for="(item, index) in items" :key="item.displayName"> <template
<div v-for="(item, index) in getFavoriteMenus"
:class="{ :key="item.displayName"
'border-r-0': index % 3 === 2, >
'border-b-0': index < 3, <Dropdown :trigger="['contextmenu']">
'pb-4': index > 2, <div
'rounded-bl-xl': index === items.length - 3, :class="{
'rounded-br-xl': index === items.length - 1, 'border-r-0': index % 3 === 2,
}" 'border-b-0': index < 3,
class="flex-col-center border-border group w-1/3 cursor-pointer border-r border-t py-8 hover:shadow-xl" 'pb-4': index > 2,
> 'rounded-bl-xl': index === items.length - 3,
<VbenIcon 'rounded-br-xl': index === items.length - 1,
:color="item.color" }"
:icon="item.icon" class="flex-col-center border-border group w-1/3 cursor-pointer border-r border-t py-8 hover:shadow-xl"
class="size-7 transition-all duration-300 group-hover:scale-125" @click="onClick(item)"
@click="$emit('click', item)" >
/> <VbenIcon
<span class="text-md mt-2 truncate">{{ item.displayName }}</span> :color="item.color"
</div> :icon="item.icon"
class="size-7 transition-all duration-300 group-hover:scale-125"
/>
<span class="text-md mt-2 truncate">{{ item.displayName }}</span>
</div>
<template #overlay>
<Menu
v-if="!item.isDefault"
@click="
({ key: menuKey }) => onMenuClick(menuKey.toString(), item)
"
>
<MenuItem key="delete" :icon="h(DeleteOutlined)">
{{ $t('workbench.content.favoriteMenu.delete') }}
</MenuItem>
</Menu>
</template>
</Dropdown>
</template> </template>
</CardContent> </CardContent>
</Card> </Card>

137
apps/vben5/packages/@abp/platform/src/components/workbench/components/WorkbenchQuickNavModal.vue

@ -0,0 +1,137 @@
<script setup lang="ts">
import type { MenuDto, UserFavoriteMenuDto } from '../../../types';
import { defineAsyncComponent, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { listToTree } from '@abp/core';
import { message, TreeSelect } from 'ant-design-vue';
import { useMyFavoriteMenusApi } from '../../../api/useMyFavoriteMenusApi';
import { useMyMenusApi } from '../../../api/useMyMenusApi';
const emits = defineEmits<{
(event: 'change', data: UserFavoriteMenuDto): void;
}>();
const ColorPicker = defineAsyncComponent(() =>
import('vue3-colorpicker').then((res) => {
import('vue3-colorpicker/style.css');
return res.ColorPicker;
}),
);
const availableMenus = ref<MenuDto[]>([]);
const { getAllApi } = useMyMenusApi();
const { createApi } = useMyFavoriteMenusApi();
const { uiFramework } = useAppConfig(import.meta.env, import.meta.env.PROD);
const [Form, formApi] = useVbenForm({
schema: [
{
label: $t('workbench.content.favoriteMenu.select'),
fieldName: 'menuId',
component: 'TreeSelect',
rules: 'selectRequired',
},
{
label: $t('workbench.content.favoriteMenu.color'),
fieldName: 'color',
component: 'ColorPicker',
defaultValue: '#000000',
modelPropName: 'pureColor',
},
{
label: $t('workbench.content.favoriteMenu.alias'),
fieldName: 'aliasName',
component: 'Input',
},
{
label: $t('workbench.content.favoriteMenu.icon'),
fieldName: 'icon',
component: 'IconPicker',
},
],
showDefaultActions: false,
handleSubmit: onSubmit,
commonConfig: {
colon: true,
componentProps: {
class: 'w-full',
},
},
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formApi.validateAndSubmitForm();
},
async onOpenChange(isOpen) {
if (isOpen) {
await onInitMenus();
}
},
});
async function onInitMenus() {
const { items } = await getAllApi({
framework: uiFramework,
});
const menus = listToTree<MenuDto>(items, { id: 'id', pid: 'parentId' });
availableMenus.value = menus;
}
async function onSubmit(values: Record<string, any>) {
try {
modalApi.setState({ submitting: true });
const menuDto = await createApi({
framework: uiFramework,
menuId: values.menuId,
color: values.color,
icon: values.icon,
aliasName: values.aliasName,
});
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', menuDto);
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
}
</script>
<template>
<Modal :title="$t('workbench.content.favoriteMenu.manage')">
<Form>
<template #color="slotProps">
<div class="flex flex-row items-center">
<ColorPicker v-bind="slotProps" format="hex" />
<span>({{ slotProps.value }})</span>
</div>
</template>
<template #menuId="slotProps">
<TreeSelect
allow-clear
class="w-full"
tree-icon
v-bind="slotProps"
:field-names="{ label: 'displayName', value: 'id' }"
:tree-data="availableMenus"
>
<template #title="item">
<div class="flex flex-row items-center gap-1">
<IconifyIcon v-if="item.meta?.icon" :icon="item.meta.icon" />
<span>{{ item.displayName }}</span>
</div>
</template>
</TreeSelect>
</template>
</Form>
</Modal>
</template>
<style scoped></style>

29
apps/vben5/packages/@abp/platform/src/components/workbench/index.vue

@ -3,8 +3,9 @@ import type { WorkbenchTodoItem, WorkbenchTrendItem } from '@vben/common-ui';
import type { FavoriteMenu } from './types'; import type { FavoriteMenu } from './types';
import { computed, onMounted, ref } from 'vue'; import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks'; import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences'; import { preferences } from '@vben/preferences';
@ -16,7 +17,7 @@ import {
useMyNotifilersApi, useMyNotifilersApi,
useNotificationSerializer, useNotificationSerializer,
} from '@abp/notifications'; } from '@abp/notifications';
import { Empty } from 'ant-design-vue'; import { Empty, message } from 'ant-design-vue';
import { useMyFavoriteMenusApi } from '../../api/useMyFavoriteMenusApi'; import { useMyFavoriteMenusApi } from '../../api/useMyFavoriteMenusApi';
import WorkbenchHeader from './components/WorkbenchHeader.vue'; import WorkbenchHeader from './components/WorkbenchHeader.vue';
@ -30,7 +31,8 @@ defineEmits<{
const userStore = useUserStore(); const userStore = useUserStore();
const { getMyNotifilersApi } = useMyNotifilersApi(); const { getMyNotifilersApi } = useMyNotifilersApi();
const { getListApi: getFavoriteMenusApi } = useMyFavoriteMenusApi(); const { getListApi: getFavoriteMenusApi, deleteApi: deleteFavoriteMenuApi } =
useMyFavoriteMenusApi();
const { deserialize } = useNotificationSerializer(); const { deserialize } = useNotificationSerializer();
const { uiFramework } = useAppConfig(import.meta.env, import.meta.env.PROD); const { uiFramework } = useAppConfig(import.meta.env, import.meta.env.PROD);
@ -107,6 +109,13 @@ const getWelcomeTitle = computed(() => {
} }
return ''; return '';
}); });
const [WorkbenchQuickNavModal, quickNavModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./components/WorkbenchQuickNavModal.vue'),
),
});
async function onInit() { async function onInit() {
await Promise.all([ await Promise.all([
onInitFavoriteMenus(), onInitFavoriteMenus(),
@ -119,6 +128,7 @@ async function onInitFavoriteMenus() {
favoriteMenus.value = items.map((item) => { favoriteMenus.value = items.map((item) => {
return { return {
...item, ...item,
id: item.menuId,
isDefault: false, isDefault: false,
}; };
}); });
@ -144,6 +154,16 @@ async function onInitTodoList() {
todoList.value = []; todoList.value = [];
} }
function onCreatingFavoriteMenu() {
quickNavModalApi.open();
}
async function onDeleteFavoriteMenu(menu: FavoriteMenu) {
await deleteFavoriteMenuApi(menu.id);
await onInitFavoriteMenus();
message.success($t('AbpUi.SuccessfullyDeleted'));
}
onMounted(onInit); onMounted(onInit);
</script> </script>
@ -166,6 +186,8 @@ onMounted(onInit);
:items="getFavoriteMenus" :items="getFavoriteMenus"
class="mt-5 lg:mt-0" class="mt-5 lg:mt-0"
:title="$t('workbench.content.favoriteMenu.title')" :title="$t('workbench.content.favoriteMenu.title')"
@add="onCreatingFavoriteMenu"
@delete="onDeleteFavoriteMenu"
@click="(menu: FavoriteMenu) => $emit('navTo', menu)" @click="(menu: FavoriteMenu) => $emit('navTo', menu)"
/> />
<WorkbenchTodo <WorkbenchTodo
@ -189,6 +211,7 @@ onMounted(onInit);
</WorkbenchTrends> </WorkbenchTrends>
</div> </div>
</div> </div>
<WorkbenchQuickNavModal @change="onInitFavoriteMenus" />
</div> </div>
</template> </template>

9
apps/vben5/packages/@abp/platform/src/locales/langs/en-US/workbench.json

@ -18,7 +18,14 @@
"dashboard": "Dashboard", "dashboard": "Dashboard",
"profile": "Personal Profile", "profile": "Personal Profile",
"settings": "Personal Settings", "settings": "Personal Settings",
"notifiers": "Notifiers" "notifiers": "Notifiers",
"manage": "Manage menu",
"create": "New menu",
"delete": "Delete Menu",
"select": "Select Menu",
"color": "Select Color",
"alias": "Alias Name",
"icon": "Icon"
}, },
"trends": { "trends": {
"title": "Latest News" "title": "Latest News"

9
apps/vben5/packages/@abp/platform/src/locales/langs/zh-CN/workbench.json

@ -18,7 +18,14 @@
"dashboard": "仪表盘", "dashboard": "仪表盘",
"profile": "个人中心", "profile": "个人中心",
"settings": "个人设置", "settings": "个人设置",
"notifiers": "通知消息" "notifiers": "通知消息",
"manage": "管理菜单",
"create": "添加菜单",
"delete": "删除菜单",
"select": "选择菜单",
"color": "选择颜色",
"alias": "自定义别名",
"icon": "自定义图标"
}, },
"trends": { "trends": {
"title": "最新消息" "title": "最新消息"

1
apps/vben5/pnpm-workspace.yaml

@ -205,6 +205,7 @@ catalog:
vue-simple-uploader: ^1.0.3 vue-simple-uploader: ^1.0.3
vue-tippy: ^6.7.0 vue-tippy: ^6.7.0
vue-tsc: 2.2.10 vue-tsc: 2.2.10
vue3-colorpicker: ^2.3.0
vxe-pc-ui: ^4.7.12 vxe-pc-ui: ^4.7.12
vxe-table: ^4.14.4 vxe-table: ^4.14.4
watermark-js-plus: ^1.6.0 watermark-js-plus: ^1.6.0

Loading…
Cancel
Save