feat: 优化菜单管理页面
parent
873cfac8c3
commit
b490b6c9c5
2
.env
2
.env
|
|
@ -6,7 +6,7 @@ VITE_TITLE = 绝弹管理后台
|
||||||
# 网站副标题
|
# 网站副标题
|
||||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||||
# 接口前缀 说明:参见 axios 的 baseURL
|
# 接口前缀 说明:参见 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({
|
const output = await generateApi({
|
||||||
url: "http://localhost:3030/openapi.json",
|
url: "http://localhost:3030/openapi.json",
|
||||||
templates: path.resolve(__dirname, "./template"),
|
templates: path.resolve(__dirname, "./template"),
|
||||||
// output: path.resolve(process.cwd(), "src/api/service"),
|
output: path.resolve(process.cwd(), "src/api/service"),
|
||||||
name: "Api.ts",
|
name: "Api.ts",
|
||||||
singleHttpClient: false,
|
singleHttpClient: false,
|
||||||
httpClientType: "axios",
|
httpClientType: "axios",
|
||||||
|
|
@ -28,7 +28,6 @@ const run = async () => {
|
||||||
parser: "typescript",
|
parser: "typescript",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
debugger;
|
|
||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -58,7 +58,7 @@ export const FormItem = (props: any, { emit }: any) => {
|
||||||
{{
|
{{
|
||||||
default: () => {
|
default: () => {
|
||||||
if (item.component) {
|
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;
|
const comp = nodeMap[item.type as NodeType]?.component;
|
||||||
if (!comp) {
|
if (!comp) {
|
||||||
|
|
@ -105,7 +105,7 @@ type FormItemBase = {
|
||||||
* 标签名
|
* 标签名
|
||||||
* @description 同FormItem组件的label属性
|
* @description 同FormItem组件的label属性
|
||||||
*/
|
*/
|
||||||
label?: string | ((item: IFormItem, model: Record<string, any>) => any);
|
label?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 传递给`FormItem`组件的参数
|
* 传递给`FormItem`组件的参数
|
||||||
|
|
|
||||||
|
|
@ -14,23 +14,24 @@ import {
|
||||||
Slider,
|
Slider,
|
||||||
Textarea,
|
Textarea,
|
||||||
TimePicker,
|
TimePicker,
|
||||||
|
TreeSelect,
|
||||||
} from "@arco-design/web-vue";
|
} from "@arco-design/web-vue";
|
||||||
|
|
||||||
const initOptions = ({ item, model }: any) => {
|
const initOptions = ({ item, model }: any, key = "options") => {
|
||||||
if (Array.isArray(item.options)) {
|
if (Array.isArray(item.options)) {
|
||||||
item.nodeProps.options = item.options;
|
item.nodeProps[key] = item.options;
|
||||||
}
|
}
|
||||||
if (typeof item.options === "function") {
|
if (typeof item.options === "function") {
|
||||||
const loadData = item.options;
|
const loadData = item.options;
|
||||||
item.nodeProps.options = reactive([]);
|
item.nodeProps[key] = reactive([]);
|
||||||
item._updateOptions = async () => {
|
item._updateOptions = async () => {
|
||||||
let data = await loadData({ item, model });
|
let data = await loadData({ item, model });
|
||||||
if (Array.isArray(data?.data?.data)) {
|
if (Array.isArray(data?.data?.data)) {
|
||||||
data = data.data.data.map((i: any) => ({ label: i.name, value: i.id }));
|
data = data.data.data.map((i: any) => ({ label: i.name, value: i.id }));
|
||||||
}
|
}
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
item.nodeProps.options.splice(0);
|
item.nodeProps[key].splice(0);
|
||||||
item.nodeProps.options.push(...data);
|
item.nodeProps[key].push(...data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
item._updateOptions();
|
item._updateOptions();
|
||||||
|
|
@ -104,6 +105,19 @@ export const nodeMap = {
|
||||||
} as InstanceType<typeof Select>["$props"],
|
} as InstanceType<typeof Select>["$props"],
|
||||||
init: initOptions,
|
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
|
* @see src/components/form/use-form.tsx
|
||||||
*/
|
*/
|
||||||
export const useForm = (options: Options) => {
|
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[] = [];
|
const items: IFormItem[] = [];
|
||||||
|
|
||||||
for (const item of options.items) {
|
for (const item of options.items) {
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ type ExtendedFormItem = Partial<IFormItem> & {
|
||||||
|
|
||||||
type SearchFormItem = ExtendedFormItem & {
|
type SearchFormItem = ExtendedFormItem & {
|
||||||
enableLoad?: boolean;
|
enableLoad?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
enterable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Search = Partial<
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const search = !item.enableLoad ? undefined : () => getTable().reloadData();
|
const onSearch = item.searchable ? () => getTable().reloadData() : undefined;
|
||||||
searchItems.push(
|
const onPressEnter = item.enterable ? () => getTable().reloadData() : undefined;
|
||||||
merge(
|
searchItems.push(merge({ nodeProps: { onSearch, onPressEnter } }, item));
|
||||||
{
|
|
||||||
nodeProps: {
|
|
||||||
onSearch: search,
|
|
||||||
onPressEnter: search,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (options.search.button !== false) {
|
if (options.search.button !== false) {
|
||||||
searchItems.push(config.searchItemSubmit);
|
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 { fileItem, onError, onProgress, onSuccess } = option;
|
||||||
const source = axios.CancelToken.source();
|
const source = axios.CancelToken.source();
|
||||||
if (fileItem.file) {
|
if (fileItem.file) {
|
||||||
api.upload
|
api.file
|
||||||
.addFile(
|
.addFile(
|
||||||
{
|
{
|
||||||
file: fileItem.file,
|
file: fileItem.file,
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const getIcon = (mimetype: string) => {
|
||||||
|
|
||||||
const table = useTable({
|
const table = useTable({
|
||||||
data: async (model, paging) => {
|
data: async (model, paging) => {
|
||||||
return api.upload.getUploads();
|
return api.file.getFiles();
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|
@ -105,7 +105,7 @@ const table = useTable({
|
||||||
type: "delete",
|
type: "delete",
|
||||||
text: "删除",
|
text: "删除",
|
||||||
onClick({ record }) {
|
onClick({ record }) {
|
||||||
return api.upload.delFile(record.id);
|
return api.file.delFile(record.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,44 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page class="">
|
<bread-page class="">
|
||||||
<Table v-bind="table"></Table>
|
<Table v-bind="table">
|
||||||
|
<template #action>
|
||||||
|
<a-button type="outline">展开/折叠</a-button>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
</bread-page>
|
</bread-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||||
import { menus } from "@/router";
|
import { MenuTypes, MenuType } from "@/constants/menu";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { flatedMenus } from "@/router";
|
||||||
|
|
||||||
const items = cloneDeep(menus) as any;
|
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
|
||||||
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 table = useTable({
|
const table = useTable({
|
||||||
data: items,
|
data: (search, paging) => {
|
||||||
|
return api.menu.getMenus({ ...search, ...paging, tree: true });
|
||||||
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "角色名称",
|
title: "菜单名称",
|
||||||
dataIndex: "title",
|
dataIndex: "name",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: ({ record }) => {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<i class={`${record.icon} mr-1 ml-1 vertical-[-2px]`}></i>
|
|
||||||
{ record.title }
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "类型",
|
title: "类型",
|
||||||
dataIndex: "description",
|
dataIndex: "description",
|
||||||
align: 'center',
|
align: "center",
|
||||||
width: 80,
|
width: 120,
|
||||||
render: () => <a-tag color="blue">菜单</a-tag>,
|
render: ({ record }) => (
|
||||||
|
<a-tag color={MenuTypes.fmt(record.type, "color")}>
|
||||||
|
{{
|
||||||
|
icon: <i class={record.icon}></i>,
|
||||||
|
default: () => MenuTypes.fmt(record.type),
|
||||||
|
}}
|
||||||
|
</a-tag>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "访问路径",
|
title: "访问路径",
|
||||||
|
|
@ -82,7 +48,7 @@ const table = useTable({
|
||||||
title: "启用",
|
title: "启用",
|
||||||
dataIndex: "createdAt",
|
dataIndex: "createdAt",
|
||||||
width: 80,
|
width: 80,
|
||||||
align: 'center',
|
align: "center",
|
||||||
render: ({ record }) => <a-switch size="small" checked-color="#3c9"></a-switch>,
|
render: ({ record }) => <a-switch size="small" checked-color="#3c9"></a-switch>,
|
||||||
},
|
},
|
||||||
createColumn,
|
createColumn,
|
||||||
|
|
@ -97,7 +63,8 @@ const table = useTable({
|
||||||
text: "修改",
|
text: "修改",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "分配权限",
|
text: "新增下级",
|
||||||
|
disabled: ({ record }) => record.type === MenuType.BUTTON,
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
console.log(record);
|
console.log(record);
|
||||||
},
|
},
|
||||||
|
|
@ -106,14 +73,14 @@ const table = useTable({
|
||||||
text: "删除",
|
text: "删除",
|
||||||
type: "delete",
|
type: "delete",
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
return api.role.delRole(record.id);
|
return api.menu.delMenu(record.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
visible: false
|
visible: false,
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -121,59 +88,122 @@ const table = useTable({
|
||||||
extend: "name",
|
extend: "name",
|
||||||
required: false,
|
required: false,
|
||||||
nodeProps: {
|
nodeProps: {
|
||||||
placeholder: "请输入角色名称",
|
placeholder: "菜单名称",
|
||||||
},
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: "新建角色",
|
title: "新建菜单",
|
||||||
modalProps: {
|
modalProps: {
|
||||||
width: 580,
|
width: 732,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
},
|
},
|
||||||
formProps: {
|
formProps: {
|
||||||
layout: "vertical",
|
layout: "vertical",
|
||||||
|
class: "!grid grid-cols-2 gap-x-4",
|
||||||
},
|
},
|
||||||
items: [
|
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",
|
field: "name",
|
||||||
label: "角色名称",
|
label: "名称",
|
||||||
type: "input",
|
type: "input",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "slug",
|
field: "code",
|
||||||
label: "角色标识",
|
label: "标识",
|
||||||
type: "input",
|
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",
|
field: "description",
|
||||||
label: "个人描述",
|
label: "菜单描述",
|
||||||
type: "textarea",
|
type: "textarea",
|
||||||
},
|
itemProps: {
|
||||||
{
|
class: "col-span-2",
|
||||||
field: "permissionIds",
|
|
||||||
label: "关联权限",
|
|
||||||
type: "select",
|
|
||||||
options: () => api.permission.getPermissions(),
|
|
||||||
nodeProps: {
|
|
||||||
multiple: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: ({ model }) => {
|
submit: ({ model }) => {
|
||||||
return api.role.addRole(model);
|
return api.menu.addMenu(model);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: "修改角色",
|
title: "修改菜单",
|
||||||
submit: ({ model }) => {
|
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>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<Table v-bind="table"> </Table>
|
<Table v-bind="table"> </Table>
|
||||||
|
<pass-modal></pass-modal>
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
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({
|
const table = useTable({
|
||||||
data: async (model, paging) => {
|
data: async (model, paging) => {
|
||||||
|
|
@ -20,7 +26,7 @@ const table = useTable({
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a-avatar size={32}>
|
<a-avatar size={32}>
|
||||||
<img src={`https://picsum.photos/200?${Math.random()}`} alt="" />
|
<img src={record.avatar} alt="" />
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
||||||
<span>{record.nickname}</span>
|
<span>{record.nickname}</span>
|
||||||
|
|
@ -51,6 +57,9 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "设置密码",
|
text: "设置密码",
|
||||||
|
onClick({ record }) {
|
||||||
|
passCtx.open(record);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "delete",
|
type: "delete",
|
||||||
|
|
@ -63,13 +72,14 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
search: {
|
||||||
button: false,
|
button: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
extend: "nickname",
|
field: "nickname",
|
||||||
required: false,
|
label: "用户昵称",
|
||||||
type: 'search',
|
type: "search",
|
||||||
enableLoad: true,
|
searchable: true,
|
||||||
|
enterable: true,
|
||||||
itemProps: {
|
itemProps: {
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
},
|
},
|
||||||
|
|
@ -89,23 +99,41 @@ const table = useTable({
|
||||||
layout: "vertical",
|
layout: "vertical",
|
||||||
class: "!grid grid-cols-2 gap-x-6",
|
class: "!grid grid-cols-2 gap-x-6",
|
||||||
},
|
},
|
||||||
|
model: {},
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
field: "avatar",
|
||||||
|
label: "用户头像",
|
||||||
|
type: "custom",
|
||||||
|
itemProps: {
|
||||||
|
class: "col-span-2",
|
||||||
|
},
|
||||||
|
component({ model }) {
|
||||||
|
return <InputAvatar v-model={model.avatar}></InputAvatar>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "username",
|
field: "username",
|
||||||
label: "登录账号",
|
label: "登录账号",
|
||||||
type: "input",
|
type: "input",
|
||||||
required: true,
|
required: true,
|
||||||
|
nodeProps: {
|
||||||
|
placeholder: "英文字母+数组组成,5~10位",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "password",
|
||||||
|
label: "登陆密码",
|
||||||
|
type: "input",
|
||||||
|
nodeProps: {
|
||||||
|
placeholder: "包含大小写,长度6 ~ 12位",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "nickname",
|
field: "nickname",
|
||||||
label: "用户昵称",
|
label: "用户昵称",
|
||||||
type: "input",
|
type: "input",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: "password",
|
|
||||||
label: "密码",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: "roleIds",
|
field: "roleIds",
|
||||||
label: "关联角色",
|
label: "关联角色",
|
||||||
|
|
@ -123,8 +151,8 @@ const table = useTable({
|
||||||
class: "col-span-2",
|
class: "col-span-2",
|
||||||
},
|
},
|
||||||
nodeProps: {
|
nodeProps: {
|
||||||
class: 'h-[96px]'
|
class: "h-[96px]",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: ({ model }) => {
|
submit: ({ model }) => {
|
||||||
|
|
@ -135,7 +163,7 @@ const table = useTable({
|
||||||
extend: true,
|
extend: true,
|
||||||
title: "修改用户",
|
title: "修改用户",
|
||||||
submit: ({ model }) => {
|
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 };
|
export type { MenuItem };
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ declare module '@vue/runtime-core' {
|
||||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||||
|
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
||||||
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
||||||
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
||||||
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
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];
|
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: {
|
server: {
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
proxy: {
|
||||||
|
"/upload": {
|
||||||
|
target: "http://127.0.0.1:3030",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue