2
0

初始化项目

This commit is contained in:
caiyuchao
2024-11-14 11:06:38 +08:00
parent 988b9e6799
commit 4ffac789e1
320 changed files with 34244 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
<script setup lang="tsx">
import { Icon } from '@iconify/vue';
import { Button, Tag } from 'ant-design-vue';
import { SimpleScrollbar } from '~/packages/materials/src';
import MenuOperateModal from './modules/menu-operate-modal.vue';
const { data, columns, loading, getData } = useTable({
apiFn: doGetMenuList,
columns: () => [
{
key: 'menuName',
dataIndex: 'menuName',
title: '菜单名称',
align: 'left',
width: 200
},
{
key: 'path',
dataIndex: 'path',
title: '路由地址',
align: 'center'
},
{
key: 'component',
dataIndex: 'component',
title: '组件路径',
align: 'center'
},
{
key: 'status',
dataIndex: 'status',
title: '状态',
align: 'center',
customRender: ({ record }) => {
const colorTagMap: Record<string, string> = {
0: 'success',
1: 'warning'
};
const labelMap: Record<string, string> = {
0: '正常',
1: '停用'
};
return <Tag color={colorTagMap[record.status]}>{labelMap[record.status]}</Tag>;
}
},
{
key: 'menuType',
dataIndex: 'menuType',
title: '类型',
align: 'center',
customRender: ({ record }) => {
const colorTagMap: Record<string, string> = {
M: 'processing',
C: 'success',
F: 'default'
};
const labelMap: Record<string, string> = {
M: '目录',
C: '菜单',
F: '按钮'
};
return <Tag color={colorTagMap[record.menuType]}>{labelMap[record.menuType]}</Tag>;
}
},
{
key: 'orderNum',
dataIndex: 'orderNum',
title: '排序',
align: 'center'
},
{
key: 'icon',
dataIndex: 'icon',
title: '图标',
align: 'center',
customRender: ({ record }) => {
return (
<div class="flex-center text-5">
<Icon icon={`${record.icon}`} />
</div>
);
}
},
{
key: 'createTime',
dataIndex: 'createTime',
align: 'center',
title: '创建时间'
},
{
key: 'operate',
title: '操作',
align: 'center',
width: 200,
customRender: ({ record }) => (
<div class="flex justify-around gap-8px">
<Button size="small" onClick={() => edit(record.menuId)}>
编辑
</Button>
<Button size="small" danger onClick={() => handleDelete(record.menuId)}>
删除
</Button>
</div>
)
}
],
rowKey: 'menuId'
});
const { handleEdit, handleAdd, checkedRowKeys, operateType, drawerVisible, onDeleted, editingData } = useTableOperate(
data,
{
getData,
idKey: 'menuId'
}
);
function edit(id: number) {
handleEdit(id);
}
function handleDelete(id: number) {
$modal.confirm({
title: '提示',
content: '确定删除该菜单吗?',
onOk: async () => {
const { error } = await doDeleteMenu(id);
if (!error) {
$message.success('删除成功');
onDeleted();
}
}
});
}
const treeData = computed(() => {
return transformListToTree(data.value, 'menuId');
});
function handleSubmitSuccess() {
getData();
}
</script>
<template>
<SimpleScrollbar>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ACard title="菜单列表">
<AButton mb-4 type="primary" @click="handleAdd">新增菜单</AButton>
<ATable
:checked-row-keys="checkedRowKeys"
:data-source="treeData"
row-key="menuId"
:columns="columns"
:pagination="false"
:loading="loading"
/>
<MenuOperateModal
v-model:visible="drawerVisible"
:editing-data="editingData"
:operate-type="operateType"
@submit-success="handleSubmitSuccess"
/>
</ACard>
</div>
</SimpleScrollbar>
</template>
<style scoped></style>

View File

