优化功能
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
李琦
2026-01-15 13:09:58 +08:00
parent 0245f8ff20
commit 721c5ca610
19 changed files with 1986 additions and 29 deletions

View File

@@ -65,7 +65,9 @@ const downloadQRCode = async (name: string, isQr = false) => {
// 创建a标签下载
const link = document.createElement('a'); // 创建a标签
link.href = canvas.toDataURL(); // 是canvas对象的一种方法用于将canvas对象转换为base64位编码
link.setAttribute('download', `${doctorInfo.value.doctor.name}${name}.png`); // 利用了a标签的download 来下载 canvas图片
// 使用推广员名称或门店名称作为文件名
const fileName = data.value?.nick_name || data.value?.qr_code?.store?.name || '推广二维码';
link.setAttribute('download', `${fileName}${name}.png`); // 利用了a标签的download 来下载 canvas图片
link.style.display = 'none'; // 将图片隐藏起来
document.body.append(link); // 插入到其中
link.click();

View File

@@ -24,6 +24,7 @@ import { openWindow } from '@vben/utils';
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
import {getQuickMenuApi} from "#/views/system/admin/api";
import {getLatestNotice} from "#/views/system/notice/api";
const userStore = useUserStore();
@@ -95,32 +96,17 @@ const todoItems = ref<WorkbenchTodoItem[]>([
title: '修复UI显示问题',
},
]);
const trendItems: WorkbenchTrendItem[] = [
{
avatar: 'svg:logo',
content: `药师审方功能上线啦!`,
date: '2025-5-26',
title: '萧康云医',
},
{
avatar: 'svg:logo',
content: `诊所、医生、药师基础功能已完成`,
date: '2025-4-30',
title: '萧康云医',
},
{
avatar: 'svg:logo',
content: `医生开方功能上线啦!`,
date: '2025-3-11',
title: '萧康云医',
},
{
avatar: 'svg:logo',
content: `萧康云医上线啦!`,
date: '2025-1-7',
title: '萧康云医',
},
];
const trendItems = ref<WorkbenchTrendItem[]>([]);
/**
* 获取最新公告
*/
function getLatestNoticeList() {
getLatestNotice(10).then((res) => {
trendItems.value = res;
});
}
getLatestNoticeList();
const router = useRouter();

View File

