feat: 添加字典管理

master
luoer 2023-10-27 17:34:26 +08:00
parent 09498ec02e
commit 1133555ca2
11 changed files with 1304 additions and 114 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
<template>
<bread-page>
<template #content>
<div class="h-full w-full grid grid-cols-[auto_1fr] gap-4 p-4">
<div class="bg-white w-[256px]">
<div class="flex items-center justify-between gap-2 px-4 h-14">
<span class="text-base">菜单列表</span>
<div>
<a-button>
<template #icon>
<i class="icon-park-outline-plus"></i>
</template>
</a-button>
</div>
</div>
<a-tree
:data="menus"
:default-expand-all="true"
:block-node="true"
:field-names="{
icon: undefined,
title: 'name',
key: 'id',
}"
>
<template #title="node">
<div class="group flex-1 flex items-center justify-between gap-2">
<div @click="onEdit(node)">
<!-- <a-tag :color="MenuTypes.fmt(node.type, 'color')" size="small" :bordered="true">
{{ MenuTypes.fmt(node.type) }}
</a-tag> -->
<i :class="node.icon" class="ml-2"></i>
{{ node.name }}
</div>
<div class="hidden group-hover:block">
<i
v-if="node.type === MenuType.MENU"
class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-plus"
></i>
<i class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-delete"></i>
</div>
</div>
</template>
</a-tree>
</div>
<div class="bg-white">
<a-card title="菜单信息" :bordered="false">
<Form ref="formRef" v-bind="form"></Form>
</a-card>
<a-divider :margin="0"></a-divider>
<div class="px-4 mt-4">
<btn-table></btn-table>
</div>
</div>
</div>
</template>
</bread-page>
</template>
<script setup lang="tsx">
import { Menu, api } from "@/api";
import { useForm, Form, useAniTable, FormInstance } from "@/components";
import { MenuType, MenuTypes } from "@/constants/menu";
const formRef = ref<FormInstance | null>(null);
const menus = ref<any[]>([]);
const treeEach = (tree: any[], fn: any) => {
for (const item of tree) {
if (item.children) {
treeEach(item.children, fn);
}
fn(item);
}
};
const onEdit = (row: any) => {
formRef.value?.setModel(row);
(btn.props as any).data = row.buttons;
};
onMounted(async () => {
const res = await api.menu.getMenus({ tree: true });
const data = res.data.data ?? [];
treeEach(data, (item: Menu) => {
if (item.type === MenuType.BUTTON) {
return;
}
if (item.type === MenuType.PAGE) {
(item as any).buttons = (item as any).children;
delete (item as any).children;
}
(item as any).iconRender = () => <i class={item.icon} />;
});
menus.value = data;
});
const form = useForm({
items: [
{
field: "name",
label: "菜单名称",
type: "input",
},
{
field: "icon",
label: "菜单图标",
type: "input",
},
],
async submit(arg) {
console.log(arg);
},
});
const [btnTable, btn] = useAniTable({
columns: [
{
title: " 名称",
dataIndex: "name",
},
{
title: "标识",
dataIndex: "code",
},
{
title: "操作",
type: "button",
width: 140,
buttons: [
{
type: "modify",
text: "修改",
},
{
text: "删除",
type: "delete",
},
],
},
],
create: {},
modify: {},
});
</script>
<style lang="less" scoped></style>
<route lang="json">
{
"meta": {
"sort": 10302,
"title": "菜单管理",
"icon": "icon-park-outline-add-subtract"
}
}
</route>

View File

