🔖 项目重命名.

This commit is contained in:
lijiahangmax
2024-05-16 00:03:30 +08:00
parent f7189e34cb
commit d3a045ec20
1511 changed files with 4199 additions and 4128 deletions

View File

@@ -0,0 +1,275 @@
<template>
<a-modal v-model:visible="visible"
body-class="modal-form-large"
title-align="start"
:title="title"
:top="30"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 上级菜单 -->
<a-form-item field="parentId" label="上级菜单">
<menu-tree-selector v-model:model-value="formModel.parentId as number"
:disabled="formModel.type === MenuType.PARENT_MENU" />
</a-form-item>
<!-- 菜单名称 -->
<a-form-item field="name" label="菜单名称">
<a-input v-model="formModel.name" placeholder="请输入菜单名称" />
</a-form-item>
<!-- 菜单类型 -->
<a-form-item v-if="isAddHandle"
field="type"
label="菜单类型">
<a-radio-group type="button"
v-model="formModel.type"
:options="toRadioOptions(menuTypeKey)" />
</a-form-item>
<!-- 菜单图标 -->
<a-form-item v-if="formModel.type !== MenuType.FUNCTION"
field="icon"
label="菜单图标">
<icon-picker v-model:icon="formModel.icon as string">
<template #iconSelect>
<a-input v-model="formModel.icon" placeholder="请选择菜单图标" />
</template>
</icon-picker>
</a-form-item>
<!-- 菜单权限 -->
<a-form-item v-if="formModel.type === MenuType.FUNCTION"
field="permission"
label="菜单权限">
<a-input v-model="formModel.permission"
placeholder="菜单权限 infra:system-menu:query"
allow-clear />
</a-form-item>
<!-- 组件名称 -->
<a-form-item v-if="formModel.type !== MenuType.FUNCTION"
field="component"
label="组件名称">
<a-input v-model="formModel.component"
placeholder="路由组件名称"
allow-clear />
</a-form-item>
<!-- 外链地址 -->
<a-form-item v-if="formModel.type !== MenuType.FUNCTION"
field="path"
label="外链地址"
tooltip="输入组件名称后则不会生效">
<a-input v-model="formModel.path"
placeholder="组件名称与外链地址二选一"
allow-clear />
</a-form-item>
<!-- 菜单排序 -->
<a-form-item field="sort" label="菜单排序">
<a-input-number v-model="formModel.sort"
placeholder="排序"
:style="{ width: '120px' }"
allow-clear
hide-button />
</a-form-item>
<!-- 是否可见 -->
<a-form-item v-if="formModel.type !== MenuType.FUNCTION"
field="type"
label="是否可见"
tooltip="选择隐藏后不会在菜单以及 tab 中显示 但是可以访问">
<a-switch type="round"
v-model="formModel.visible"
:checked-text="getDictValue(menuVisibleKey, EnabledStatus.ENABLED)"
:unchecked-text="getDictValue(menuVisibleKey, EnabledStatus.DISABLED)"
:checked-value="EnabledStatus.ENABLED"
:unchecked-value="EnabledStatus.DISABLED" />
</a-form-item>
<!-- 是否新窗口打开 -->
<a-form-item v-if="formModel.type !== MenuType.FUNCTION"
field="type"
label="新窗口打开"
tooltip="选择后点击菜单会使用新页面打开">
<a-switch type="round"
v-model="formModel.newWindow"
:checked-text="getDictValue(menuNewWindowKey, EnabledStatus.ENABLED)"
:unchecked-text="getDictValue(menuNewWindowKey, EnabledStatus.DISABLED)"
:checked-value="EnabledStatus.ENABLED"
:unchecked-value="EnabledStatus.DISABLED" />
</a-form-item>
<!-- 是否缓存 -->
<a-form-item v-if="formModel.type !== MenuType.FUNCTION"
field="type"
label="是否缓存"
tooltip="选择缓存后则会使用 keep-alive 缓存组件">
<a-switch type="round"
v-model="formModel.cache"
:checked-text="getDictValue(menuCacheKey, EnabledStatus.ENABLED)"
:unchecked-text="getDictValue(menuCacheKey, EnabledStatus.DISABLED)"
:checked-value="EnabledStatus.ENABLED"
:unchecked-value="EnabledStatus.DISABLED" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'menuFormModal'
};
</script>
<script lang="ts" setup>
import type { MenuUpdateRequest } from '@/api/system/menu';
import { ref, watch } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { menuCacheKey, menuNewWindowKey, sortStep } from '../types/const';
import { menuVisibleKey, menuTypeKey, MenuType } from '../types/const';
import { EnabledStatus } from '@/types/const';
import { createMenu, updateMenu } from '@/api/system/menu';
import { Message } from '@arco-design/web-vue';
import { useDictStore } from '@/store';
import IconPicker from '@sanqi377/arco-vue-icon-picker';
import MenuTreeSelector from '@/components/system/menu/tree-selector/index.vue';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const { toRadioOptions, getDictValue } = useDictStore();
const title = ref<string>();
const isAddHandle = ref<boolean>(true);
const defaultForm = (): MenuUpdateRequest => {
return {
id: undefined,
parentId: 0,
name: undefined,
type: MenuType.PARENT_MENU,
permission: undefined,
sort: undefined,
visible: EnabledStatus.ENABLED,
cache: EnabledStatus.ENABLED,
newWindow: EnabledStatus.DISABLED,
icon: undefined,
path: undefined,
component: undefined,
};
};
const formRef = ref();
const formModel = ref<MenuUpdateRequest>({});
const emits = defineEmits(['added', 'updated']);
// 选择根目录 parentId 设置为 0
watch(() => formModel.value.type, () => {
if (formModel.value.type === MenuType.PARENT_MENU) {
formModel.value.parentId = 0;
}
});
// 打开新增
const openAdd = (record: any) => {
title.value = '添加菜单';
isAddHandle.value = true;
renderForm({ ...defaultForm(), parentId: record.parentId, sort: (record.sort || 0) + sortStep });
// 如果是父菜单默认选中子菜单 如果是子菜单默认选中功能
if (record.type === MenuType.PARENT_MENU || record.type === MenuType.SUB_MENU) {
formModel.value.type = record.type + 1;
}
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改菜单';
isAddHandle.value = false;
renderForm({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
formModel.value = Object.assign({}, record);
};
defineExpose({ openAdd, openUpdate });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
// 验证父菜单
if (formModel.value.parentId === 0
&& (formModel.value.type === MenuType.SUB_MENU || formModel.value.type === MenuType.FUNCTION)) {
formRef.value.setFields({
parentId: {
status: 'error',
message: '创建子目录或功能时 父菜单不能为根目录'
}
});
return false;
}
// 验证组件名称
if ((formModel.value.type === MenuType.PARENT_MENU || formModel.value.type === MenuType.SUB_MENU)
&& !formModel.value.component
&& !formModel.value.path) {
formRef.value.setFields({
component: {
status: 'error',
message: '组件名称与外链地址二选一'
}
});
return false;
}
if (isAddHandle.value) {
// 新增
await createMenu(formModel.value);
Message.success('创建成功');
emits('added');
} else {
// 修改
await updateMenu(formModel.value);
Message.success('修改成功');
emits('updated');
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
// 触发 watch 防止第二次加载变成根目录
renderForm(defaultForm());
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,333 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<a-row>
<!-- 搜索框 -->
<a-col :span="12">
<a-form :model="formModel"
ref="formRef"
label-align="left"
:auto-label-width="true"
@keyup.enter="loadMenuData">
<a-row :gutter="32">
<!-- 菜单名称 -->
<a-col :span="12">
<a-form-item field="name" label="菜单名称">
<a-input v-model="formModel.name" placeholder="请输入菜单名称" allow-clear />
</a-form-item>
</a-col>
<!-- 菜单状态 -->
<a-col :span="12">
<a-form-item field="status" label="菜单状态">
<a-select v-model="formModel.status"
:options="toOptions(menuStatusKey)"
placeholder="请选择菜单状态"
allow-clear />
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-col>
<!-- 操作 -->
<a-col :span="12" class="table-right-bar-handle">
<a-space>
<!-- 新增 -->
<a-button type="primary"
@click="emits('openAdd',{ parentId: 0, sort: getMaxSort(tableRenderData) })"
v-permission="['infra:system-menu:create']">
新增
<template #icon>
<icon-plus />
</template>
</a-button>
<!-- 查询 -->
<a-button type="primary" :loading="fetchLoading" @click="loadMenuData">
查询
<template #icon>
<icon-search />
</template>
</a-button>
<!-- 重置 -->
<a-button @click="resetForm" :loading="fetchLoading">
重置
<template #icon>
<icon-refresh />
</template>
</a-button>
<!-- 折叠/展开 -->
<a-button @click="toggleExpand">
{{ expandStatus ? '折叠' : '展开' }}
<template #icon>
<icon-shrink v-if="expandStatus" />
<icon-expand v-else />
</template>
</a-button>
<!-- 刷新缓存 -->
<a-popconfirm content="确定要刷新全局菜单缓存吗?"
position="left"
type="warning"
@ok="doRefreshCache">
<a-button v-permission="['infra:system-menu:management:refresh-cache']"
type="primary"
status="warning">
刷新缓存
<template #icon>
<icon-sync />
</template>
</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<a-table row-key="id"
class="table-wrapper-16"
ref="tableRef"
:loading="fetchLoading"
:pagination="false"
:columns="columns"
:data="tableRenderData"
:bordered="false">
<!-- 菜单名称 -->
<template #menuName="{ record }">
<span class="ml8">{{ record.name }}</span>
</template>
<!-- 图标 -->
<template #icon="{ record }">
<component v-if="record.icon" :is="record.icon" />
</template>
<!-- 类型 -->
<template #type="{ record }">
<a-tag>{{ getDictValue(menuTypeKey, record.type) }}</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-space>
<!-- 菜单状态 -->
<a-popconfirm v-if="hasPermission('infra:system-menu:update-status')"
position="top"
type="warning"
:content="`确定要将当前节点以及所有子节点改为${toggleDictValue(menuStatusKey, record.status, 'label')}?`"
@ok="updateStatus(record.id, toggleDictValue(menuStatusKey, record.status))">
<a-tooltip content="点击切换状态">
<a-tag :color="getDictValue(menuStatusKey, record.status, 'color')" class="pointer">
{{ getDictValue(menuStatusKey, record.status) }}
</a-tag>
</a-tooltip>
</a-popconfirm>
<a-tag v-else :color="getDictValue(menuStatusKey, record.status, 'color')">
{{ getDictValue(menuStatusKey, record.status) }}
</a-tag>
<!-- 显示状态 -->
<a-popconfirm v-if="hasPermission('infra:system-menu:update-status')"
position="top"
type="warning"
:content="`确定要将当前节点以及所有子节点改为${toggleDictValue(menuVisibleKey, record.visible, 'label')}?`"
@ok="updateVisible(record.id, toggleDictValue(menuVisibleKey, record.visible))">
<a-tooltip content="点击切换状态">
<a-tag v-if="(record.visible || record.visible === 0) && record.type !== MenuType.FUNCTION"
:color="getDictValue(menuVisibleKey, record.visible, 'color')"
class="pointer">
{{ getDictValue(menuVisibleKey, record.visible) }}
</a-tag>
</a-tooltip>
</a-popconfirm>
<a-tag v-else-if="(record.visible || record.visible === 0) && record.type !== MenuType.FUNCTION"
:color="getDictValue(menuVisibleKey, record.visible, 'color')">
{{ getDictValue(menuVisibleKey, record.visible) }}
</a-tag>
</a-space>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 新增 -->
<a-button type="text"
size="mini"
v-if="record.type !== MenuType.FUNCTION"
v-permission="['infra:system-menu:create']"
@click="emits('openAdd', { parentId: record.id, type: record.type, sort: getMaxSort(record.children) })">
新增
</a-button>
<!-- 修改 -->
<a-button type="text"
size="mini"
v-permission="['infra:system-menu:update']"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="doDeleteMenu(record)">
<a-button v-permission="['infra:system-menu:delete']"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</template>
<script lang="ts">
export default {
name: 'menuTable'
};
</script>
<script lang="ts" setup>
import type { MenuQueryRequest, MenuQueryResponse } from '@/api/system/menu';
import { reactive, ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { getMenuList, deleteMenu, updateMenuStatus, refreshCache } from '@/api/system/menu';
import { menuStatusKey, menuVisibleKey, menuTypeKey, MenuType } from '../types/const';
import columns from '../types/table.columns';
import { Message } from '@arco-design/web-vue';
import { useCacheStore, useDictStore } from '@/store';
import usePermission from '@/hooks/permission';
import { findParentNode } from '@/utils/tree';
const { toOptions, getDictValue, toggleDictValue } = useDictStore();
const cacheStore = useCacheStore();
const { hasPermission } = usePermission();
const formRef = ref();
const formModel = reactive<MenuQueryRequest>({
name: undefined,
status: undefined
});
const tableRef = ref();
const expandStatus = ref<boolean>(false);
const tableRenderData = ref<MenuQueryResponse[]>([]);
const { loading: fetchLoading, setLoading: setFetchLoading } = useLoading();
const emits = defineEmits(['openAdd', 'openUpdate']);
// 删除菜单
const doDeleteMenu = async ({ id }: any) => {
try {
setFetchLoading(true);
// 调用删除接口
await deleteMenu(id);
// 获取父级容器
const parent = findParentNode(id, tableRenderData.value, 'id');
if (parent) {
// 页面删除 不重新调用接口
let children;
if (parent.root) {
children = tableRenderData.value;
} else {
children = parent.children;
}
if (children) {
// 删除
for (let i = 0; i < children.length; i++) {
if (children[i].id === id) {
children.splice(i, 1);
break;
}
}
}
}
cacheStore.reset('menus');
Message.success('删除成功');
} catch (e) {
} finally {
setFetchLoading(false);
}
};
// 添加后回调
const addedCallback = () => {
loadMenuData(true);
};
// 更新后回调
const updatedCallback = () => {
loadMenuData(true);
};
defineExpose({
addedCallback, updatedCallback
});
// 加载菜单
const loadMenuData = async (all: any = undefined) => {
try {
setFetchLoading(true);
const { data } = await getMenuList(all === true ? {} : formModel);
tableRenderData.value = data as MenuQueryResponse[];
// 重设缓存
if (all) {
cacheStore.set('menus', data);
}
} catch (e) {
} finally {
setFetchLoading(false);
}
};
onMounted(() => {
loadMenuData(true);
});
// 重置菜单
const resetForm = () => {
formRef.value.resetFields();
loadMenuData(true);
};
// 切换展开/折叠
const toggleExpand = () => {
tableRef.value.expandAll(expandStatus.value = !expandStatus.value);
};
// 刷新缓存
const doRefreshCache = async () => {
try {
setFetchLoading(true);
await refreshCache();
Message.success('刷新成功 页面缓存刷新后生效');
// 加载菜单数据
await loadMenuData(true);
} catch (e) {
} finally {
setFetchLoading(false);
}
};
// 修改状态
const updateStatus = async (id: number, status: number) => {
await updateMenuStatus({ id, status });
await loadMenuData();
};
// 修改可见状态
const updateVisible = async (id: number, visible: number) => {
await updateMenuStatus({ id, visible });
await loadMenuData();
};
// 获取最大排序
const getMaxSort = (array: MenuQueryResponse[]) => {
if (array?.length) {
return array[array.length - 1].sort;
} else {
return 0;
}
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="layout-container" v-if="render">
<!-- 表格 -->
<menu-table ref="table"
@open-add="(e) => modal.openAdd(e)"
@open-update="(e) => modal.openUpdate(e)" />
<!-- 添加修改模态框 -->
<menu-form-modal ref="modal"
@added="() => table.addedCallback()"
@updated="() => table.updatedCallback()" />
</div>
</template>
<script lang="ts">
export default {
name: 'systemMenu'
};
</script>
<script lang="ts" setup>
import MenuTable from '@/views/system/menu/components/menu-table.vue';
import MenuFormModal from '@/views/system/menu/components/menu-form-modal.vue';
import { ref, onBeforeMount, onUnmounted } from 'vue';
import { useCacheStore, useDictStore } from '@/store';
import { dictKeys } from './types/const';
const table = ref();
const modal = ref();
const render = ref(false);
// 加载字典项
onBeforeMount(async () => {
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
render.value = true;
});
// 卸载时清除 cache
onUnmounted(() => {
const cacheStore = useCacheStore();
cacheStore.reset('menus');
});
</script>

View File

@@ -0,0 +1,26 @@
// 自增排序步长
export const sortStep = 10;
// 菜单类型 值
export const MenuType = {
// 父菜单
PARENT_MENU: 1,
// 子菜单
SUB_MENU: 2,
// 功能
FUNCTION: 3
};
// 菜单类型 字典项
export const menuTypeKey = 'systemMenuType';
// 菜单状态 字典项
export const menuStatusKey = 'systemMenuStatus';
// 是否可见 字典项
export const menuVisibleKey = 'systemMenuVisible';
// 是否缓存 字典项
export const menuCacheKey = 'systemMenuCache';
// 是否新窗口打开 字典项
export const menuNewWindowKey = 'systemMenuNewWindow';
// 加载的字典值
export const dictKeys = [menuTypeKey, menuStatusKey, menuVisibleKey, menuCacheKey, menuNewWindowKey];

View File

@@ -0,0 +1,44 @@
import type { FieldRule } from '@arco-design/web-vue';
export const parentId = [{
required: true,
message: '请选择父菜单'
}] as FieldRule[];
export const name = [{
required: true,
message: '请输入菜单名称'
}, {
maxLength: 32,
message: `菜单名称长度不能大于32位`
}] as FieldRule[];
export const type = [{
required: true,
message: '请选择菜单类型'
}] as FieldRule[];
export const sort = [{
required: true,
message: '请输入菜单排序'
}] as FieldRule[];
export const visible = [{
required: true,
message: '请选择是否可见'
}] as FieldRule[];
export const cache = [{
required: true,
message: '请选择是否缓存'
}] as FieldRule[];
export default {
parentId,
name,
type,
sort,
visible,
cache,
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -0,0 +1,57 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
const columns = [
{
title: '菜单名称',
dataIndex: 'menuName',
slotName: 'menuName',
fixed: 'left',
width: 250,
}, {
title: '图标',
dataIndex: 'icon',
slotName: 'icon',
align: 'center',
width: 60,
}, {
title: '类型',
dataIndex: 'type',
slotName: 'type',
width: 80,
}, {
title: '排序',
dataIndex: 'sort',
slotName: 'sort',
width: 70,
}, {
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 120,
}, {
title: '权限标识',
dataIndex: 'permission',
slotName: 'permission',
ellipsis: true,
tooltip: true
}, {
title: '组件名称',
dataIndex: 'component',
slotName: 'component',
ellipsis: true,
tooltip: true,
}, {
title: '链接路径',
dataIndex: 'path',
slotName: 'path',
ellipsis: true,
tooltip: true,
}, {
title: '操作',
slotName: 'handle',
width: 168,
fixed: 'right',
}
] as TableColumnData[];
export default columns;