@@ -42,6 +42,14 @@ export async function getMenuTreeOptionSelect() {
});
}
/**
* 搜索菜单(支持按 title 模糊搜索)
* @param data 搜索参数 { keyword: string, filter_by_user?: number }
*/
export async function searchMenu(data: { keyword: string; filter_by_user?: number }) {
return requestClient.get<any>(`${prefix}search-menu`, { params: data });
}
/**
* 获取详情
* @param id

View File

@@ -0,0 +1,50 @@
import { requestClient } from '#/api/request';
const prefix = 'announcement/';
/**
* 分页查询公告列表
* @param data
*/
export async function getNoticeList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取最新公告(用于工作台)
* @param limit
*/
export async function getLatestNotice(limit: number = 10) {
return requestClient.get<any>(`${prefix}latest`, { params: { limit } });
}
/**
* 获取详情
* @param id
*/
export async function getNoticeInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 新增公告
* @param data
*/
export async function createNotice(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑公告
* @param data
*/
export async function updateNotice(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除公告
* @param data
*/
export async function deleteNotice(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createNotice, updateNotice } from '../api';
import { modalFormProps } from '../config/form';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value ? updateNotice : createNotice;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
if (values) {
isUpdate.value = update;
formApi.setValues(values);
}
}
},
});
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}公告`"
class="w-[60%]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,93 @@
import type { VbenFormProps } from '#/adapter/form';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
formItemClass: 'col-span-6',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入标题',
},
fieldName: 'title',
label: '标题',
rules: 'required',
},
{
component: 'VbenTextarea',
componentProps: {
placeholder: '请输入内容',
rows: 4,
},
fieldName: 'content',
label: '内容',
formItemClass: 'col-span-12',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入头像/图标svg:logo',
},
fieldName: 'avatar',
label: '头像/图标',
defaultValue: 'svg:logo',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入日期格式2025-5-26',
},
fieldName: 'date',
label: '日期',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入排序',
},
defaultValue: 0,
fieldName: 'sort',
label: '排序',
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '启用',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
defaultValue: 1,
fieldName: 'status',
label: '状态',
rules: 'required',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,45 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-6',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入标题',
},
fieldName: 'title',
label: '标题',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '全部',
value: '',
},
{
label: '启用',
value: 1,
},
{
label: '禁用',
value: 0,
},
],
},
defaultValue: '',
fieldName: 'status',
label: '状态',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,84 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getNoticeList } from '../api';
interface RowType {
id: string;
title: string;
content: string;
avatar: string;
date: string;
sort: number;
status: number;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'title', align: 'left', title: '标题' },
{ field: 'content', align: 'left', title: '内容', width: 300 },
{ field: 'avatar', title: '头像/图标' },
{ field: 'date', title: '日期', width: 150 },
{ field: 'sort', title: '排序', width: 100 },
{
field: 'status',
title: '状态',
width: 100,
slots: { default: 'status' },
},
{ field: 'created_at', title: '创建时间' },
{
type: 'html',
align: 'right',
title: '操作',
slots: { default: 'action' },
width: 200,
},
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getNoticeList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,135 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { Icon } from '#/components/icon';
import { deleteNotice } from './api';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
// 表单值
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteNotice({ ids }).then(() => {
message.success('删除成功!');
gridApi.reload();
});
};
</script>
<template>
<Page auto-content-height title="公告管理">
<FormModal />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null),
},
]"
:drop-down-actions="[
{
label: '删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, false),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #status="{ row }">
<Tag :color="row.status === 1 ? 'green' : 'red'">
{{ row.status === 1 ? '启用' : '禁用' }}
</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
]"
:drop-down-actions="[
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, row.id),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,58 @@
import { requestClient } from '#/api/request';
const prefix = 'role-quick-nav/';
/**
* 分页查询快捷导航列表
* @param data
*/
export async function getRoleQuickNavList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取详情
* @param id
*/
export async function getRoleQuickNavInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 新增快捷导航
* @param data
*/
export async function createRoleQuickNav(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑快捷导航
* @param data
*/
export async function updateRoleQuickNav(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除快捷导航
* @param data
*/
export async function deleteRoleQuickNav(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/**
* 根据角色ID获取快捷导航
* @param roleId
*/
export async function getRoleQuickNavByRoleId(roleId: number) {
return requestClient.get<any>(`${prefix}get-by-role-id`, { params: { role_id: roleId } });
}
/**
* 批量保存快捷导航
* @param data
*/
export async function batchSaveRoleQuickNav(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}batch-save`, data);
}

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRoleQuickNav, updateRoleQuickNav } from '../api';
import { modalFormProps } from '../config/form';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value ? updateRoleQuickNav : createRoleQuickNav;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
if (values) {
isUpdate.value = update;
formApi.setValues(values);
}
}
},
});
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}快捷导航`"
class="w-[60%]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,107 @@
import type { VbenFormProps } from '#/adapter/form';
import { getMenuTreeOptionSelect } from '#/views/system/menu/api';
import { getRoleOption } from '#/views/system/role/api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
formItemClass: 'col-span-6',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'ApiSelect',
componentProps: {
api: getRoleOption,
labelField: 'name',
valueField: 'id',
placeholder: '请选择角色',
},
fieldName: 'role_id',
label: '角色',
rules: 'required',
},
{
component: 'ApiTreeSelect',
componentProps: {
childrenField: 'children',
labelField: 'title',
valueField: 'id',
api: getMenuTreeOptionSelect,
placeholder: '请选择菜单(选择后自动填充标题、图标、地址)',
onChange: (value: any, formApi: any) => {
// 选择菜单后自动填充title、icon、url
if (value) {
// 这里需要通过API获取菜单详情来填充
// 暂时由后端处理
}
},
},
fieldName: 'menu_id',
label: '菜单',
defaultValue: 0,
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入标题',
},
fieldName: 'title',
label: '标题',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入图标',
},
fieldName: 'icon',
label: '图标',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入跳转地址',
},
fieldName: 'url',
label: '跳转地址',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入颜色(如:#00d8ff',
},
fieldName: 'color',
label: '颜色',
defaultValue: '#00d8ff',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入排序',
},
defaultValue: 0,
fieldName: 'sort',
label: '排序',
rules: 'required',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,37 @@
import type { VbenFormProps } from '#/adapter/form';
import { getRoleOption } from '#/views/system/role/api';
export const formOptions: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-6',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'ApiSelect',
componentProps: {
api: getRoleOption,
labelField: 'name',
valueField: 'id',
placeholder: '请选择角色',
allowClear: true,
},
fieldName: 'role_id',
label: '角色',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入标题',
},
fieldName: 'title',
label: '标题',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,80 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getRoleQuickNavList } from '../api';
interface RowType {
id: string;
role_id: number;
menu_id: number;
title: string;
icon: string;
url: string;
color: string;
sort: number;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'role_id', align: 'left', title: '角色ID', width: 100 },
{ field: 'title', align: 'left', title: '标题' },
{ field: 'icon', title: '图标', slots: { default: 'icon' } },
{ field: 'url', title: '跳转地址' },
{ field: 'color', title: '颜色', width: 120 },
{ field: 'sort', title: '排序', width: 100 },
{ field: 'created_at', title: '创建时间' },
{
type: 'html',
align: 'right',
title: '操作',
slots: { default: 'action' },
width: 200,
},
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getRoleQuickNavList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { Icon } from '#/components/icon';
import { deleteRoleQuickNav } from './api';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
// 表单值
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteRoleQuickNav({ ids }).then(() => {
message.success('删除成功!');
gridApi.reload();
});
};
</script>
<template>
<Page auto-content-height title="快捷导航管理">
<FormModal />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null),
},
]"
:drop-down-actions="[
{
label: '删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, false),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #icon="{ row }">
<Icon :icon="row.icon" style="font-size: 20px" />
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
]"
:drop-down-actions="[
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, row.id),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
@@ -17,6 +17,12 @@ const treeRef = ref();
const treeData = ref([]);
const isExpand = ref(false);
// 计算标题:授权菜单 - 角色名称
const drawerTitle = computed(() => {
const roleName = record.value?.name || '';
return roleName ? `授权菜单 - ${roleName}` : '授权菜单';
});
// 勾选的key
const checkedKeys = ref([]);
// 提交的勾选的key,会进行特殊处理包含半勾状态的父节点halfCheckedKeys
@@ -111,7 +117,7 @@ defineExpose(DrawerApi);
</script>
<template>
<div>
<Drawer class="w-[60%]" title="授权菜单">
<Drawer class="w-[60%]" :title="drawerTitle">
<Button type="primary" @click="handleExpandAndCollapse">
{{ isExpand ? '折叠' : '展开' }}
</Button>

View File

@@ -0,0 +1,648 @@
<script lang="ts" setup>
/**
* 菜单搜索选择组件(自定义实现)
*
* @description 自定义菜单搜索下拉选择器不依赖antd的Select组件
* - 支持模糊搜索菜单 title
* - 支持多选
* - 只显示叶子节点(有 path 和 component 的菜单项)
* - 支持 roleMenuIds 过滤
* @author 系统
* @date 2024
*/
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { Input, Spin, Tag } from 'ant-design-vue';
import { debounce } from 'lodash-es';
import { $t } from '#/locales';
import { getMenuTreeOption, searchMenu } from '#/views/system/menu/api';
import { Icon } from '#/components/icon';
// ==================== Props 定义 ====================
interface Props {
/**
* 选中的菜单ID列表
*/
modelValue?: number[];
/**
* 是否按用户过滤
*/
filterByUser?: boolean;
/**
* 角色已授权的菜单ID列表
*/
roleMenuIds?: number[];
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
filterByUser: true,
roleMenuIds: undefined,
});
// ==================== Emits 定义 ====================
const emit = defineEmits<{
/**
* 选中值变化时触发
* @param value 选中的菜单ID列表
*/
(e: 'update:modelValue', value: number[]): void;
}>();
// ==================== 响应式数据 ====================
/**
* 搜索关键词
*/
const searchKeyword = ref('');
/**
* 是否正在搜索
*/
const isSearching = ref(false);
/**
* 扁平化的菜单列表(搜索结果或已选中的菜单)
*/
const menuList = ref<any[]>([]);
/**
* 已选中菜单的完整信息(用于显示标签)
*/
const selectedMenuMap = ref<Map<number, any>>(new Map());
/**
* 过滤后的菜单列表(搜索结果直接来自后端,不需要前端过滤)
*/
const filteredMenuList = computed(() => {
return menuList.value;
});
/**
* 已选中的菜单列表(从 selectedMenuMap 获取)
*/
const selectedMenus = computed(() => {
if (!props.modelValue || props.modelValue.length === 0) {
return [];
}
return props.modelValue
.map((id) => selectedMenuMap.value.get(id))
.filter(Boolean);
});
/**
* 是否显示下拉列表
*/
const showDropdown = ref(false);
/**
* 组件容器引用
*/
const containerRef = ref<HTMLElement | null>(null);
/**
* 当前高亮的选项索引(用于键盘导航)
*/
const highlightIndex = ref(-1);
// ==================== 方法定义 ====================
/**
* 从树形数据中提取所有叶子节点
* @param nodes 树形节点数组
* @param roleMenuIds 角色已授权的菜单ID列表
*/
const flattenLeafNodes = (nodes: any[], roleMenuIds?: number[]): any[] => {
const leafNodes: any[] = [];
const traverse = (node: any) => {
const hasPath = node.path && node.path.trim() !== '';
const hasComponent = node.component && node.component.trim() !== '';
const noChildren = !node.children || node.children.length === 0;
// 如果是叶子节点
if (hasPath && hasComponent && noChildren) {
// 如果提供了 roleMenuIds需要检查是否在授权列表中
if (roleMenuIds === undefined || roleMenuIds.includes(node.id)) {
leafNodes.push({
id: node.id,
title: node.title,
icon: node.icon,
path: node.path,
component: node.component,
});
}
}
// 递归遍历子节点
if (node.children && node.children.length > 0) {
node.children.forEach(traverse);
}
};
nodes.forEach(traverse);
return leafNodes;
};
/**
* 搜索菜单
* @param keyword 搜索关键词
*/
const searchMenus = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
menuList.value = [];
showDropdown.value = false;
return;
}
isSearching.value = true;
showDropdown.value = true;
try {
const res = await searchMenu({
keyword: keyword,
filter_by_user: props.filterByUser ? 1 : 0,
});
// 如果提供了 roleMenuIds需要过滤
let filteredRes = res;
if (props.roleMenuIds !== undefined && props.roleMenuIds.length > 0) {
filteredRes = res.filter((menu: any) => props.roleMenuIds!.includes(menu.id));
}
// 更新已选中菜单的信息(如果搜索结果中包含)
filteredRes.forEach((menu: any) => {
if (props.modelValue?.includes(menu.id)) {
selectedMenuMap.value.set(menu.id, menu);
}
});
menuList.value = filteredRes;
} catch (error) {
console.error('搜索菜单失败:', error);
menuList.value = [];
} finally {
isSearching.value = false;
}
}, 300);
/**
* 处理输入变化
* @param e 输入事件
*/
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
searchKeyword.value = target.value;
highlightIndex.value = -1;
if (target.value) {
isSearching.value = true;
searchMenus(target.value);
} else {
showDropdown.value = false;
}
}
/**
* 处理输入框获得焦点
*/
function handleFocus() {
if (filteredMenuList.value.length > 0 && searchKeyword.value) {
showDropdown.value = true;
}
}
/**
* 处理选中菜单
* @param menu 选中的菜单数据
*/
function handleSelectMenu(menu: any) {
const currentValue = props.modelValue || [];
const isSelected = currentValue.includes(menu.id);
if (isSelected) {
// 取消选中
const newValue = currentValue.filter((id) => id !== menu.id);
selectedMenuMap.value.delete(menu.id);
emit('update:modelValue', newValue);
} else {
// 选中
const newValue = [...currentValue, menu.id];
selectedMenuMap.value.set(menu.id, menu);
emit('update:modelValue', newValue);
}
// 不清空搜索关键词,保持下拉显示
highlightIndex.value = -1;
}
/**
* 处理移除菜单
* @param menuId 菜单ID
*/
function handleRemoveMenu(menuId: number) {
const currentValue = props.modelValue || [];
const newValue = currentValue.filter((id) => id !== menuId);
selectedMenuMap.value.delete(menuId);
emit('update:modelValue', newValue);
}
/**
* 处理键盘事件
* @param e 键盘事件
*/
function handleKeydown(e: KeyboardEvent) {
if (!showDropdown.value || filteredMenuList.value.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlightIndex.value = Math.min(
highlightIndex.value + 1,
filteredMenuList.value.length - 1,
);
break;
case 'ArrowUp':
e.preventDefault();
highlightIndex.value = Math.max(highlightIndex.value - 1, 0);
break;
case 'Enter':
e.preventDefault();
if (highlightIndex.value >= 0) {
handleSelectMenu(filteredMenuList.value[highlightIndex.value]);
}
break;
case 'Escape':
showDropdown.value = false;
highlightIndex.value = -1;
break;
}
}
/**
* 处理点击外部关闭下拉
* @param e 点击事件
*/
function handleClickOutside(e: MouseEvent) {
if (containerRef.value && !containerRef.value.contains(e.target as Node)) {
showDropdown.value = false;
highlightIndex.value = -1;
}
}
/**
* 初始化数据(用于显示已选中的菜单)
*/
const initData = async () => {
try {
// 如果需要显示已选中的菜单,加载完整列表
if (props.modelValue && props.modelValue.length > 0) {
const res = await getMenuTreeOption({
filterByUser: props.filterByUser ? 1 : 0,
});
// 扁平化为叶子节点列表
const allMenus = flattenLeafNodes(res, props.roleMenuIds);
// 更新已选中菜单的完整信息
allMenus.forEach((menu) => {
if (props.modelValue!.includes(menu.id)) {
selectedMenuMap.value.set(menu.id, menu);
}
});
}
// 搜索时 menuList 会被更新,初始时为空
menuList.value = [];
} catch (error) {
console.error('获取菜单列表失败:', error);
}
};
// ==================== 生命周期 ====================
onMounted(() => {
document.addEventListener('click', handleClickOutside);
initData();
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
// ==================== 监听 ====================
/**
* 监听 roleMenuIds 变化,重新初始化数据
*/
watch(
() => props.roleMenuIds,
(newVal) => {
if (newVal !== undefined) {
initData();
}
},
{ deep: true, immediate: true }
);
/**
* 监听 filterByUser 变化,重新初始化数据
*/
watch(
() => props.filterByUser,
() => {
initData();
}
);
/**
* 监听 modelValue 变化,更新已选中菜单信息
*/
watch(
() => props.modelValue,
async (newVal) => {
if (newVal && newVal.length > 0) {
// 检查是否有缺失的菜单信息
const missingIds = newVal.filter((id) => !selectedMenuMap.value.has(id));
if (missingIds.length > 0) {
// 加载缺失的菜单信息
try {
const res = await getMenuTreeOption({
filterByUser: props.filterByUser ? 1 : 0,
});
const allMenus = flattenLeafNodes(res, props.roleMenuIds);
allMenus.forEach((menu) => {
if (missingIds.includes(menu.id)) {
selectedMenuMap.value.set(menu.id, menu);
}
});
} catch (error) {
console.error('获取菜单信息失败:', error);
}
}
// 清理已取消选中的菜单
selectedMenuMap.value.forEach((menu, id) => {
if (!newVal.includes(id)) {
selectedMenuMap.value.delete(id);
}
});
} else {
// 清空已选中菜单
selectedMenuMap.value.clear();
}
},
{ immediate: true }
);
// ==================== 暴露方法 ====================
/**
* 暴露方法供父组件调用
*/
defineExpose({
refresh: initData,
});
</script>
<template>
<div ref="containerRef" class="menu-search-select">
<!-- 搜索输入框 -->
<Input
v-model:value="searchKeyword"
placeholder="输入菜单名称搜索..."
allow-clear
@input="handleInput"
@focus="handleFocus"
@keydown="handleKeydown"
>
<template #prefix>
<LoadingOutlined v-if="isSearching" class="text-gray-400" />
<SearchOutlined v-else class="text-gray-400" />
</template>
</Input>
<!-- 已选中的菜单标签 -->
<div v-if="selectedMenus.length > 0" class="selected-tags">
<Tag
v-for="menu in selectedMenus"
:key="menu.id"
closable
class="mb-2"
@close="handleRemoveMenu(menu.id)"
>
<Icon v-if="menu.icon" :icon="menu.icon" class="mr-1" />
{{ $t(menu.title) }}
</Tag>
</div>
<!-- 提示信息 -->
<div class="mt-2 text-sm text-gray-500">
已选择 {{ selectedMenus.length }} 个菜单项
</div>
<!-- 下拉列表 -->
<div v-if="showDropdown" class="menu-dropdown">
<!-- 加载中 -->
<div v-if="isSearching" class="menu-dropdown__loading">
<Spin size="small" />
<span>搜索中...</span>
</div>
<!-- 无结果 -->
<div
v-else-if="filteredMenuList.length === 0"
class="menu-dropdown__empty"
>
暂无匹配菜单
</div>
<!-- 结果列表 -->
<div v-else class="menu-dropdown__list">
<div
v-for="(menu, index) in filteredMenuList"
:key="menu.id"
class="menu-item"
:class="{
'menu-item--active': index === highlightIndex,
'menu-item--selected': props.modelValue?.includes(menu.id),
}"
@click="handleSelectMenu(menu)"
@mouseenter="highlightIndex = index"
>
<div class="menu-item__content">
<Icon v-if="menu.icon" :icon="menu.icon" class="menu-item__icon" />
<div class="menu-item__info">
<div class="menu-item__title">{{ $t(menu.title) }}</div>
<div class="menu-item__path">{{ menu.path }}</div>
</div>
</div>
<div v-if="props.modelValue?.includes(menu.id)" class="menu-item__check">
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.menu-search-select {
position: relative;
width: 100%;
}
.selected-tags {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.menu-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1050;
margin-top: 4px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
max-height: 400px;
overflow: hidden;
&__loading,
&__empty {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 24px;
color: #999;
font-size: 14px;
}
&__list {
max-height: 400px;
overflow-y: auto;
padding: 4px 0;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
&:hover,
&--active {
background-color: #f5f7fa;
}
&--selected {
background-color: #e6f7ff;
}
&__content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
&__icon {
flex-shrink: 0;
font-size: 16px;
color: #666;
}
&__info {
flex: 1;
min-width: 0;
overflow: hidden;
}
&__title {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
&__path {
font-size: 12px;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__check {
flex-shrink: 0;
color: #1890ff;
font-weight: bold;
font-size: 16px;
}
}
/* 暗色模式适配 */
.dark {
.menu-dropdown {
background: #1f2937;
border-color: #374151;
}
.menu-item {
&:hover,
&--active {
background-color: #374151;
}
&--selected {
background-color: #1e3a5f;
}
&__title {
color: #f3f4f6;
}
&__path {
color: #9ca3af;
}
&__icon {
color: #d1d5db;
}
}
}
</style>

View File

@@ -0,0 +1,342 @@
<script lang="ts" setup>
import { ref, computed, nextTick } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Button, message, Card, Row, Col, Empty, Popconfirm } from 'ant-design-vue';
import { Icon } from '#/components/icon';
import { getMenuTreeOption } from '#/views/system/menu/api';
import { getRoleQuickNavList, deleteRoleQuickNav, batchSaveRoleQuickNav } from '#/views/system/role-quick-nav/api';
import { getMenuIdsByRoleIds } from '../api';
import MenuSearchSelect from './menu-search-select.vue';
const record = ref();
const quickNavList = ref<any[]>([]);
const menuTreeData = ref<any[]>([]);
const checkedMenuIds = ref<number[]>([]);
const loading = ref(false);
const roleMenuIds = ref<number[]>([]); // 角色已授权的菜单ID列表
// 计算标题:快捷导航管理 - 角色名称
const drawerTitle = computed(() => {
const roleName = record.value?.name || '';
return roleName ? `快捷导航管理 - ${roleName}` : '快捷导航管理';
});
// 从菜单树中获取菜单信息
const getMenuById = (menuId: number) => {
const findMenu = (nodes: any[]): any => {
for (const node of nodes) {
if (node.id === menuId) {
return node;
}
if (node.children && node.children.length > 0) {
const found = findMenu(node.children);
if (found) return found;
}
}
return null;
};
return findMenu(menuTreeData.value);
};
// 合并已保存和新选择的快捷导航
const displayQuickNavs = computed(() => {
// 已保存的快捷导航
const saved = quickNavList.value.map((item) => ({
...item,
isSaved: true,
}));
// 新选择但未保存的快捷导航(从菜单树查找)
const newSelected = checkedMenuIds.value
.filter((id) => !quickNavList.value.some((item) => item.menu_id === id))
.map((menuId, index) => {
const menu = getMenuById(menuId);
if (!menu) return null;
return {
id: menuId,
menu_id: menuId,
title: menu.title || '',
icon: menu.icon || '',
url: menu.path || '',
color: ['#1fdaca', '#bf0c2c', '#e18525', '#4daf1bc9', '#00d8ff'][index % 5] || '#00d8ff',
sort: quickNavList.value.length + index + 1,
isSaved: false,
};
})
.filter(Boolean);
return [...saved, ...newSelected];
});
// 获取角色已授权的菜单ID列表
const fetchRoleMenuIds = async () => {
if (!record.value?.id) {
roleMenuIds.value = undefined; // 改为 undefined
return;
}
try {
const res = await getMenuIdsByRoleIds({
id: record.value.id,
});
// 空数组也改为 undefined表示该角色没有授权任何菜单
roleMenuIds.value = Array.isArray(res) && res.length > 0 ? res : undefined;
} catch (error) {
console.error('获取角色菜单ID失败:', error);
message.error('获取角色菜单ID失败');
roleMenuIds.value = undefined; // 改为 undefined
}
};
// 获取菜单树数据
const fetchMenuTree = async () => {
try {
const res = await getMenuTreeOption({
filterByUser: 1,
});
menuTreeData.value = res;
} catch (error) {
console.error('获取菜单树失败:', error);
message.error('获取菜单树失败');
}
};
// 获取快捷导航列表
const fetchQuickNavList = async () => {
if (!record.value?.id) return;
loading.value = true;
try {
const res = await getRoleQuickNavList({
role_id: record.value.id,
page: 1,
pageSize: 1000, // 获取所有数据
});
// 处理返回的数据格式
const items = res.items || res.data || res || [];
// 按sort排序然后按id排序
quickNavList.value = items.sort((a: any, b: any) => {
if (a.sort !== b.sort) {
return (a.sort || 0) - (b.sort || 0);
}
return (a.id || 0) - (b.id || 0);
});
// 设置已选中的菜单ID
checkedMenuIds.value = items.map((item: any) => item.menu_id).filter(Boolean);
// 使用 nextTick 确保 MenuTreeSelector 能正确响应
await nextTick();
} catch (error) {
console.error('获取快捷导航列表失败:', error);
message.error('获取快捷导航列表失败');
quickNavList.value = [];
} finally {
loading.value = false;
}
};
// 批量保存快捷导航
const handleSave = async () => {
if (!record.value?.id) {
message.error('角色ID不能为空');
return;
}
if (checkedMenuIds.value.length === 0) {
message.warning('请至少选择一个菜单');
return;
}
DrawerApi.setState({
loading: true,
confirmLoading: true,
});
try {
await batchSaveRoleQuickNav({
role_id: record.value.id,
menu_id: checkedMenuIds.value,
});
message.success('保存成功');
await fetchQuickNavList();
DrawerApi.close();
} catch (error) {
console.error('保存失败:', error);
message.error('保存失败');
} finally {
DrawerApi.setState({
loading: false,
confirmLoading: false,
});
}
};
// 删除快捷导航(从已选中移除)
const handleRemove = (menuId: number) => {
checkedMenuIds.value = checkedMenuIds.value.filter((id) => id !== menuId);
message.success('已移除');
};
// 删除已保存的快捷导航(从数据库删除)
const handleDelete = async (id: number) => {
try {
// 找到要删除的快捷导航
const item = quickNavList.value.find((item) => item.id === id);
if (item && item.menu_id) {
// 从选中列表中移除
checkedMenuIds.value = checkedMenuIds.value.filter((menuId) => menuId !== item.menu_id);
}
await deleteRoleQuickNav({ ids: [id] });
message.success('删除成功');
await fetchQuickNavList();
} catch (error) {
console.error('删除失败:', error);
message.error('删除失败');
}
};
const [Drawer, DrawerApi] = useVbenDrawer({
onOpenChange(isOpen) {
record.value = isOpen ? DrawerApi.getData()?.record : {};
if (isOpen) {
DrawerApi.setState({
loading: true,
});
// 先获取角色已授权的菜单ID再加载菜单树最后加载快捷导航列表
fetchRoleMenuIds()
.then(() => {
// 等待 MenuTreeSelector 初始化完成,响应 roleMenuIds 的变化
return nextTick();
})
.then(() => fetchMenuTree())
.then(() => {
// 等待菜单树准备好后再获取快捷导航列表
return nextTick();
})
.then(() => fetchQuickNavList())
.finally(() => {
DrawerApi.setState({
loading: false,
});
});
}
},
onConfirm: handleSave,
});
// 暴露DrawerApi供父组件调用
defineExpose({
DrawerApi,
});
</script>
<template>
<Drawer :title="drawerTitle" class="w-[95%]">
<div class="flex h-full gap-4">
<!-- 左侧菜单树选择 -->
<div class="w-1/2 border-r pr-4">
<MenuSearchSelect
v-model="checkedMenuIds"
:filter-by-user="true"
:role-menu-ids="roleMenuIds"
/>
</div>
<!-- 右侧已选择的快捷导航卡片列表 -->
<div class="w-1/2 pl-4 flex flex-col">
<div class="mb-4">
<div class="text-sm text-gray-500 mb-2">
已选择 {{ displayQuickNavs.length }} 个快捷导航
<span v-if="quickNavList.length > 0" class="ml-2 text-gray-400">
已保存 {{ quickNavList.length }}
</span>
</div>
</div>
<div v-if="loading" class="text-center py-8 flex-1">
<Icon icon="ant-design:loading-outlined" class="text-2xl animate-spin" />
</div>
<Empty
v-else-if="displayQuickNavs.length === 0"
description="请在左侧选择菜单"
class="flex-1"
/>
<div v-else class="flex-1 overflow-y-auto">
<Row :gutter="[16, 16]">
<Col
v-for="item in displayQuickNavs"
:key="`${item.menu_id}-${item.isSaved ? 'saved' : 'new'}`"
:xs="24"
:sm="12"
>
<Card
class="quick-nav-card"
:style="{
borderTop: `4px solid ${item.color || '#00d8ff'}`,
}"
hoverable
>
<div class="flex flex-col items-center justify-center p-4 min-h-[180px]">
<!-- 图标 -->
<div
class="mb-3 flex items-center justify-center"
:style="{
color: item.color || '#00d8ff',
}"
>
<Icon :icon="item.icon" class="text-4xl" />
</div>
<!-- 标题 -->
<div class="mb-2 text-center font-medium text-base truncate w-full">
{{ item.title }}
</div>
<!-- 跳转地址 -->
<div class="mb-3 text-center text-xs text-gray-500 truncate w-full px-2">
{{ item.url }}
</div>
<!-- 操作按钮 -->
<div class="flex gap-2 mt-auto">
<Popconfirm
:title="item.isSaved ? '确定要删除这个快捷导航吗?' : '确定要移除这个快捷导航吗?'"
@confirm="item.isSaved ? handleDelete(item.id) : handleRemove(item.menu_id)"
>
<Button type="link" size="small" danger @click.stop>
<Icon icon="ant-design:delete-outlined" />
{{ item.isSaved ? '删除' : '移除' }}
</Button>
</Popconfirm>
</div>
</div>
</Card>
</Col>
</Row>
</div>
</div>
</div>
</Drawer>
</template>
<style scoped>
.quick-nav-card {
transition: all 0.3s;
height: 100%;
}
.quick-nav-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.quick-nav-card :deep(.ant-card-body) {
padding: 0;
height: 100%;
}
</style>

View File

@@ -15,6 +15,7 @@ import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
import AuthMenu from "#/views/system/role/components/auth-menu.vue";
import QuickNav from "#/views/system/role/components/quick-nav.vue";
const hasTopTableDropDownActions = ref(false);
@@ -65,6 +66,17 @@ const handleAuthMenu = (record: any) => {
});
authMenuRef.value.open();
};
// 快捷导航
const quickNavRef = ref();
const handleQuickNav = (record: any) => {
if (quickNavRef.value && quickNavRef.value.DrawerApi) {
quickNavRef.value.DrawerApi.setData({
record,
});
quickNavRef.value.DrawerApi.open();
}
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
@@ -83,6 +95,7 @@ const deleteApi = (row: any) => {
<Page auto-content-height title="角色管理">
<FormModal />
<AuthMenu ref="authMenuRef" />
<QuickNav ref="quickNavRef" />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -139,6 +152,14 @@ const deleteApi = (row: any) => {
// auth: ['admin', 'sys:role:detail'],
onClick: handleAuthMenu.bind(null, row),
},
{
label: '快捷导航',
type: 'link',
icon: 'mdi:compass-outline',
size: 'small',
// auth: ['admin', 'sys:role:detail'],
onClick: handleQuickNav.bind(null, row),
},
]"
:drop-down-actions="[
{