@@ -0,0 +1,70 @@
const { defaultRequiredRule } = useFormRules();
export type MenuModelType = Pick<
Api.SystemManage.Menu,
| 'menuName'
| 'menuType'
| 'icon'
| 'path'
| 'component'
| 'orderNum'
| 'status'
| 'parentId'
| 'hideInMenu'
| 'fixedIndexInTab'
| 'iconType'
| 'isFrame'
| 'perms'
| 'isCache'
| 'name'
>;
export function resetAddForm(): MenuModelType {
return {
menuName: '',
menuType: 'M',
icon: '',
path: '',
component: '',
orderNum: 0,
status: '0',
parentId: 0,
iconType: '1',
hideInMenu: '0',
isFrame: '1',
perms: '',
name: '',
isCache: '0'
};
}
export const formRules = {
menuName: defaultRequiredRule,
menuType: defaultRequiredRule,
icon: defaultRequiredRule,
path: defaultRequiredRule,
component: defaultRequiredRule,
status: defaultRequiredRule,
orderNum: [
defaultRequiredRule,
{
validator: (rule, value) => {
if (value < 0) {
return Promise.reject(rule.message || '排序必须大于等于0');
}
return Promise.resolve();
}
}
]
} as Record<string, App.Global.FormRule | App.Global.FormRule[]>;
export const menuTypeOptions = [
{ label: '目录', value: 'M' },
{ label: '菜单', value: 'C' },
{ label: '按钮', value: 'F' }
];
export const menuStatusOptions = [
{ label: '正常', value: '0' },
{ label: '停用', value: '1' }
];

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue';
import { SimpleScrollbar } from '~/packages/materials/src';
import { icons } from './icon';
const visible = ref(false);
defineOptions({
name: 'IconSelect'
});
const value = defineModel<string>({ default: '' });
const iconSelectRef = ref<HTMLElement | null>(null);
const { isOutside } = useMouseInElement(iconSelectRef);
const blur = () => {
if (isOutside.value) {
visible.value = false;
}
};
function selectIcon(icon: string) {
value.value = icon;
setTimeout(() => {
visible.value = false;
}, 100);
}
</script>
<template>
<div relative>
<div flex items-center gap-2 @click="visible = true">
<AInput v-model:value="value" @blur="blur">
<template #suffix>
<Icon v-if="value" text-4 :icon="`${value}`" @click.prevent="() => {}"></Icon>
<Icon v-if="!value" text-4 icon="carbon:apps" @click.prevent="() => {}"></Icon>
</template>
</AInput>
</div>
<Transition>
<template v-if="visible">
<div
ref="iconSelectRef"
class="absolute left-0 top-36px z-3000 h-80 w-92 rounded-md bg-light-2 shadow-md dark:bg-dark-4"
>
<SimpleScrollbar>
<div flex="~ wrap" gap-1 p-4>
<div
v-for="icon in icons"
:key="icon"
:class="
icon === value ? 'text-dark-800 bg-gray-200 dark:text-gray-4 dark:bg-dark6' : 'text-gray-500 bg-none'
"
hover="bg-light-500 dark:bg-dark-500 "
class="flex-center cursor-pointer rounded-md p-2 transition-all"
@click="selectIcon(icon)"
>
<Icon class="text-5.5 font-bold" :icon="`${icon}`" />
</div>
</div>
</SimpleScrollbar>
</div>
</template>
</Transition>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,39 @@
export const icons = [
'carbon:3d-cursor',
'carbon:account',
'carbon:ai-results-low',
'carbon:assembly-cluster',
'carbon:battery-charging',
'carbon:battery-full',
'carbon:bicycle',
'carbon:building-insights-3',
'carbon:calendar',
'carbon:carbon',
'carbon:carbon-for-ibm-product',
'carbon:category',
'carbon:center-to-fit',
'carbon:chart-area-smooth',
'carbon:chart-median',
'carbon:chat-bot',
'carbon:cics-system-group',
'carbon:database-messaging',
'carbon:document',
'carbon:document-multiple-01',
'carbon:earth-americas-filled',
'carbon:folder',
'carbon:forum',
'carbon:group',
'carbon:ibm-cloud-bare-metal-server',
'carbon:ibm-telehealth',
'carbon:image-search-alt',
'carbon:laptop',
'carbon:machine-learning',
'carbon:report',
'carbon:rocket',
'carbon:settings',
'carbon:settings-services',
'carbon:shopping-cart',
'carbon:user-avatar',
'carbon:user-multiple',
'carbon:volume-block-storage'
];

View File

