feat: 优化菜单管理页面
parent
873cfac8c3
commit
b490b6c9c5
2
.env
2
.env
|
|
@ -6,7 +6,7 @@ VITE_TITLE = 绝弹管理后台
|
|||
# 网站副标题
|
||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||
# 接口前缀 说明:参见 axios 的 baseURL
|
||||
VITE_API = https://nest.dev.juetan.cn/
|
||||
VITE_API = http://127.0.0.1:3030/
|
||||
|
||||
# =====================================================================================
|
||||
# 开发设置
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const run = async () => {
|
|||
const output = await generateApi({
|
||||
url: "http://localhost:3030/openapi.json",
|
||||
templates: path.resolve(__dirname, "./template"),
|
||||
// output: path.resolve(process.cwd(), "src/api/service"),
|
||||
output: path.resolve(process.cwd(), "src/api/service"),
|
||||
name: "Api.ts",
|
||||
singleHttpClient: false,
|
||||
httpClientType: "axios",
|
||||
|
|
@ -28,7 +28,6 @@ const run = async () => {
|
|||
parser: "typescript",
|
||||
},
|
||||
});
|
||||
debugger;
|
||||
return output;
|
||||
};
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -58,7 +58,7 @@ export const FormItem = (props: any, { emit }: any) => {
|
|||
{{
|
||||
default: () => {
|
||||
if (item.component) {
|
||||
return <item.component {...item.nodeProps} />;
|
||||
return <item.component {...item.nodeProps} model={props.model} item={props.item} />;
|
||||
}
|
||||
const comp = nodeMap[item.type as NodeType]?.component;
|
||||
if (!comp) {
|
||||
|
|
@ -105,7 +105,7 @@ type FormItemBase = {
|
|||
* 标签名
|
||||
* @description 同FormItem组件的label属性
|
||||
*/
|
||||
label?: string | ((item: IFormItem, model: Record<string, any>) => any);
|
||||
label?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
||||
|
||||
/**
|
||||
* 传递给`FormItem`组件的参数
|
||||
|
|
|
|||
|
|
@ -14,23 +14,24 @@ import {
|
|||
Slider,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
} from "@arco-design/web-vue";
|
||||
|
||||
const initOptions = ({ item, model }: any) => {
|
||||
const initOptions = ({ item, model }: any, key = "options") => {
|
||||
if (Array.isArray(item.options)) {
|
||||
item.nodeProps.options = item.options;
|
||||
item.nodeProps[key] = item.options;
|
||||
}
|
||||
if (typeof item.options === "function") {
|
||||
const loadData = item.options;
|
||||
item.nodeProps.options = reactive([]);
|
||||
item.nodeProps[key] = reactive([]);
|
||||
item._updateOptions = async () => {
|
||||
let data = await loadData({ item, model });
|
||||
if (Array.isArray(data?.data?.data)) {
|
||||
data = data.data.data.map((i: any) => ({ label: i.name, value: i.id }));
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
item.nodeProps.options.splice(0);
|
||||
item.nodeProps.options.push(...data);
|
||||
item.nodeProps[key].splice(0);
|
||||
item.nodeProps[key].push(...data);
|
||||
}
|
||||
};
|
||||
item._updateOptions();
|
||||
|
|
@ -104,6 +105,19 @@ export const nodeMap = {
|
|||
} as InstanceType<typeof Select>["$props"],
|
||||
init: initOptions,
|
||||
},
|
||||
/**
|
||||
* 选择框
|
||||
*/
|
||||
treeSelect: {
|
||||
component: TreeSelect,
|
||||
nodeProps: {
|
||||
placeholder: "请选择",
|
||||
allowClear: true,
|
||||
allowSearch: true,
|
||||
options: [{}],
|
||||
} as InstanceType<typeof TreeSelect>["$props"],
|
||||
init: (arg: any) => initOptions(arg, "data"),
|
||||
},
|
||||
/**
|
||||
* 级联选择框
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export type Options = {
|
|||
* @see src/components/form/use-form.tsx
|
||||
*/
|
||||
export const useForm = (options: Options) => {
|
||||
const { model = { id: undefined } } = options;
|
||||
const { model: _model = {} } = options;
|
||||
const model: Record<string, any> = { id: undefined, ..._model };
|
||||
const items: IFormItem[] = [];
|
||||
|
||||
for (const item of options.items) {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ type ExtendedFormItem = Partial<IFormItem> & {
|
|||
|
||||
type SearchFormItem = ExtendedFormItem & {
|
||||
enableLoad?: boolean;
|
||||
searchable?: boolean;
|
||||
enterable?: boolean;
|
||||
};
|
||||
|
||||
type Search = Partial<
|
||||
|
|
@ -115,7 +117,7 @@ type Search = Partial<
|
|||
/**
|
||||
* 显示/隐藏搜索按钮
|
||||
*/
|
||||
button?: boolean
|
||||
button?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
|
|||
|
|
@ -167,18 +167,9 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
|
|||
continue;
|
||||
}
|
||||
}
|
||||
const search = !item.enableLoad ? undefined : () => getTable().reloadData();
|
||||
searchItems.push(
|
||||
merge(
|
||||
{
|
||||
nodeProps: {
|
||||
onSearch: search,
|
||||
onPressEnter: search,
|
||||
},
|
||||
},
|
||||
item
|
||||
)
|
||||
);
|
||||
const onSearch = item.searchable ? () => getTable().reloadData() : undefined;
|
||||
const onPressEnter = item.enterable ? () => getTable().reloadData() : undefined;
|
||||
searchItems.push(merge({ nodeProps: { onSearch, onPressEnter } }, item));
|
||||
}
|
||||
if (options.search.button !== false) {
|
||||
searchItems.push(config.searchItemSubmit);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { defineConstants } from "./defineConstants";
|
||||
|
||||
export enum MenuType {
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
MENU = 1,
|
||||
/**
|
||||
* 页面
|
||||
*/
|
||||
PAGE = 2,
|
||||
/**
|
||||
* 按钮
|
||||
*/
|
||||
BUTTON = 3,
|
||||
}
|
||||
|
||||
export const MenuTypes = defineConstants([
|
||||
{
|
||||
value: MenuType.MENU,
|
||||
label: "目录",
|
||||
color: 'purple'
|
||||
},
|
||||
{
|
||||
value: MenuType.PAGE,
|
||||
label: "页面",
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
value: MenuType.BUTTON,
|
||||
label: "按钮",
|
||||
color: 'blue'
|
||||
},
|
||||
]);
|
||||
|
|
@ -17,7 +17,7 @@ const upload = (option: RequestOption) => {
|
|||
const { fileItem, onError, onProgress, onSuccess } = option;
|
||||
const source = axios.CancelToken.source();
|
||||
if (fileItem.file) {
|
||||
api.upload
|
||||
api.file
|
||||
.addFile(
|
||||
{
|
||||
file: fileItem.file,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const getIcon = (mimetype: string) => {
|
|||
|
||||
const table = useTable({
|
||||
data: async (model, paging) => {
|
||||
return api.upload.getUploads();
|
||||
return api.file.getFiles();
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
|
|
@ -105,7 +105,7 @@ const table = useTable({
|
|||
type: "delete",
|
||||
text: "删除",
|
||||
onClick({ record }) {
|
||||
return api.upload.delFile(record.id);
|
||||
return api.file.delFile(record.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,78 +1,44 @@
|
|||
<template>
|
||||
<bread-page class="">
|
||||
<Table v-bind="table"></Table>
|
||||
<Table v-bind="table">
|
||||
<template #action>
|
||||
<a-button type="outline">展开/折叠</a-button>
|
||||
</template>
|
||||
</Table>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||
import { menus } from "@/router";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { MenuTypes, MenuType } from "@/constants/menu";
|
||||
import { flatedMenus } from "@/router";
|
||||
|
||||
const items = cloneDeep(menus) as any;
|
||||
for (const item of items) {
|
||||
item.checked = false;
|
||||
// if (item.icon) {
|
||||
// const icon = item.icon;
|
||||
// item.icon = () => <i class={icon}></i>;
|
||||
// }
|
||||
item.switcherIcon = () => null;
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
// if (child.icon) {
|
||||
// const icon = child.icon;
|
||||
// child.icon = () => <i class={icon}></i>;
|
||||
// }
|
||||
child.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
menus: items,
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const indeter = (items: any[]) => {
|
||||
if (!items) {
|
||||
return false;
|
||||
}
|
||||
const checked = items.filter((item) => item.checked);
|
||||
return checked.length > 0 && checked.length < items.length;
|
||||
};
|
||||
|
||||
const onItemChange = (item: any, menu: any) => {
|
||||
const checked = menu.children.filter((item: any) => item.checked);
|
||||
if (checked === 0) {
|
||||
menu.checked = false;
|
||||
} else if (checked === menu.children.length) {
|
||||
menu.checked = true;
|
||||
}
|
||||
};
|
||||
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
|
||||
|
||||
const table = useTable({
|
||||
data: items,
|
||||
data: (search, paging) => {
|
||||
return api.menu.getMenus({ ...search, ...paging, tree: true });
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: "角色名称",
|
||||
dataIndex: "title",
|
||||
title: "菜单名称",
|
||||
dataIndex: "name",
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return (
|
||||
<span>
|
||||
<i class={`${record.icon} mr-1 ml-1 vertical-[-2px]`}></i>
|
||||
{ record.title }
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "description",
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: () => <a-tag color="blue">菜单</a-tag>,
|
||||
align: "center",
|
||||
width: 120,
|
||||
render: ({ record }) => (
|
||||
<a-tag color={MenuTypes.fmt(record.type, "color")}>
|
||||
{{
|
||||
icon: <i class={record.icon}></i>,
|
||||
default: () => MenuTypes.fmt(record.type),
|
||||
}}
|
||||
</a-tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "访问路径",
|
||||
|
|
@ -82,7 +48,7 @@ const table = useTable({
|
|||
title: "启用",
|
||||
dataIndex: "createdAt",
|
||||
width: 80,
|
||||
align: 'center',
|
||||
align: "center",
|
||||
render: ({ record }) => <a-switch size="small" checked-color="#3c9"></a-switch>,
|
||||
},
|
||||
createColumn,
|
||||
|
|
@ -97,7 +63,8 @@ const table = useTable({
|
|||
text: "修改",
|
||||
},
|
||||
{
|
||||
text: "分配权限",
|
||||
text: "新增下级",
|
||||
disabled: ({ record }) => record.type === MenuType.BUTTON,
|
||||
onClick: ({ record }) => {
|
||||
console.log(record);
|
||||
},
|
||||
|
|
@ -106,14 +73,14 @@ const table = useTable({
|
|||
text: "删除",
|
||||
type: "delete",
|
||||
onClick: ({ record }) => {
|
||||
return api.role.delRole(record.id);
|
||||
return api.menu.delMenu(record.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
visible: false
|
||||
visible: false,
|
||||
},
|
||||
search: {
|
||||
items: [
|
||||
|
|
@ -121,59 +88,122 @@ const table = useTable({
|
|||
extend: "name",
|
||||
required: false,
|
||||
nodeProps: {
|
||||
placeholder: "请输入角色名称",
|
||||
},
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
placeholder: "菜单名称",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建角色",
|
||||
title: "新建菜单",
|
||||
modalProps: {
|
||||
width: 580,
|
||||
width: 732,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-4",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "type",
|
||||
initial: 1,
|
||||
label: "类型",
|
||||
type: "radio",
|
||||
options: MenuTypes.raw,
|
||||
nodeProps: {
|
||||
type: "button",
|
||||
class: "w-full",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "parentId",
|
||||
initial: 0,
|
||||
label: "父级",
|
||||
type: "treeSelect",
|
||||
async options(arg) {
|
||||
const res = await api.menu.getMenus({ size: 0, tree: true });
|
||||
const data = res.data.data;
|
||||
console.log(arg);
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: "主类目",
|
||||
children: data,
|
||||
},
|
||||
];
|
||||
},
|
||||
nodeProps: {
|
||||
fieldNames: {
|
||||
icon: undefined,
|
||||
key: "id",
|
||||
title: "name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
label: "角色名称",
|
||||
label: "名称",
|
||||
type: "input",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: "slug",
|
||||
label: "角色标识",
|
||||
field: "code",
|
||||
label: "标识",
|
||||
type: "input",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: "icon",
|
||||
label: "图标",
|
||||
type: "input",
|
||||
required: true,
|
||||
visible: ({ model }) => model.type !== MenuType.BUTTON,
|
||||
},
|
||||
{
|
||||
field: "path",
|
||||
label: "路径",
|
||||
type: "input",
|
||||
required: true,
|
||||
visible: ({ model }) => model.type !== MenuType.BUTTON,
|
||||
nodeProps: {
|
||||
placeholder: "内链请以 / 开头,外链请以 http 开头",
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
match: /^(\/|http)/,
|
||||
message: "请以 / 或 http 开头",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: "component",
|
||||
label: "关联组件",
|
||||
type: "select",
|
||||
required: true,
|
||||
visible: ({ model }) => model.type === MenuType.PAGE,
|
||||
options: menuArr,
|
||||
nodeProps: {
|
||||
placeholder: "当前页面对应的前端组件",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "个人描述",
|
||||
label: "菜单描述",
|
||||
type: "textarea",
|
||||
},
|
||||
{
|
||||
field: "permissionIds",
|
||||
label: "关联权限",
|
||||
type: "select",
|
||||
options: () => api.permission.getPermissions(),
|
||||
nodeProps: {
|
||||
multiple: true,
|
||||
itemProps: {
|
||||
class: "col-span-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
return api.role.addRole(model);
|
||||
return api.menu.addMenu(model);
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改角色",
|
||||
title: "修改菜单",
|
||||
submit: ({ model }) => {
|
||||
return api.role.updateRole(model.id, model);
|
||||
return api.menu.setMenu(model.id, model);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div class="flex gap-2 items-center">
|
||||
<a-avatar>
|
||||
<img v-show="file?.url" :src="file?.url" alt="" />
|
||||
</a-avatar>
|
||||
<div>
|
||||
<a-upload
|
||||
action="/"
|
||||
accept=".png,.jpg,.webp"
|
||||
:fileList="file ? [file] : []"
|
||||
:show-file-list="false"
|
||||
:custom-request="upload"
|
||||
@change="onChange"
|
||||
@progress="onProgress"
|
||||
>
|
||||
<template #upload-button>
|
||||
<a-link>选择文件...</a-link>
|
||||
</template>
|
||||
</a-upload>
|
||||
<div class="text-gray-400 text-xs">请选择不超过5MB,.png, .jpg, .webp格式的图片</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RequestParams, api } from "@/api";
|
||||
import { FileItem, RequestOption } from "@arco-design/web-vue";
|
||||
import axios from "axios";
|
||||
import { ref } from "vue";
|
||||
|
||||
const file = ref();
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
file.value = { url: value };
|
||||
}
|
||||
);
|
||||
|
||||
const upload = (option: RequestOption) => {
|
||||
const { fileItem, onError, onProgress, onSuccess } = option;
|
||||
const source = axios.CancelToken.source();
|
||||
const send = async (file: File) => {
|
||||
const data = { file };
|
||||
const params: RequestParams = {
|
||||
onUploadProgress(e) {
|
||||
let percent = 0;
|
||||
if (e.total && e.total > 0) {
|
||||
percent = e.loaded / e.total;
|
||||
}
|
||||
onProgress(percent, e as any);
|
||||
},
|
||||
cancelToken: source.token,
|
||||
};
|
||||
try {
|
||||
const res = await api.file.addFile(data, params);
|
||||
onSuccess(res);
|
||||
console.log(res.data.data);
|
||||
emit("update:modelValue", res.data.data?.path);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
if (fileItem.file) {
|
||||
send(fileItem.file);
|
||||
}
|
||||
return {
|
||||
abort() {
|
||||
source.cancel();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const onChange = (_: any, currentFile: FileItem) => {
|
||||
file.value = {
|
||||
...currentFile,
|
||||
};
|
||||
};
|
||||
|
||||
const onProgress = (currentFile: FileItem) => {
|
||||
file.value = currentFile;
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<Table v-bind="table"> </Table>
|
||||
<pass-modal></pass-modal>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||
import InputAvatar from "./avatar.vue";
|
||||
import { usePassworModal } from "./password";
|
||||
import { MenuType } from "@/constants/menu";
|
||||
|
||||
const [passModal, passCtx] = usePassworModal();
|
||||
|
||||
const table = useTable({
|
||||
data: async (model, paging) => {
|
||||
|
|
@ -20,7 +26,7 @@ const table = useTable({
|
|||
render: ({ record }) => (
|
||||
<div class="flex items-center">
|
||||
<a-avatar size={32}>
|
||||
<img src={`https://picsum.photos/200?${Math.random()}`} alt="" />
|
||||
<img src={record.avatar} alt="" />
|
||||
</a-avatar>
|
||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
||||
<span>{record.nickname}</span>
|
||||
|
|
@ -51,6 +57,9 @@ const table = useTable({
|
|||
},
|
||||
{
|
||||
text: "设置密码",
|
||||
onClick({ record }) {
|
||||
passCtx.open(record);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
|
|
@ -63,13 +72,14 @@ const table = useTable({
|
|||
},
|
||||
],
|
||||
search: {
|
||||
button: false,
|
||||
button: true,
|
||||
items: [
|
||||
{
|
||||
extend: "nickname",
|
||||
required: false,
|
||||
type: 'search',
|
||||
enableLoad: true,
|
||||
field: "nickname",
|
||||
label: "用户昵称",
|
||||
type: "search",
|
||||
searchable: true,
|
||||
enterable: true,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
|
|
@ -89,23 +99,41 @@ const table = useTable({
|
|||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-6",
|
||||
},
|
||||
model: {},
|
||||
items: [
|
||||
{
|
||||
field: "avatar",
|
||||
label: "用户头像",
|
||||
type: "custom",
|
||||
itemProps: {
|
||||
class: "col-span-2",
|
||||
},
|
||||
component({ model }) {
|
||||
return <InputAvatar v-model={model.avatar}></InputAvatar>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "username",
|
||||
label: "登录账号",
|
||||
type: "input",
|
||||
required: true,
|
||||
nodeProps: {
|
||||
placeholder: "英文字母+数组组成,5~10位",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "登陆密码",
|
||||
type: "input",
|
||||
nodeProps: {
|
||||
placeholder: "包含大小写,长度6 ~ 12位",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "用户昵称",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "密码",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "roleIds",
|
||||
label: "关联角色",
|
||||
|
|
@ -123,8 +151,8 @@ const table = useTable({
|
|||
class: "col-span-2",
|
||||
},
|
||||
nodeProps: {
|
||||
class: 'h-[96px]'
|
||||
}
|
||||
class: "h-[96px]",
|
||||
},
|
||||
},
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
|
|
@ -135,7 +163,7 @@ const table = useTable({
|
|||
extend: true,
|
||||
title: "修改用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.updateUser(model.id, model);
|
||||
return api.user.setUser(model.id, model);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { api } from "@/api";
|
||||
import { useAniFormModal } from "@/components";
|
||||
|
||||
export const usePassworModal = () => {
|
||||
return useAniFormModal({
|
||||
title: "重置密码",
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
width: 432,
|
||||
},
|
||||
model: {
|
||||
id: undefined,
|
||||
nickname: undefined,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "password",
|
||||
label: ({ model }) => (
|
||||
<span>
|
||||
设置 <span class="text-brand-500 font-semibold">{model.nickname}</span>
|
||||
的新密码
|
||||
</span>
|
||||
),
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
return api.user.setUser(model.id, model);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -114,9 +114,19 @@ function transformToMenuItems(routes: RouteRecordRaw[]) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 由应用路由生成的菜单项
|
||||
* 扁平化的菜单
|
||||
*/
|
||||
const menus = transformToMenuItems(appRoutes);
|
||||
const flatedMenus = routesToItems(appRoutes);
|
||||
|
||||
export { menus };
|
||||
/**
|
||||
* 树结构菜单
|
||||
*/
|
||||
const treeMenus = listToTree(flatedMenus);
|
||||
|
||||
/**
|
||||
* 排序过的树级菜单
|
||||
*/
|
||||
const menus = sort(treeMenus);
|
||||
|
||||
export { menus, treeMenus, flatedMenus };
|
||||
export type { MenuItem };
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ declare module '@vue/runtime-core' {
|
|||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
||||
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
||||
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
||||
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
||||
|
|
|
|||
|
|
@ -9,3 +9,12 @@ export const listToTree = (list: any[], id = "id", pid = "parentId", cid = "chil
|
|||
return !item[pid];
|
||||
});
|
||||
};
|
||||
|
||||
export function treeEach(tree: any[], fn: (item: any) => void) {
|
||||
for (const item of tree) {
|
||||
fn(item);
|
||||
if (item.children) {
|
||||
treeEach(item.children, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ export default defineConfig(({ mode }) => {
|
|||
server: {
|
||||
host,
|
||||
port,
|
||||
proxy: {
|
||||
"/upload": {
|
||||
target: "http://127.0.0.1:3030",
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue