@@ -101,10 +101,12 @@ const BasicLayout: React.FC = () => { | |||||
if(!item.parentPaths) { | if(!item.parentPaths) { | ||||
item.parentPaths = [] | item.parentPaths = [] | ||||
} | } | ||||
if(!item.path.startsWith('/')) { | if(!item.path.startsWith('/')) { | ||||
item.path = `/${item.path}` | item.path = `/${item.path}` | ||||
} | } | ||||
item.path = [...item.parentPaths, item.path].join('') | item.path = [...item.parentPaths, item.path].join('') | ||||
item.component = `${item.path}/index.tsx` | |||||
routes.push(item) | routes.push(item) | ||||
if(item.children && item.children.length > 0) { | if(item.children && item.children.length > 0) { | ||||
item.children.forEach(it => { | item.children.forEach(it => { | ||||
@@ -115,7 +117,6 @@ const BasicLayout: React.FC = () => { | |||||
} | } | ||||
menuList.data.forEach(item => fixAndPushMenu(item)) | menuList.data.forEach(item => fixAndPushMenu(item)) | ||||
setMenus([...formatedMenus, ...menuList.data]); | setMenus([...formatedMenus, ...menuList.data]); | ||||
debugger | |||||
console.log('components', components); | console.log('components', components); | ||||
replaceRoutes('*', [ | replaceRoutes('*', [ | ||||
...routes.map(menu => ({ | ...routes.map(menu => ({ | ||||
@@ -59,6 +59,7 @@ export interface UserDTO { | |||||
export interface Menu { | export interface Menu { | ||||
id: number; | id: number; | ||||
permission?: string; | |||||
parentId?: number; | parentId?: number; | ||||
name?: string; | name?: string; | ||||
icon?: string; | icon?: string; | ||||
@@ -67,7 +68,10 @@ export interface Menu { | |||||
component?: string; | component?: string; | ||||
keepAlive: boolean; | keepAlive: boolean; | ||||
type?: number; | type?: number; | ||||
sort?: number; | |||||
status?: number; | |||||
parentPaths?: string[]; | parentPaths?: string[]; | ||||
createTime: number; | |||||
children?: Array<Menu>; | children?: Array<Menu>; | ||||
} | } | ||||
@@ -0,0 +1,167 @@ | |||||
import { Space, Table, Button, Image, Divider, Tag, Card, Badge } from 'antd'; | |||||
import type { TableColumnsType } from 'antd'; | |||||
import { t } from '@/utils/i18n'; | |||||
import React, { useState, useEffect } from 'react'; | |||||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined, UndoOutlined } from '@ant-design/icons'; | |||||
import type { Menu } from '@/models' | |||||
import { antdUtils } from '@/utils/antd'; | |||||
import { useRequest } from '@/hooks/use-request'; | |||||
import menuService from '@/request/service/menu' | |||||
import { formatDate } from '@/utils/formatTime' | |||||
const MenuListPage: React.FC = () => { | |||||
const [dataSource, setDataSource] = useState<Menu[]>([]); | |||||
const { runAsync: getMenuList } = useRequest(menuService.getMenuList, { manual: true }); | |||||
const loadMenus = async () => { | |||||
const [error, { data: menus }] = await getMenuList(); | |||||
if (!error) { | |||||
const menuIds = menus.map(it => it.id) | |||||
const rootMenus = menus.filter((menu) => menuIds.indexOf(menu.parentId??0) < 0) | |||||
menus.forEach(menu => { | |||||
const parentMenu = menus.find(it => it.id === menu.parentId); //menu.parentId | |||||
if(parentMenu) { | |||||
if(!parentMenu.children) { | |||||
parentMenu.children = [] | |||||
} | |||||
parentMenu.children.push(menu) | |||||
} | |||||
}) | |||||
setDataSource(rootMenus); | |||||
} | |||||
}; | |||||
const showDeleteConfirm = () => { | |||||
antdUtils.modal?.confirm({ | |||||
title: '确认要将该菜单删除吗?', | |||||
icon: <ExclamationCircleFilled />, | |||||
content: '请注意删除以后不可恢复!', | |||||
okText: '删除', | |||||
okType: 'danger', | |||||
cancelText: '取消', | |||||
onOk() { | |||||
return new Promise((resolve, reject) => { | |||||
}).catch(() => antdUtils.message?.open({ | |||||
type: 'error', | |||||
content: '操作失败', | |||||
})); | |||||
}, | |||||
onCancel() { | |||||
}, | |||||
}); | |||||
}; | |||||
const columns: TableColumnsType<Menu> = [ | |||||
{ | |||||
title: '菜单名称', | |||||
dataIndex: 'name', | |||||
key: 'name', | |||||
align: 'center', | |||||
width: 200, | |||||
}, | |||||
{ | |||||
title: '菜单类型', | |||||
dataIndex: 'type', | |||||
key: 'type', | |||||
width: 100, | |||||
render: (value: number, record, index) => { | |||||
return (value === 1 ? <Tag color="purple">目录</Tag> : (value === 2 ?<Tag color="blue">菜单</Tag>: <Tag color="green">按钮</Tag>)) | |||||
} | |||||
}, | |||||
{ | |||||
title: '路由地址', | |||||
dataIndex: 'path', | |||||
key: 'path', | |||||
width: 150 | |||||
}, | |||||
{ | |||||
title: '组件路径', | |||||
dataIndex: 'component', | |||||
key: 'component', | |||||
width: 150 | |||||
}, | |||||
{ | |||||
title: '权限标识', | |||||
key: 'permission', | |||||
dataIndex: 'permission', | |||||
}, | |||||
{ | |||||
title: '排序', | |||||
key: 'sort', | |||||
dataIndex: 'sort', | |||||
width: 100, | |||||
}, | |||||
{ | |||||
title: '显示状态', | |||||
key: 'visible', | |||||
dataIndex: 'visible', | |||||
width: 100, | |||||
render: (value: boolean) => { | |||||
return (value? <Badge status="success" text="已显示" /> : <Badge status="warning" text="已隐藏" />) | |||||
} | |||||
}, | |||||
{ | |||||
title: '开启状态', | |||||
key: 'status', | |||||
dataIndex: 'status', | |||||
width: 100, | |||||
render: (value: number) => { | |||||
return (value === 0 ? <Badge status="success" text="已开启" /> : <Badge status="error" text="已关闭" />) | |||||
} | |||||
}, | |||||
{ | |||||
title: '创建时间', | |||||
key: 'createTime', | |||||
dataIndex: 'createTime', | |||||
width: 200, | |||||
render:(value: number) => { | |||||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||||
} | |||||
}, | |||||
{ | |||||
title: t("QkOmYwne" /* 操作 */), | |||||
key: 'action', | |||||
render: (_, record) => ( | |||||
<Space size="small" split={( | |||||
<Divider type='vertical' /> | |||||
)}> | |||||
<a | |||||
onClick={() => { | |||||
}}> | |||||
编辑 | |||||
</a> | |||||
<a | |||||
onClick={() => { | |||||
showDeleteConfirm() | |||||
}}> | |||||
删除 | |||||
</a> | |||||
</Space> | |||||
), | |||||
}, | |||||
]; | |||||
useEffect(() => { | |||||
loadMenus(); | |||||
}, []); | |||||
return ( | |||||
<> | |||||
<div> | |||||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||||
<Table rowKey="id" | |||||
scroll={{ x: true }} | |||||
columns={columns} | |||||
dataSource={dataSource} | |||||
className='bg-transparent' | |||||
pagination={{ position: ['bottomRight'] }} /> | |||||
</Card> | |||||
</div> | |||||
</> | |||||
); | |||||
}; | |||||
export default MenuListPage; |
@@ -14,7 +14,7 @@ import { ResponseDTO } from '@/models'; | |||||
const { apiUrl = '' } = useGlobSetting(); | const { apiUrl = '' } = useGlobSetting(); | ||||
const loginUrl = '/auth/login'; | const loginUrl = '/auth/login'; | ||||
const refreshTokenUrl = '/app-api/system/auth/refresh-token'; | |||||
const refreshTokenUrl = '/admin-api/system/auth/refresh-token'; | |||||
export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>; | export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>; | ||||
@@ -134,9 +134,19 @@ class Request { | |||||
response: AxiosResponse<any, any> | response: AxiosResponse<any, any> | ||||
): Promise<any> { | ): Promise<any> { | ||||
if (response.config.url !== refreshTokenUrl) { | if (response.config.url !== refreshTokenUrl) { | ||||
this.requestingCount -= 1; | |||||
if (this.requestQueue.length) { | |||||
this.requestByQueue(); | |||||
if(response.data && response.data.code && response.data.code === 401) { | |||||
return new Promise((resolve) => { | |||||
this.requestQueue.unshift({resolve, config: response.config, type: 'response'}); | |||||
if (this.refreshTokenFlag) return; | |||||
this.refreshTokenFlag = true; | |||||
this.refreshToken(); | |||||
}); | |||||
} else { | |||||
this.requestingCount -= 1; | |||||
if (this.requestQueue.length) { | |||||
this.requestByQueue(); | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -146,7 +156,6 @@ class Request { | |||||
private async responseErrorInterceptor(error: any): Promise<any> { | private async responseErrorInterceptor(error: any): Promise<any> { | ||||
this.requestingCount -= 1; | this.requestingCount -= 1; | ||||
const {config, status} = error?.response || {}; | const {config, status} = error?.response || {}; | ||||
if (status === 401) { | if (status === 401) { | ||||
return new Promise((resolve) => { | return new Promise((resolve) => { | ||||
this.requestQueue.unshift({resolve, config, type: 'response'}); | this.requestQueue.unshift({resolve, config, type: 'response'}); | ||||
@@ -0,0 +1 @@ | |||||
export * as menuService from './menu' |
@@ -0,0 +1,8 @@ | |||||
import request from '@/request'; | |||||
import { Menu } from '@/models'; | |||||
export default { | |||||
getMenuList: (params?: {name?: string, status?: number}) => { | |||||
return request.get<Menu[]>('/admin-api/system/menu/list', {params}); | |||||
}, | |||||
}; |
@@ -0,0 +1,176 @@ | |||||
/** | |||||
* 时间日期转换 | |||||
* @param date 当前时间,new Date() 格式 | |||||
* @param format 需要转换的时间格式字符串 | |||||
* @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd` | |||||
* @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ" | |||||
* @description format 星期:"YYYY-mm-dd HH:MM:SS WWW" | |||||
* @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ" | |||||
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" | |||||
* @returns 返回拼接后的时间字符串 | |||||
*/ | |||||
export function formatDate(date: Date, format: string): string { | |||||
const we = date.getDay() // 星期 | |||||
const z = getWeek(date) // 周 | |||||
const qut = Math.floor((date.getMonth() + 3) / 3).toString() // 季度 | |||||
const opt: { [key: string]: string } = { | |||||
'Y+': date.getFullYear().toString(), // 年 | |||||
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1) | |||||
'd+': date.getDate().toString(), // 日 | |||||
'H+': date.getHours().toString(), // 时 | |||||
'M+': date.getMinutes().toString(), // 分 | |||||
'S+': date.getSeconds().toString(), // 秒 | |||||
'q+': qut // 季度 | |||||
} | |||||
// 中文数字 (星期) | |||||
const week: { [key: string]: string } = { | |||||
'0': '日', | |||||
'1': '一', | |||||
'2': '二', | |||||
'3': '三', | |||||
'4': '四', | |||||
'5': '五', | |||||
'6': '六' | |||||
} | |||||
// 中文数字(季度) | |||||
const quarter: { [key: string]: string } = { | |||||
'1': '一', | |||||
'2': '二', | |||||
'3': '三', | |||||
'4': '四' | |||||
} | |||||
if (/(W+)/.test(format)) | |||||
format = format.replace( | |||||
RegExp.$1, | |||||
RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we] | |||||
) | |||||
if (/(Q+)/.test(format)) | |||||
format = format.replace( | |||||
RegExp.$1, | |||||
RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut] | |||||
) | |||||
if (/(Z+)/.test(format)) | |||||
format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '') | |||||
for (const k in opt) { | |||||
const r = new RegExp('(' + k + ')').exec(format) | |||||
// 若输入的长度不为1,则前面补零 | |||||
if (r) | |||||
format = format.replace( | |||||
r[1], | |||||
RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0') | |||||
) | |||||
} | |||||
return format | |||||
} | |||||
/** | |||||
* 获取当前日期是第几周 | |||||
* @param dateTime 当前传入的日期值 | |||||
* @returns 返回第几周数字值 | |||||
*/ | |||||
export function getWeek(dateTime: Date): number { | |||||
const temptTime = new Date(dateTime.getTime()) | |||||
// 周几 | |||||
const weekday = temptTime.getDay() || 7 | |||||
// 周1+5天=周六 | |||||
temptTime.setDate(temptTime.getDate() - weekday + 1 + 5) | |||||
let firstDay = new Date(temptTime.getFullYear(), 0, 1) | |||||
const dayOfWeek = firstDay.getDay() | |||||
let spendDay = 1 | |||||
if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1 | |||||
firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay) | |||||
const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000) | |||||
const result = Math.ceil(d / 7) | |||||
return result | |||||
} | |||||
/** | |||||
* 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前` | |||||
* @param param 当前时间,new Date() 格式或者字符串时间格式 | |||||
* @param format 需要转换的时间格式字符串 | |||||
* @description param 10秒: 10 * 1000 | |||||
* @description param 1分: 60 * 1000 | |||||
* @description param 1小时: 60 * 60 * 1000 | |||||
* @description param 24小时:60 * 60 * 24 * 1000 | |||||
* @description param 3天: 60 * 60* 24 * 1000 * 3 | |||||
* @returns 返回拼接后的时间字符串 | |||||
*/ | |||||
export function formatPast(param: string | Date, format = 'YYYY-mm-dd HH:MM:SS'): string { | |||||
// 传入格式处理、存储转换值 | |||||
let t: any, s: number | |||||
// 获取js 时间戳 | |||||
let time: number = new Date().getTime() | |||||
// 是否是对象 | |||||
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param) | |||||
// 当前时间戳 - 传入时间戳 | |||||
time = Number.parseInt(`${time - t}`) | |||||
if (time < 10000) { | |||||
// 10秒内 | |||||
return '刚刚' | |||||
} else if (time < 60000 && time >= 10000) { | |||||
// 超过10秒少于1分钟内 | |||||
s = Math.floor(time / 1000) | |||||
return `${s}秒前` | |||||
} else if (time < 3600000 && time >= 60000) { | |||||
// 超过1分钟少于1小时 | |||||
s = Math.floor(time / 60000) | |||||
return `${s}分钟前` | |||||
} else if (time < 86400000 && time >= 3600000) { | |||||
// 超过1小时少于24小时 | |||||
s = Math.floor(time / 3600000) | |||||
return `${s}小时前` | |||||
} else if (time < 259200000 && time >= 86400000) { | |||||
// 超过1天少于3天内 | |||||
s = Math.floor(time / 86400000) | |||||
return `${s}天前` | |||||
} else { | |||||
// 超过3天 | |||||
const date = typeof param === 'string' || 'object' ? new Date(param) : param | |||||
return formatDate(date, format) | |||||
} | |||||
} | |||||
/** | |||||
* 时间问候语 | |||||
* @param param 当前时间,new Date() 格式 | |||||
* @description param 调用 `formatAxis(new Date())` 输出 `上午好` | |||||
* @returns 返回拼接后的时间字符串 | |||||
*/ | |||||
export function formatAxis(param: Date): string { | |||||
const hour: number = new Date(param).getHours() | |||||
if (hour < 6) return '凌晨好' | |||||
else if (hour < 9) return '早上好' | |||||
else if (hour < 12) return '上午好' | |||||
else if (hour < 14) return '中午好' | |||||
else if (hour < 17) return '下午好' | |||||
else if (hour < 19) return '傍晚好' | |||||
else if (hour < 22) return '晚上好' | |||||
else return '夜里好' | |||||
} | |||||
/** | |||||
* 将毫秒,转换成时间字符串。例如说,xx 分钟 | |||||
* | |||||
* @param ms 毫秒 | |||||
* @returns {string} 字符串 | |||||
*/ | |||||
export function formatPast2(ms: number) { | |||||
const day = Math.floor(ms / (24 * 60 * 60 * 1000)) | |||||
const hour = Math.floor(ms / (60 * 60 * 1000) - day * 24) | |||||
const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60) | |||||
const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60) | |||||
if (day > 0) { | |||||
return day + '天' + hour + '小时' + minute + '分钟' | |||||
} | |||||
if (hour > 0) { | |||||
return hour + '小时' + minute + '分钟' | |||||
} | |||||
if (minute > 0) { | |||||
return minute + '分钟' | |||||
} | |||||
if (second > 0) { | |||||
return second + '秒' | |||||
} else { | |||||
return 0 + '秒' | |||||
} | |||||
} |