@@ -0,0 +1,185 @@
<script lang="ts" setup>
import type {} from 'ant-design-vue';
import { generatedRoutes } from '@/router/elegant/routes';
import { $t } from '@/locales';
import type { MenuModelType } from './form';
import { formRules, menuStatusOptions, menuTypeOptions, resetAddForm } from './form';
import IconSelect from './icon-select.vue';
const props = defineProps<{
operateType: AntDesign.TableOperateType;
editingData?: Api.SystemManage.Menu | null;
}>();
const emits = defineEmits<{
'submit-success': [];
}>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, resetFields } = useAntdForm();
const model = ref<MenuModelType>(resetAddForm());
const treeData = ref<Api.SystemManage.MenuTree[]>([]);
const title = computed(() => {
const titles: Record<AntDesign.TableOperateType, string> = {
add: '新增菜单',
edit: '编辑菜单'
};
return titles[props.operateType];
});
// TODO: 根据菜单类型动态加载组件路径、目录类型只允许选择带有子元素的,菜单类型只允许选择没有子元素的
const componentOptions = computed(() => {
const excludePaths = ['/404', '/403', '/500'];
function transformRoutes(routes: any[]): any[] {
return routes.filter(route => {
if (route.children) {
route.children = transformRoutes(route.children);
return true;
}
if (!route.hideInMenu && !excludePaths.includes(route.path) && !route.path.startsWith('/login')) {
return true;
}
return false;
});
}
return transformRoutes(generatedRoutes);
});
watch(visible, val => {
if (val) {
if (props.operateType === 'edit' && props.editingData) {
model.value = {
...props.editingData,
parentId: props.editingData.parentId || 0,
hideInMenu: props.editingData.visible === '0' ? '1' : '0'
};
}
} else {
resetFields();
model.value = resetAddForm();
}
});
const submitForm = async () => {
await validate();
const { error } = await (props.operateType === 'add' ? doAddMenu(model.value) : doEditMenu(model.value));
if (!error) {
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
emits('submit-success');
}
closeModal();
};
function closeModal() {
visible.value = false;
resetFields();
model.value = resetAddForm();
}
async function getTreeData() {
const { data, error } = await fetchGetMenuTree();
if (!error && data) {
// 添加根节点
treeData.value = [
{
id: 0,
pId: -1,
label: '根节点',
children: data
}
];
}
}
function handleTreeSelect(node: any) {
model.value.component = node.component;
model.value.name = node.name;
}
getTreeData();
</script>
<template>
<AModal v-model:open="visible" :title="title" :width="700">
<AForm
ref="formRef"
:model="model"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16, offset: 1 }"
class="grid grid-cols-2 gap-3 px-4 py-4 pt-6"
>
<AFormItem label="上级菜单" name="parentId">
<ATreeSelect
v-model:value="model.parentId"
show-search
:field-names="{ value: 'id' }"
allow-clear
:tree-data="treeData"
tree-node-filter-prop="label"
/>
</AFormItem>
<AFormItem label="菜单名称" name="menuName">
<AInput v-model:value="model.menuName" />
</AFormItem>
<AFormItem label="菜单类型" name="menuType">
<ASelect v-model:value="model.menuType" :options="menuTypeOptions" />
</AFormItem>
<AFormItem v-if="model.menuType !== 'F'" label="菜单图标" name="icon">
<IconSelect v-model="model.icon" />
</AFormItem>
<AFormItem v-if="model.menuType === 'C'" label="是否外链">
<ARadioGroup v-model:value="model.isFrame" name="radioGroup" @change="() => (model.path = '')">
<ARadio value="0"></ARadio>
<ARadio value="1"></ARadio>
</ARadioGroup>
</AFormItem>
<AFormItem v-if="model.menuType !== 'F'" label="菜单路径" name="path">
<div>
<!-- @vue-ignore -->
<ATreeSelect
v-if="model.isFrame === '1'"
v-model:value="model.path"
show-search
:field-names="{ value: 'path', label: 'path' }"
allow-clear
:tree-data="componentOptions"
tree-node-filter-prop="label"
@select="(_val, node) => handleTreeSelect(node)"
/>
<AInput v-else v-model:value="model.path" />
</div>
</AFormItem>
<AFormItem v-if="model.menuType === 'C'" label="隐藏菜单" name="hideInMenu">
<ASwitch v-model:checked="model.hideInMenu" checked-value="0" un-checked-value="1" />
</AFormItem>
<AFormItem label="排序" name="orderNum">
<AInputNumber v-model:value="model.orderNum" w-full />
</AFormItem>
<AFormItem v-if="model.menuType !== 'F'" label="状态" name="status">
<ASelect v-model:value="model.status" :options="menuStatusOptions" />
</AFormItem>
<AFormItem v-if="model.menuType === 'C'" label="缓存" name="isCache">
<ASwitch v-model:checked="model.isCache" checked-value="0" un-checked-value="1" />
</AFormItem>
<AFormItem v-if="model.menuType === 'F'" label="权限标识" name="perms">
<AInput v-model:value="model.perms" />
</AFormItem>
</AForm>
<template #footer>
<AButton @click="submitForm">确定</AButton>
<AButton @click="closeModal">取消</AButton>
</template>
</AModal>
</template>
<style scoped>
:deep(.ant-input-number) {
width: 100%;
}
</style>