@ -143,7 +143,7 @@ export const FormModal = defineComponent({
}
if (typeof props.trigger === "object") {
content = (
<Button type="primary" {...omit(props.trigger, "text")}>
<Button type="primary" {...props.trigger.buttonProps}>
{props.trigger?.text || "新增"}
</Button>
);

View File

@ -1,10 +1,12 @@
<template>
<bread-page>
<iframe
src="https://apifox.com/apidoc/shared-f1ea65e6-cee8-4fe3-949f-288a7cd1af49"
frameborder="0"
class="w-full h-full"
></iframe>
<template #content>
<iframe
src="https://apifox.com/apidoc/shared-f1ea65e6-cee8-4fe3-949f-288a7cd1af49"
frameborder="0"
class="w-full h-full"
></iframe>
</template>
</bread-page>
</template>

View File

@ -65,7 +65,7 @@
</template>
<script setup lang="ts">
import doc from "@/dd.json";
import doc from "./data.json";
import editorModal from "./editor.vue";
import ejs from "ejs";
import template from "./page.ejs?raw";
@ -85,8 +85,8 @@ const onChange = (value: string | number) => {
const onOpen = () => {
const data = {
tag: '',
operationId: '',
tag: "",
operationId: "",
create: {},
select: {},
modify: {},
@ -106,7 +106,6 @@ const onOpen = () => {
data.delete = route;
}
}
console.log(data);
content.value = ejs.render(template, data);
};
</script>

View File

@ -1,8 +1,8 @@
<template>
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
<div class="flex gap-2">
<a-input-search allow-clear placeholder="字典名称..." class="mb-2"></a-input-search>
<a-button @click="onCreateRow">
<a-input-search allow-clear placeholder="字典类型" class="mb-2"></a-input-search>
<a-button @click="formCtx.open">
<template #icon>
<i class="icon-park-outline-add"></i>
</template>
@ -13,12 +13,13 @@
<ul class="pl-0 mt-0">
<li
v-for="item in list"
:key="item.id"
:key="item.code"
:class="{ active: item.id === current?.id }"
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
>
<div>
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', item)">
<i class="icon-park-outline-folder-close align-[-2px]"></i>
{{ item.title }}
<span class="flex-1 truncate">{{ item.name }}</span>
</div>
<div>
<a-dropdown>
@ -28,13 +29,13 @@
</template>
</a-button>
<template #content>
<a-doption @click="onModifyRow(item)">
<a-doption @click="formCtx.open(item)">
<template #icon>
<i class="icon-park-outline-edit"></i>
</template>
修改
</a-doption>
<a-doption class="!text-red-500" @click="onDeleteRow">
<a-doption class="!text-red-500" @click="onDeleteRow(item)">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
@ -50,86 +51,77 @@
</template>
<script setup lang="ts">
import { DictType, api } from "@/api";
import { useAniFormModal } from "@/components";
import { delConfirm } from "@/utils";
import { Message } from "@arco-design/web-vue";
import { PropType } from "vue";
const data = [
{
id: 1,
title: "用户性别",
count: 23,
defineProps({
current: {
type: Object as PropType<DictType>,
},
{
id: 2,
title: "微信头像",
count: 52,
},
{
id: 3,
title: "文章封面",
count: 19,
},
{
id: 4,
title: "山水诗画",
count: 81,
},
{
id: 5,
title: "虾米沙雕",
count: 12,
},
];
});
const list = ref(data);
const emit = defineEmits(["change"]);
const list = ref<DictType[]>([]);
const onModifyRow = (row: any) => {
formCtx.props.title = "修改字典";
formCtx.open(row);
const updateDictTypes = async () => {
const res = await api.dictType.getDictTypes({ size: 0 });
list.value = res.data.data ?? [];
list.value.length && emit("change", list.value[0]);
};
const onCreateRow = () => {
formCtx.props.title = "新建字典";
formCtx.open();
};
onMounted(updateDictTypes);
const onDeleteRow = async () => {
const onDeleteRow = async (row: DictType) => {
await delConfirm();
const res = await api.dictType.delDictType(row.id);
Message.success(res.data.message);
};
const [formModal, formCtx] = useAniFormModal({
title: "修改分组",
title: ({ model }) => (!model.id ? "新建字典类型" : "修改字典类型"),
trigger: false,
modalProps: {
width: 432,
width: 580,
},
model: {
id: undefined,
},
items: [
{
field: "title",
label: "分组名称",
field: "name",
label: "名称",
type: "input",
},
{
field: "code",
label: "唯一编码",
type: "input",
},
{
field: "description",
label: "备注信息",
type: "textarea",
},
],
submit: async ({ model }) => {
let res;
if (model.id) {
const item = list.value.find((i) => i.id === model.id);
if (item) {
item.title = model.title;
}
res = await api.dictType.setDictType(model.id, model);
} else {
const ids = list.value.map((i) => i.id);
const maxId = Math.max.apply(null, ids);
list.value.push({
id: maxId,
title: model.title,
count: 0,
});
res = await api.dictType.addDictType(model);
}
updateDictTypes();
return res;
},
});
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.active {
color: rgb(var(--primary-6));
background-color: rgb(var(--primary-1));
}
</style>

View File

@ -3,56 +3,125 @@
<div class="py-2 px-4 bg-white">
<bread-crumb></bread-crumb>
</div>
<div class="grid grid-cols-[auto_auto_1fr] h-full overflow-hidden bg-white p-4 m-4 rounded">
<div class="grid grid-cols-[auto_1fr] gap-4 overflow-hidden bg-white p-4 m-4 rounded">
<div>
<ani-group></ani-group>
<ani-group :current="current" @change="onTypeChange"></ani-group>
</div>
<a-divider direction="vertical"></a-divider>
<div>
<a-alert :show-icon="false" class="mb-3 !border-brand-500">
<span class="text-brand-500 font-bold">{{ current?.name }}</span>
<div class="mt-1">描述{{ current?.description }}</div>
</a-alert>
<dict-table></dict-table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
<script setup lang="tsx">
import { DictType, api } from "@/api";
import aniGroup from "./components/group.vue";
import { useAniTable, useForm, Form } from "@/components";
import { useAniTable, createColumn, updateColumn } from "@/components";
const form = useForm({
items: [
{
field: "字典名",
label: "字典名",
type: "input",
},
{
field: "字典名",
label: "",
type: "submit",
},
],
});
const current = ref<DictType>();
const onTypeChange = (item: DictType) => {
current.value = item;
dict.refresh();
};
const [dictTable, dict] = useAniTable({
async data(search, paging) {
return api.dict.getDicts({ ...search, ...paging, typeId: current.value?.id } as any);
},
columns: [
{
title: "字典名",
title: "字典项",
dataIndex: "name",
render: ({ record }) => {
return (
<div>
<div>
{record.name}: {record.code}
</div>
<div class="text-gray-400 text-xs">{record.description}</div>
</div>
);
},
},
createColumn,
updateColumn,
{
title: "字典值",
dataIndex: "name",
title: "操作",
type: "button",
width: 140,
buttons: [
{
type: "modify",
text: "修改",
},
{
type: "delete",
text: "删除",
onClick: ({ record }) => {
return api.dict.delDict(record.id);
},
},
],
},
],
create: {
search: {
button: false,
items: [
{
field: "字典名",
field: "name",
label: "名称",
type: "search",
searchable: true,
enterable: true,
nodeProps: {
placeholder: "字典名称",
},
itemProps: {
hideLabel: true,
},
},
],
},
create: {
title: '新增字典',
model: {
typeId: undefined,
},
modalProps: {
width: 580,
},
items: [
{
field: "name",
label: "字典名",
type: "input",
},
{
field: "code",
label: "字典指",
type: "input",
},
{
field: "description",
label: "备注",
type: "textarea",
},
],
submit: async ({ model }) => {
return api.dict.addDict({ ...model, typeId: current.value?.id });
},
},
modify: {
extend: true,
title: "修改字典",
submit: async ({ model }) => {
return api.dict.setDict(model.id, { ...model, typeId: current.value?.id });
},
},
});
</script>

View File

@ -18,7 +18,6 @@ const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
const expanded = ref(false);
const toggleExpand = () => {
console.log(menu.tableRef.value);
expanded.value = !expanded.value;
menu.tableRef.value?.tableRef?.expandAll(expanded.value);
};
@ -64,12 +63,7 @@ const [menuTable, menu] = useAniTable({
<span>{record.name ?? "无"}</span>
<span class="text-gray-400 text-xs truncate">{id}</span>
</div>
<a-switch checked-color="#3c9" size="small">
{{
"checked-icon": () => <i class="icon-park-outline-check"></i>,
"unchecked-icon": () => <i class="icon-park-outline-close"></i>,
}}
</a-switch>
<a-switch checked-color="#3c9" size="small"></a-switch>
</div>
</div>
);
@ -102,13 +96,6 @@ const [menuTable, menu] = useAniTable({
},
],
},
// {
// title: "",
// dataIndex: "createdAt",
// width: 80,
// align: "center",
// render: ({ record }) => <a-switch checked-color="#3c9"></a-switch>,
// },
],
search: {
items: [
@ -137,10 +124,9 @@ const [menuTable, menu] = useAniTable({
initial: 0,
label: "父级",
type: "treeSelect",
async options(arg) {
async options() {
const res = await api.menu.getMenus({ size: 0, tree: true });
const data = res.data.data;
console.log(arg);
return [
{
id: 0,

View File

@ -17,7 +17,6 @@ declare module '@vue/runtime-core' {
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
@ -68,6 +67,7 @@ declare module '@vue/runtime-core' {
Editor: typeof import('./../components/editor/index.vue')['default']
Header: typeof import('./../components/editor/panel-main/components/header.vue')['default']
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
'Index.dev1': typeof import('./../components/breadcrumb/index.dev1.vue')['default']
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
@ -81,6 +81,7 @@ declare module '@vue/runtime-core' {
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
Texter: typeof import('./../components/editor/panel-main/components/texter.vue')['default']
Toast: typeof import('./../components/toast/toast.vue')['default']
}

View File

@ -1,3 +1,11 @@
/**
*
* @param list
* @param id ID key
* @param pid key
* @param cid key
* @returns
*/
export const listToTree = (list: any[], id = "id", pid = "parentId", cid = "children") => {
const map = list.reduce((res, v) => ((res[v[id]] = v), res), {});
return list.filter((item) => {
@ -10,11 +18,18 @@ export const listToTree = (list: any[], id = "id", pid = "parentId", cid = "chil
});
};
export function treeEach(tree: any[], fn: (item: any) => void) {
/**
*
* @param tree
* @param fn
* @param before 广
*/
export function treeEach(tree: any[], fn: (item: any) => void, before = true) {
for (const item of tree) {
fn(item);
before && fn(item);
if (item.children) {
treeEach(item.children, fn);
}
!before && fn(item);
}
}