feat: 优化菜单管理页面
parent
b490b6c9c5
commit
5b9c14184e
2
.env
2
.env
|
|
@ -2,7 +2,7 @@
|
||||||
# 应用配置
|
# 应用配置
|
||||||
# =====================================================================================
|
# =====================================================================================
|
||||||
# 网站标题
|
# 网站标题
|
||||||
VITE_TITLE = 绝弹管理后台
|
VITE_TITLE = 绝弹项目管理
|
||||||
# 网站副标题
|
# 网站副标题
|
||||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||||
# 接口前缀 说明:参见 axios 的 baseURL
|
# 接口前缀 说明:参见 axios 的 baseURL
|
||||||
|
|
|
||||||
|
|
@ -166,43 +166,6 @@ export interface AuthUserDto {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginedUserVo {
|
|
||||||
/** 用户ID */
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* 访问令牌
|
|
||||||
* @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjIsInVzZXJuYW1lIjoianVldGFuIiwiaWF0IjoxNjkxMTM5MjI3LCJleHAiOjE2OTExOTkyMjd9.6z7f-xfsHABbsyg401o2boKeqNQ1epPDYfEdavIcfYc"
|
|
||||||
*/
|
|
||||||
token: string;
|
|
||||||
/**
|
|
||||||
* 登录账号
|
|
||||||
* @example "juetan"
|
|
||||||
*/
|
|
||||||
username: string;
|
|
||||||
/**
|
|
||||||
* 用户昵称
|
|
||||||
* @example "绝弹"
|
|
||||||
*/
|
|
||||||
nickname: string;
|
|
||||||
/**
|
|
||||||
* 用户介绍
|
|
||||||
* @example "这个人很懒, 什么也没有留下!"
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* 用户头像
|
|
||||||
* @example "/upload/assets/222421415123.png "
|
|
||||||
*/
|
|
||||||
avatar: string;
|
|
||||||
/**
|
|
||||||
* 用户邮箱
|
|
||||||
* @example "example@mail.com"
|
|
||||||
*/
|
|
||||||
email: string;
|
|
||||||
/** 用户角色ID */
|
|
||||||
roleIds: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateLogDto {
|
export interface CreateLogDto {
|
||||||
/**
|
/**
|
||||||
* 字段描述(Swagger用途)
|
* 字段描述(Swagger用途)
|
||||||
|
|
@ -545,7 +508,6 @@ export interface Menu {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMenuDto {
|
export interface UpdateMenuDto {
|
||||||
id: number;
|
|
||||||
/**
|
/**
|
||||||
* 父级ID
|
* 父级ID
|
||||||
* @example 0
|
* @example 0
|
||||||
|
|
@ -950,7 +912,7 @@ export namespace Role {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @description 批量查询角色
|
* @description 分页查询角色
|
||||||
* @tags role
|
* @tags role
|
||||||
* @name GetRoles
|
* @name GetRoles
|
||||||
* @request GET:/api/v1/roles
|
* @request GET:/api/v1/roles
|
||||||
|
|
@ -1037,7 +999,7 @@ export namespace Role {
|
||||||
|
|
||||||
export namespace Auth {
|
export namespace Auth {
|
||||||
/**
|
/**
|
||||||
* @description 账号登陆
|
* @description 登陆
|
||||||
* @tags auth
|
* @tags auth
|
||||||
* @name Login
|
* @name Login
|
||||||
* @request POST:/api/v1/auth/login
|
* @request POST:/api/v1/auth/login
|
||||||
|
|
@ -1059,7 +1021,33 @@ export namespace Auth {
|
||||||
* @example "请求成功"
|
* @example "请求成功"
|
||||||
*/
|
*/
|
||||||
message: string;
|
message: string;
|
||||||
data?: LoginedUserVo;
|
data?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @description 获取登陆用户信息
|
||||||
|
* @tags auth
|
||||||
|
* @name GetUserInfo
|
||||||
|
* @request POST:/api/v1/auth/info
|
||||||
|
*/
|
||||||
|
export namespace GetUserInfo {
|
||||||
|
export type RequestParams = {};
|
||||||
|
export type RequestQuery = {};
|
||||||
|
export type RequestBody = never;
|
||||||
|
export type RequestHeaders = {};
|
||||||
|
export type ResponseBody = {
|
||||||
|
/**
|
||||||
|
* 状态码
|
||||||
|
* @format int32
|
||||||
|
* @example 2000
|
||||||
|
*/
|
||||||
|
code: number;
|
||||||
|
/**
|
||||||
|
* 提示信息
|
||||||
|
* @example "请求成功"
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
data?: User;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1889,7 +1877,7 @@ export namespace Menu {
|
||||||
*/
|
*/
|
||||||
export namespace GetMenu {
|
export namespace GetMenu {
|
||||||
export type RequestParams = {
|
export type RequestParams = {
|
||||||
id: string;
|
id: number;
|
||||||
};
|
};
|
||||||
export type RequestQuery = {};
|
export type RequestQuery = {};
|
||||||
export type RequestBody = never;
|
export type RequestBody = never;
|
||||||
|
|
@ -1917,7 +1905,7 @@ export namespace Menu {
|
||||||
*/
|
*/
|
||||||
export namespace SetMenu {
|
export namespace SetMenu {
|
||||||
export type RequestParams = {
|
export type RequestParams = {
|
||||||
id: string;
|
id: number;
|
||||||
};
|
};
|
||||||
export type RequestQuery = {};
|
export type RequestQuery = {};
|
||||||
export type RequestBody = UpdateMenuDto;
|
export type RequestBody = UpdateMenuDto;
|
||||||
|
|
@ -2250,7 +2238,7 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量查询角色
|
* 分页查询角色
|
||||||
*
|
*
|
||||||
* @tags role
|
* @tags role
|
||||||
* @name GetRoles
|
* @name GetRoles
|
||||||
|
|
@ -2349,7 +2337,7 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||||
};
|
};
|
||||||
auth = {
|
auth = {
|
||||||
/**
|
/**
|
||||||
* 账号登陆
|
* 登陆
|
||||||
*
|
*
|
||||||
* @tags auth
|
* @tags auth
|
||||||
* @name Login
|
* @name Login
|
||||||
|
|
@ -2369,7 +2357,7 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||||
* @example "请求成功"
|
* @example "请求成功"
|
||||||
*/
|
*/
|
||||||
message: string;
|
message: string;
|
||||||
data?: LoginedUserVo;
|
data?: string;
|
||||||
},
|
},
|
||||||
any
|
any
|
||||||
>({
|
>({
|
||||||
|
|
@ -2381,6 +2369,38 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取登陆用户信息
|
||||||
|
*
|
||||||
|
* @tags auth
|
||||||
|
* @name GetUserInfo
|
||||||
|
* @request POST:/api/v1/auth/info
|
||||||
|
*/
|
||||||
|
getUserInfo: (params: RequestParams = {}) => {
|
||||||
|
return this.request<
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 状态码
|
||||||
|
* @format int32
|
||||||
|
* @example 2000
|
||||||
|
*/
|
||||||
|
code: number;
|
||||||
|
/**
|
||||||
|
* 提示信息
|
||||||
|
* @example "请求成功"
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
data?: User;
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>({
|
||||||
|
path: `/api/v1/auth/info`,
|
||||||
|
method: "POST",
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
log = {
|
log = {
|
||||||
/**
|
/**
|
||||||
|
|
@ -3188,7 +3208,7 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||||
* @name GetMenu
|
* @name GetMenu
|
||||||
* @request GET:/api/v1/menus/{id}
|
* @request GET:/api/v1/menus/{id}
|
||||||
*/
|
*/
|
||||||
getMenu: (id: string, params: RequestParams = {}) => {
|
getMenu: (id: number, params: RequestParams = {}) => {
|
||||||
return this.request<
|
return this.request<
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -3220,7 +3240,7 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||||
* @name SetMenu
|
* @name SetMenu
|
||||||
* @request PATCH:/api/v1/menus/{id}
|
* @request PATCH:/api/v1/menus/{id}
|
||||||
*/
|
*/
|
||||||
setMenu: (id: string, data: UpdateMenuDto, params: RequestParams = {}) => {
|
setMenu: (id: number, data: UpdateMenuDto, params: RequestParams = {}) => {
|
||||||
return this.request<Response, any>({
|
return this.request<Response, any>({
|
||||||
path: `/api/v1/menus/${id}`,
|
path: `/api/v1/menus/${id}`,
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const defineColumn = <T extends TableColumn>(column: T) => {
|
||||||
export const updateColumn = defineColumn({
|
export const updateColumn = defineColumn({
|
||||||
title: "更新者",
|
title: "更新者",
|
||||||
dataIndex: "createdAt",
|
dataIndex: "createdAt",
|
||||||
width: 200,
|
width: 190,
|
||||||
render({ record }) {
|
render({ record }) {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
|
@ -24,7 +24,7 @@ export const updateColumn = defineColumn({
|
||||||
export const createColumn = defineColumn({
|
export const createColumn = defineColumn({
|
||||||
title: "创建者",
|
title: "创建者",
|
||||||
dataIndex: "createdAt",
|
dataIndex: "createdAt",
|
||||||
width: 200,
|
width: 190,
|
||||||
render({ record }) {
|
render({ record }) {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export const Table = defineComponent({
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const tableRef = ref<InstanceType<typeof BaseTable>>()
|
||||||
const searchRef = ref<FormInstance>();
|
const searchRef = ref<FormInstance>();
|
||||||
const createRef = ref<FormModalInstance>();
|
const createRef = ref<FormModalInstance>();
|
||||||
const modifyRef = ref<FormModalInstance>();
|
const modifyRef = ref<FormModalInstance>();
|
||||||
|
|
@ -142,6 +143,7 @@ export const Table = defineComponent({
|
||||||
const state = {
|
const state = {
|
||||||
loading,
|
loading,
|
||||||
inlined,
|
inlined,
|
||||||
|
tableRef,
|
||||||
searchRef,
|
searchRef,
|
||||||
createRef,
|
createRef,
|
||||||
modifyRef,
|
modifyRef,
|
||||||
|
|
@ -184,6 +186,7 @@ export const Table = defineComponent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseTable
|
<BaseTable
|
||||||
|
ref="tableRef"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
{...this.$attrs}
|
{...this.$attrs}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { delConfirm } from "@/utils";
|
import { delConfirm } from "@/utils";
|
||||||
import { Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
|
import { Divider, Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
|
||||||
import { isArray, merge } from "lodash-es";
|
import { isArray, merge } from "lodash-es";
|
||||||
import { Component, Ref, reactive } from "vue";
|
import { Component, Ref, reactive } from "vue";
|
||||||
import { useFormModal } from "../form";
|
import { useFormModal } from "../form";
|
||||||
|
|
@ -75,18 +75,21 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
|
||||||
buttons.push(merge({}, config.columnButtonBase));
|
buttons.push(merge({}, config.columnButtonBase));
|
||||||
}
|
}
|
||||||
column.render = (columnData) => {
|
column.render = (columnData) => {
|
||||||
return column.buttons?.map((btn) => {
|
return column.buttons?.map((btn, index) => {
|
||||||
if (btn.visible?.(columnData) === false) {
|
if (btn.visible?.(columnData) === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Link
|
<>
|
||||||
{...btn.buttonProps}
|
{index !== 0 ? <Divider direction="vertical" margin={2} class="!border-gray-300"></Divider> : null}
|
||||||
onClick={() => onClick(btn, columnData, getTable)}
|
<Link
|
||||||
disabled={btn.disabled?.(columnData)}
|
{...btn.buttonProps}
|
||||||
>
|
onClick={() => onClick(btn, columnData, getTable)}
|
||||||
{btn.text}
|
disabled={btn.disabled?.(columnData)}
|
||||||
</Link>
|
>
|
||||||
|
{btn.text}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,10 @@ const onSubmitForm = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await api.auth.login(model);
|
const res = await api.auth.login(model);
|
||||||
userStore.setUser(res.data.data);
|
userStore.setAccessToken(res.data.data as unknown as string);
|
||||||
Notification.success({
|
Notification.success({
|
||||||
title: "提示",
|
title: "提示",
|
||||||
content: `欢迎回来,${res.data.data.nickname}!`,
|
content: `登陆成功!`,
|
||||||
});
|
});
|
||||||
router.push({ path: (route.query.redirect as string) || "/" });
|
router.push({ path: (route.query.redirect as string) || "/" });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
|
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<i class="icon-park-outline-folder-close"></i>
|
<i class="icon-park-outline-folder-close align-[-2px]"></i>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
<span class="text-xs text-gray-500"> ({{ item.count }}) </span>
|
<span class="text-xs text-gray-500"> ({{ item.count }}) </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<a-modal v-model:visible="modal.visible" title="上传文件" title-align="start" :footer="false">
|
<a-modal v-model:visible="modal.visible" title="上传文件" title-align="start" :footer="false" :width="732">
|
||||||
|
<a-alert class="mb-4">
|
||||||
|
提示:支持大小在 1G 以内,格式为.png、.jpg、.webp、.mp4、.ogg的文件。
|
||||||
|
</a-alert>
|
||||||
<a-upload :custom-request="upload" draggable action="/api/v1/upload"></a-upload>
|
<a-upload :custom-request="upload" draggable action="/api/v1/upload"></a-upload>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { Table, useAniFormModal, useTable } from "@/components";
|
import { Table, createColumn, useAniFormModal, useTable } from "@/components";
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import { dayjs } from "@/libs/dayjs";
|
||||||
import numeral from "numeral";
|
import numeral from "numeral";
|
||||||
import AniGroup from './components/group.vue';
|
import AniGroup from './components/group.vue';
|
||||||
|
|
@ -77,7 +77,7 @@ const table = useTable({
|
||||||
<i class={`${getIcon(record.mimetype)} text-3xl mr-2`}></i>
|
<i class={`${getIcon(record.mimetype)} text-3xl mr-2`}></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>{record.name}</span>
|
<span class="hover:text-brand-500 cursor-pointer">{record.name}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">
|
<span class="text-gray-400 text-xs truncate">
|
||||||
{numeral(record.size).format("0 b")}
|
{numeral(record.size).format("0 b")}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -86,12 +86,7 @@ const table = useTable({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
createColumn,
|
||||||
title: "上传时间",
|
|
||||||
dataIndex: "createdAt",
|
|
||||||
width: 200,
|
|
||||||
render: ({ record }) => dayjs(record.createdAt).format(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
title: "操作",
|
title: "操作",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<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">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-add"></i>
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
<form-modal></form-modal>
|
||||||
|
</div>
|
||||||
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||||
|
<ul class="pl-0 mt-0">
|
||||||
|
<li
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<i class="icon-park-outline-folder-close align-[-2px]"></i>
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a-dropdown>
|
||||||
|
<a-button size="small" type="text">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i>
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<a-doption @click="onModifyRow(item)">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-edit"></i>
|
||||||
|
</template>
|
||||||
|
修改
|
||||||
|
</a-doption>
|
||||||
|
<a-doption class="!text-red-500" @click="onDeleteRow">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-delete"></i>
|
||||||
|
</template>
|
||||||
|
删除
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAniFormModal } from "@/components";
|
||||||
|
import { delConfirm } from "@/utils";
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "用户性别",
|
||||||
|
count: 23,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 onModifyRow = (row: any) => {
|
||||||
|
formCtx.props.title = "修改字典";
|
||||||
|
formCtx.open(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateRow = () => {
|
||||||
|
formCtx.props.title = "新建字典";
|
||||||
|
formCtx.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteRow = async () => {
|
||||||
|
await delConfirm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formModal, formCtx] = useAniFormModal({
|
||||||
|
title: "修改分组",
|
||||||
|
trigger: false,
|
||||||
|
modalProps: {
|
||||||
|
width: 432,
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
id: undefined,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
label: "分组名称",
|
||||||
|
type: "input",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
submit: async ({ model }) => {
|
||||||
|
if (model.id) {
|
||||||
|
const item = list.value.find((i) => i.id === model.id);
|
||||||
|
if (item) {
|
||||||
|
item.title = model.title;
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full w-full grid grid-rows-[auto_1fr] overflow-hidden">
|
||||||
|
<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>
|
||||||
|
<ani-group></ani-group>
|
||||||
|
</div>
|
||||||
|
<a-divider direction="vertical"></a-divider>
|
||||||
|
<div>
|
||||||
|
<dict-table></dict-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import aniGroup from "./components/group.vue";
|
||||||
|
import { useAniTable, useForm, Form } from "@/components";
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: "字典名",
|
||||||
|
label: "字典名",
|
||||||
|
type: "input",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "字典名",
|
||||||
|
label: "",
|
||||||
|
type: "submit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [dictTable, dict] = useAniTable({
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
title: "字典名",
|
||||||
|
dataIndex: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "字典值",
|
||||||
|
dataIndex: "name",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
create: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: "字典名",
|
||||||
|
label: "字典名",
|
||||||
|
type: "input",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"sort": 20010,
|
||||||
|
"title": "字典管理",
|
||||||
|
"icon": "icon-park-outline-spanner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
|
@ -57,21 +57,46 @@ const table = useTable({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "登陆地址",
|
||||||
|
dataIndex: "ip",
|
||||||
|
width: 200,
|
||||||
|
render({ record }) {
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span>{record.addr || "未知"}</span>
|
||||||
|
<span class="text-gray-400 text-xs truncate">{record.ip}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "操作系统",
|
title: "操作系统",
|
||||||
dataIndex: "os",
|
dataIndex: "os",
|
||||||
width: 160,
|
width: 200,
|
||||||
|
render({ record }) {
|
||||||
|
const [os, version] = record.os.split(" ");
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span>{os || "未知"}</span>
|
||||||
|
<span class="text-gray-400 text-xs truncate">{version}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "浏览器",
|
title: "浏览器",
|
||||||
dataIndex: "browser",
|
dataIndex: "browser",
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "登陆地址",
|
|
||||||
dataIndex: "ip",
|
|
||||||
width: 200,
|
width: 200,
|
||||||
render: ({ record }) => `${record.addr || "未知"}(${record.ip})`,
|
render({ record }) {
|
||||||
|
const [browser, version] = record.browser.split(" ");
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span>{browser || "未知"}</span>
|
||||||
|
<span class="text-gray-400 text-xs truncate">v{version}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
search: {
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,93 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page class="">
|
<bread-page class="">
|
||||||
<Table v-bind="table">
|
<menu-table>
|
||||||
<template #action>
|
<template #action>
|
||||||
<a-button type="outline">展开/折叠</a-button>
|
<a-button type="outline">展开/折叠</a-button>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</menu-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 { createColumn, updateColumn, useAniTable } from "@/components";
|
||||||
import { MenuTypes, MenuType } from "@/constants/menu";
|
import { MenuTypes, MenuType } from "@/constants/menu";
|
||||||
import { flatedMenus } from "@/router";
|
import { flatedMenus } from "@/router";
|
||||||
|
|
||||||
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
|
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
|
||||||
|
|
||||||
const table = useTable({
|
const expanded = ref(false);
|
||||||
|
const toggleExpand = () => {
|
||||||
|
console.log(menu.tableRef.value);
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
menu.tableRef.value?.tableRef?.expandAll(expanded.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [menuTable, menu] = useAniTable({
|
||||||
data: (search, paging) => {
|
data: (search, paging) => {
|
||||||
return api.menu.getMenus({ ...search, ...paging, tree: true });
|
return api.menu.getMenus({ ...search, ...paging, tree: true });
|
||||||
},
|
},
|
||||||
|
tableProps: {
|
||||||
|
defaultExpandAllRows: true,
|
||||||
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "菜单名称",
|
title: () => {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
菜单名称
|
||||||
|
<a-link class="ml-1 select-none" onClick={toggleExpand}>
|
||||||
|
{expanded.value ? "收起全部" : "展开全部"}
|
||||||
|
</a-link>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
width: 180,
|
render({ record }) {
|
||||||
},
|
let id = "";
|
||||||
{
|
if (record.type === MenuType.PAGE) {
|
||||||
title: "类型",
|
id = ` => ${record.path}`;
|
||||||
dataIndex: "description",
|
}
|
||||||
align: "center",
|
if (record.type === MenuType.BUTTON) {
|
||||||
width: 120,
|
id = ` => ${record.code}`;
|
||||||
render: ({ record }) => (
|
}
|
||||||
<a-tag color={MenuTypes.fmt(record.type, "color")}>
|
return (
|
||||||
{{
|
<div class="flex items-center gap-1">
|
||||||
icon: <i class={record.icon}></i>,
|
<a-tag bordered color={MenuTypes.fmt(record.type, "color")}>
|
||||||
default: () => MenuTypes.fmt(record.type),
|
{{
|
||||||
}}
|
default: () => MenuTypes.fmt(record.type),
|
||||||
</a-tag>
|
}}
|
||||||
),
|
</a-tag>
|
||||||
},
|
<div class="flex-1 flex overflow-hidden ml-1">
|
||||||
{
|
<div class="flex-1">
|
||||||
title: "访问路径",
|
<i class={`${record.icon} mr-1`}></i>
|
||||||
dataIndex: "path",
|
<span>{record.name ?? "无"}</span>
|
||||||
},
|
<span class="text-gray-400 text-xs truncate">{id}</span>
|
||||||
{
|
</div>
|
||||||
title: "启用",
|
<a-switch checked-color="#3c9" size="small">
|
||||||
dataIndex: "createdAt",
|
{{
|
||||||
width: 80,
|
"checked-icon": () => <i class="icon-park-outline-check"></i>,
|
||||||
align: "center",
|
"unchecked-icon": () => <i class="icon-park-outline-close"></i>,
|
||||||
render: ({ record }) => <a-switch size="small" checked-color="#3c9"></a-switch>,
|
}}
|
||||||
|
</a-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
createColumn,
|
createColumn,
|
||||||
updateColumn,
|
updateColumn,
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
type: "button",
|
type: "button",
|
||||||
width: 184,
|
width: 200,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: "modify",
|
type: "modify",
|
||||||
text: "修改",
|
text: "修改",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "新增下级",
|
text: "新增子项",
|
||||||
disabled: ({ record }) => record.type === MenuType.BUTTON,
|
disabled: ({ record }) => record.type === MenuType.BUTTON,
|
||||||
onClick: ({ record }) => {
|
onClick: ({ record }) => {
|
||||||
console.log(record);
|
console.log(record);
|
||||||
|
|
@ -78,10 +102,14 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// title: "启用",
|
||||||
|
// dataIndex: "createdAt",
|
||||||
|
// width: 80,
|
||||||
|
// align: "center",
|
||||||
|
// render: ({ record }) => <a-switch checked-color="#3c9"></a-switch>,
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
pagination: {
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
search: {
|
search: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|
@ -104,17 +132,6 @@ const table = useTable({
|
||||||
class: "!grid grid-cols-2 gap-x-4",
|
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",
|
field: "parentId",
|
||||||
initial: 0,
|
initial: 0,
|
||||||
|
|
@ -140,6 +157,17 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "type",
|
||||||
|
initial: 1,
|
||||||
|
label: "类型",
|
||||||
|
type: "radio",
|
||||||
|
options: MenuTypes.raw,
|
||||||
|
nodeProps: {
|
||||||
|
type: "button",
|
||||||
|
class: "w-full",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "name",
|
field: "name",
|
||||||
label: "名称",
|
label: "名称",
|
||||||
|
|
@ -209,12 +237,18 @@ const table = useTable({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less"></style>
|
<style lang="less">
|
||||||
|
.arco-table-cell-expand-icon {
|
||||||
|
span.arco-table-cell-inline-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"sort": 10201,
|
"sort": 10302,
|
||||||
"title": "菜单管理",
|
"title": "菜单管理",
|
||||||
"icon": "icon-park-outline-add-subtract"
|
"icon": "icon-park-outline-add-subtract"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const [roleTable, roleCtx] = useAniTable({
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
type: "button",
|
type: "button",
|
||||||
width: 184,
|
width: 200,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: "modify",
|
type: "modify",
|
||||||
|
|
@ -74,7 +74,7 @@ const [roleTable, roleCtx] = useAniTable({
|
||||||
create: {
|
create: {
|
||||||
title: "新建角色",
|
title: "新建角色",
|
||||||
modalProps: {
|
modalProps: {
|
||||||
width: 580,
|
width: 1080,
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
},
|
},
|
||||||
formProps: {
|
formProps: {
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@
|
||||||
@progress="onProgress"
|
@progress="onProgress"
|
||||||
>
|
>
|
||||||
<template #upload-button>
|
<template #upload-button>
|
||||||
<a-link>选择文件...</a-link>
|
<a-link>选择文件</a-link>
|
||||||
|
<a-divider direction="vertical" :margin="4"></a-divider>
|
||||||
|
<a-link>上传文件</a-link>
|
||||||
</template>
|
</template>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<div class="text-gray-400 text-xs">请选择不超过5MB,.png, .jpg, .webp格式的图片</div>
|
<div class="text-gray-400 text-xs">请选择大小不超过5MB,.png, .jpg, .webp格式的图片</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,8 +17,7 @@ export const usePassworModal = () => {
|
||||||
field: "password",
|
field: "password",
|
||||||
label: ({ model }) => (
|
label: ({ model }) => (
|
||||||
<span>
|
<span>
|
||||||
设置 <span class="text-brand-500 font-semibold">{model.nickname}</span>
|
<span class="text-brand-500 font-semibold">{model.nickname}</span> 的新密码:
|
||||||
的新密码
|
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
type: "input",
|
type: "input",
|
||||||
|
|
@ -8,9 +8,8 @@
|
||||||
<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 InputAvatar from "./components/avatar.vue";
|
||||||
import { usePassworModal } from "./password";
|
import { usePassworModal } from "./components/password";
|
||||||
import { MenuType } from "@/constants/menu";
|
|
||||||
|
|
||||||
const [passModal, passCtx] = usePassworModal();
|
const [passModal, passCtx] = usePassworModal();
|
||||||
|
|
||||||
|
|
@ -26,7 +25,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={record.avatar} alt="" />
|
{record.avatar?.startsWith("/") ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { LoginedUserVo } from "@/api";
|
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
|
|
@ -17,7 +16,7 @@ export const useUserStore = defineStore({
|
||||||
* 用户昵称
|
* 用户昵称
|
||||||
*/
|
*/
|
||||||
nickname: "绝弹",
|
nickname: "绝弹",
|
||||||
/** `
|
/** `
|
||||||
* 用户头像地址
|
* 用户头像地址
|
||||||
*/
|
*/
|
||||||
avatar: "https://github.com/juetan.png",
|
avatar: "https://github.com/juetan.png",
|
||||||
|
|
@ -39,17 +38,21 @@ export const useUserStore = defineStore({
|
||||||
this.accessToken = token;
|
this.accessToken = token;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAccessToken(token: string) {
|
||||||
|
this.accessToken = token;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除用户信息
|
* 清除用户信息
|
||||||
*/
|
*/
|
||||||
clearUser() {
|
clearUser() {
|
||||||
this.$reset()
|
this.$reset();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置用户信息
|
* 设置用户信息
|
||||||
*/
|
*/
|
||||||
setUser(user: LoginedUserVo) {
|
setUser(user: any) {
|
||||||
this.id = user.id;
|
this.id = user.id;
|
||||||
this.username = user.username;
|
this.username = user.username;
|
||||||
this.nickname = user.nickname;
|
this.nickname = user.nickname;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
[class*=" icon-"],
|
[class*=" icon-"],
|
||||||
[class^="icon-"] {
|
[class^="icon-"] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table .arco-form-item-layout-inline {
|
.table .arco-form-item-layout-inline {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue