diff --git a/package.json b/package.json index f494b76..0e3c9e2 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host --port 3000", + "dev:proxy": "vite --mode=proxy --host --port 3000", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" diff --git a/src/components/switch/index.tsx b/src/components/switch/index.tsx new file mode 100644 index 0000000..626049b --- /dev/null +++ b/src/components/switch/index.tsx @@ -0,0 +1,12 @@ +import { convertToBool } from '@/utils' +import { Switch as AntSwitch, SwitchProps } from 'antd' + + +export const Switch = ({ value, ...props }: SwitchProps) => { + console.log(value, props) + return ( + + ) +} + +export default Switch \ No newline at end of file diff --git a/src/locales/lang/zh-CN.ts b/src/locales/lang/zh-CN.ts index 469392c..f0340cb 100644 --- a/src/locales/lang/zh-CN.ts +++ b/src/locales/lang/zh-CN.ts @@ -45,7 +45,7 @@ export default { roles }, actions: { - news: '新增加', + news: '新增', add: '添加', edit: '编辑', cancel: '取消', @@ -72,6 +72,9 @@ export default { emptyDataAdd: '暂无数据,点击添加', required: '此项为必填项', }, + rules: { + required: '此项为必填项', + }, tabs: { refresh: '刷新', maximize: '最大化', diff --git a/src/pages/system/menus/index.tsx b/src/pages/system/menus/index.tsx index 39389e5..a1b7be9 100644 --- a/src/pages/system/menus/index.tsx +++ b/src/pages/system/menus/index.tsx @@ -1,9 +1,10 @@ import Glass from '@/components/glass' import { useTranslation } from '@/i18n.ts' +import { PlusOutlined } from '@ant-design/icons' import { PageContainer, ProCard } from '@ant-design/pro-components' -import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert } from 'antd' -import { useAtomValue } from 'jotai' -import { menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts' +import { Button, Form, Input, Radio, TreeSelect, InputNumber, notification, Alert, InputRef, Divider } from 'antd' +import { useAtom, useAtomValue } from 'jotai' +import { defaultMenu, menuDataAtom, saveOrUpdateMenuAtom, selectedMenuAtom } from './store.ts' import IconPicker from '@/components/icon/picker' import ButtonTable from './components/ButtonTable.tsx' import { Flexbox } from 'react-layout-kit' @@ -12,7 +13,7 @@ import { useStyle } from './style.ts' import { MenuItem } from '@/types' import MenuTree from './components/MenuTree.tsx' import BatchButton from '@/pages/system/menus/components/BatchButton.tsx' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { createLazyFileRoute } from '@tanstack/react-router' @@ -23,7 +24,8 @@ const Menus = () => { const [ form ] = Form.useForm() const { mutate, isPending, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom) const { data = [] } = useAtomValue(menuDataAtom) - const currentMenu = useAtomValue(selectedMenuAtom) ?? {} + const [ currentMenu, setMenuData ] = useAtom(selectedMenuAtom) ?? {} + const menuInputRef = useRef(undefined) useEffect(() => { @@ -35,6 +37,12 @@ const Menus = () => { } }, [ isError, isSuccess ]) + useEffect(() => { + if (currentMenu.id === 0 && menuInputRef.current) { + menuInputRef.current.focus() + } + }, [ currentMenu ]) + return ( @@ -43,15 +51,33 @@ const Menus = () => { placement="left" defaultSize={{ width: 300 }} maxWidth={500} + style={{ position: 'relative' }} > + <> + + } > +
+ + +
{ - - + + { buttonStyle="solid" /> - - + + diff --git a/src/pages/system/menus/store.ts b/src/pages/system/menus/store.ts index 7b9abff..a64c384 100644 --- a/src/pages/system/menus/store.ts +++ b/src/pages/system/menus/store.ts @@ -1,5 +1,5 @@ import systemServ from '@/service/system.ts' -import { IApiResult, IPage, IPageResult, MenuItem } from '@/types' +import { IPage, IPageResult, MenuItem } from '@/types' import { IMenu } from '@/types/menus' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import { atom, createStore } from 'jotai' @@ -8,16 +8,16 @@ import { t } from '@/i18n.ts' export const defaultMenu = { - parent_id: 0, - type: 'menu', - name: '', - title: '', - icon: '', - path: '', - component: '', - sort: 0, - id: 0, - button: [], + parent_id: 0, + type: 'menu', + name: '', + title: '', + icon: '', + path: '', + component: '', + sort: 0, + id: 0, + button: [], } as unknown as MenuItem export const menuPageAtom = atom({}) @@ -26,67 +26,67 @@ const store = createStore() export const menuDataAtom = atomWithQuery>((get) => { - return { - queryKey: [ 'menus', get(menuPageAtom) ], - queryFn: async ({ queryKey: [ , page ] }) => { - return await systemServ.menus.list(page) - }, - select: (data) => { - return data.rows ?? [] - } + return { + queryKey: [ 'menus', get(menuPageAtom) ], + queryFn: async ({ queryKey: [ , page ] }) => { + return await systemServ.menus.list(page) + }, + select: (data) => { + return data.rows ?? [] } + } }) export const selectedMenuIdAtom = atom(0) export const selectedMenuAtom = atom({} as MenuItem) export const byIdMenuAtom = atomWithQuery((get) => ({ - queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], - queryFn: async ({ queryKey: [ , id ] }) => { - return await systemServ.menus.info(id as number) - }, - select: data => data.data, + queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], + queryFn: async ({ queryKey: [ , id ] }) => { + return await systemServ.menus.info(id as number) + }, + select: data => data.data, })) -export const saveOrUpdateMenuAtom = atomWithMutation>((get) => { +export const saveOrUpdateMenuAtom = atomWithMutation((get) => { - return { - mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], - mutationFn: async (data: IMenu) => { - if (data.id === 0) { - return await systemServ.menus.add(data) - } - return await systemServ.menus.update(data) - }, - onSuccess: (res) => { - const isAdd = !!res.data?.id - message.success(t( isAdd ? 'message.saveSuccess': 'message.editSuccess', '保存成功')) - const menu = get(selectedMenuAtom) - store.set(selectedMenuAtom, { - ...menu, - id: res.data?.id - }) - //更新列表 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore fix - get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ]}).then() - } + return { + mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], + mutationFn: async (data: IMenu) => { + if (data.id === 0) { + return await systemServ.menus.add(data) + } + return await systemServ.menus.update(data) + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + const menu = get(selectedMenuAtom) + store.set(selectedMenuAtom, { + ...menu, + id: res.data?.id + }) + //更新列表 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore fix + get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then() } + } }) export const batchIdsAtom = atom([]) export const deleteMenuAtom = atomWithMutation((get) => { - return { - mutationKey: [ 'deleteMenu', get(batchIdsAtom) ], - mutationFn: async (ids?: number[]) => { - return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom)) - }, - onSuccess: () => { - message.success(t('message.deleteSuccess', '删除成功')) - store.set(batchIdsAtom, []) - get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ]}).then() - } + return { + mutationKey: [ 'deleteMenu', get(batchIdsAtom) ], + mutationFn: async (ids?: number[]) => { + return await systemServ.menus.batchDelete(ids ?? get(batchIdsAtom)) + }, + onSuccess: () => { + message.success(t('message.deleteSuccess', '删除成功')) + store.set(batchIdsAtom, []) + get(queryClientAtom).invalidateQueries({ queryKey: [ 'menus', get(menuPageAtom) ] }).then() } + } }) \ No newline at end of file diff --git a/src/pages/system/menus/style.ts b/src/pages/system/menus/style.ts index 92550af..59194e7 100644 --- a/src/pages/system/menus/style.ts +++ b/src/pages/system/menus/style.ts @@ -26,8 +26,6 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { background: ${token.colorBgContainer}; ` const emptyForm = css` - backdrop-filter: ${token.backdropFilter}; - color: red; ` const form = css` @@ -62,7 +60,13 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { ` const treeActions = css` - + padding: 0 10px 10px; + display: flex; + flex-direction: column; + position: sticky; + bottom: 0; + z-index: 10; + background: ${token.colorBgContainer}; ` return { diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 759d57a..d72a458 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -1,22 +1,23 @@ +import Switch from '@/components/switch' import { - ActionType, - PageContainer, - ProColumns, - ProTable, - BetaSchemaForm, + ActionType, + PageContainer, + ProColumns, + ProTable, + BetaSchemaForm, ProFormColumnsType, } from '@ant-design/pro-components' import { createLazyFileRoute } from '@tanstack/react-router' import { useStyle } from './style.ts' import { memo, useEffect, useMemo, useRef, useState } from 'react' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { - deleteRoleAtom, - pageAtom, - roleAtom, - roleIdsAtom, - rolesAtom, - saveOrUpdateRoleAtom, - searchAtom + deleteRoleAtom, + pageAtom, + roleAtom, + roleIdsAtom, + rolesAtom, + saveOrUpdateRoleAtom, + searchAtom } from './store.ts' import { useTranslation } from '@/i18n.ts' import { Button, Form, Space, Spin, Table, Tree, Popconfirm } from 'antd' @@ -25,214 +26,220 @@ import { menuDataAtom } from '@/pages/system/menus/store.ts' import { getTreeCheckedStatus } from '@/utils/tree.ts' const MenuTree = (props: any) => { - const { data: menuList, isLoading: menuLoading } = useAtomValue(menuDataAtom) - const { value, onChange, form, id, mode } = props + const { data: menuList, isLoading: menuLoading } = useAtomValue(menuDataAtom) + const { value, onChange, form, id, mode } = props - const onCheck = (checkedKeys: any, info: any) => { - if (onChange) { - onChange([ ...checkedKeys, ...info.halfCheckedKeys ]) - } else { - form.setFieldsValue({ [id]: [ ...checkedKeys, ...info.halfCheckedKeys ] }) - } - } - if (menuLoading) { - return + const onCheck = (checkedKeys: any, info: any) => { + if (onChange) { + onChange([ ...checkedKeys, ...info.halfCheckedKeys ]) + } else { + form.setFieldsValue({ [id]: [ ...checkedKeys, ...info.halfCheckedKeys ] }) } - return + } + if (menuLoading) { + return + } + return } const Roles = memo(() => { - const { t } = useTranslation() - const { styles } = useStyle() - const [ form ] = Form.useForm() - const actionRef = useRef() - const [ page, setPage ] = useAtom(pageAtom) - const setSearch = useSetAtom(searchAtom) - const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom) - const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom) - const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom) - const { mutate: deleteRole, isPending: isDeleteing } = useAtomValue(deleteRoleAtom) - const [ , setRole ] = useAtom(roleAtom) - const [ open, setOpen ] = useState(false) + const { t } = useTranslation() + const { styles } = useStyle() + const [ form ] = Form.useForm() + const actionRef = useRef() + const [ page, setPage ] = useAtom(pageAtom) + const setSearch = useSetAtom(searchAtom) + const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom) + const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom) + const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom) + const { mutate: deleteRole, isPending: isDeleting } = useAtomValue(deleteRoleAtom) + const [ , setRole ] = useAtom(roleAtom) + const [ open, setOpen ] = useState(false) - const columns = useMemo(() => { - - return [ - { - title: 'id', dataIndex: 'id', - hideInTable: true, - hideInSearch: true, - formItemProps: { - hidden: true - } - }, - { - title: t('system.roles.columns.name'), dataIndex: 'name', valueType: 'text', - formItemProps: { - rules: [ { required: true, message: t('message.required') } ] - } - }, - { - title: t('system.roles.columns.code'), dataIndex: 'code', valueType: 'text', - formItemProps: { - rules: [ { required: true, message: t('message.required') } ] - } - }, - { title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch', }, - { - title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit', - }, - { title: t('system.roles.columns.description'), dataIndex: 'description', valueType: 'textarea' }, - { - title: t('system.roles.columns.menu_ids'), - hideInTable: true, - hideInSearch: true, - dataIndex: 'menu_ids', - valueType: 'text', - renderFormItem: (item, config, form) => { - return - } - }, - { - title: t('system.roles.columns.option'), valueType: 'option', - key: 'option', - render: (text, record, _, action) => [ - { - setRole(record) - setOpen(true) - form.setFieldsValue(record) - }} - > - {t('actions.edit', '编辑')} - , - { - deleteRole([ record.id ]) - }} - title={t('message.deleteConfirm')}> - - {t('actions.delete', '删除')} - - - , - ], - }, - ] as ProColumns[] - }, []) + const columns = useMemo(() => { - useEffect(() => { - if (isSuccess) { - setOpen(false) + return [ + { + title: 'id', dataIndex: 'id', + hideInTable: true, + hideInSearch: true, + formItemProps: { + hidden: true + } + }, + { + title: t('system.roles.columns.name'), dataIndex: 'name', valueType: 'text', + formItemProps: { + rules: [ { required: true, message: t('message.required') } ] + } + }, + { + title: t('system.roles.columns.code'), dataIndex: 'code', valueType: 'text', + formItemProps: { + rules: [ { required: true, message: t('message.required') } ] + } + }, + { + title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch', + render: (text) => { + return } - }, [ isSuccess ]) + }, + { + title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit', + }, + { title: t('system.roles.columns.description'), dataIndex: 'description', valueType: 'textarea' }, + { + title: t('system.roles.columns.menu_ids'), + hideInTable: true, + hideInSearch: true, + dataIndex: 'menu_ids', + valueType: 'text', + renderFormItem: (item, config, form) => { + return + } + }, + { + title: t('system.roles.columns.option'), valueType: 'option', + key: 'option', + render: (_, record) => [ + { + setRole(record) + setOpen(true) + form.setFieldsValue(record) + }} + > + {t('actions.edit', '编辑')} + , + { + deleteRole([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + , + ], + }, + ] as ProColumns[] + }, []) + + useEffect(() => { + if (isSuccess) { + setOpen(false) + } + }, [ isSuccess ]) - return ( - - { - setRoleIds(selectedRowKeys as number[]) - }, - selectedRowKeys: roleIds, - selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ], - }} - tableAlertOptionRender={() => { - return ( - - { - deleteRole(roleIds) - }} - title={t('message.batchDelete')}> - - - - ) - }} - options={{ - reload: () => { - refetch() - }, - }} - toolbar={{ - search: { - onSearch: (value: string) => { - setSearch({ key: value }) - }, - }, - actions: [ - , - ] - }} - pagination={{ - total: data?.total, - current: page.page, - pageSize: page.pageSize, - onChange: (page) => { + return ( + + { + setRoleIds(selectedRowKeys as number[]) + }, + selectedRowKeys: roleIds, + selections: [ Table.SELECTION_ALL, Table.SELECTION_INVERT ], + }} + tableAlertOptionRender={() => { + return ( + + { + deleteRole(roleIds) + }} + title={t('message.batchDelete')}> + + + + ) + }} + options={{ + reload: () => { + refetch() + }, + }} + toolbar={{ + search: { + onSearch: (value: string) => { + setSearch({ key: value }) + }, + }, + actions: [ + , + ] + }} + pagination={{ + total: data?.total, + current: page.page, + pageSize: page.pageSize, + onChange: (page) => { - setPage((prev) => { - return { ...prev, page } - }) - } - }} - /> - { - setOpen(open) - }} - loading={isPending} - onFinish={(values) => { - // console.log('values', values) - return mutate(values) - }} - columns={columns}/> - - ) + setPage((prev) => { + return { ...prev, page } + }) + } + }} + /> + { + setOpen(open) + }} + loading={isPending} + onFinish={async (values) => { + // console.log('values', values) + mutate(values) + return true + }} + columns={columns as ProFormColumnsType[]}/> + + ) }) export const Route = createLazyFileRoute('/system/roles')({ - component: Roles + component: Roles }) export default Roles \ No newline at end of file diff --git a/src/pages/system/roles/store.ts b/src/pages/system/roles/store.ts index 91c15d9..1a41dfd 100644 --- a/src/pages/system/roles/store.ts +++ b/src/pages/system/roles/store.ts @@ -1,13 +1,14 @@ +import { convertToBool } from '@/utils' import { atom } from 'jotai/index' import { IRole } from '@/types/roles' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' -import { IApiResult, IPage, IPageResult } from '@/types' +import { IPage } from '@/types' import systemServ from '@/service/system.ts' import { message } from 'antd' import { t } from '@/i18n.ts' type SearchParams = IPage & { - key?: string + key?: string } export const idAtom = atom(0) @@ -19,56 +20,67 @@ export const roleAtom = atom(undefined as unknown as IRole) export const searchAtom = atom({} as SearchParams) export const pageAtom = atom({ - pageSize: 10, - page: 1, + pageSize: 10, + page: 1, }) -export const rolesAtom = atomWithQuery>((get) => { - return { - queryKey: [ 'roles', get(searchAtom) ], - queryFn: async ({ queryKey: [ , params ] }) => { - return await systemServ.role.list(params as SearchParams) - }, +export const rolesAtom = atomWithQuery((get) => { + return { + queryKey: [ 'roles', get(searchAtom) ], + queryFn: async ({ queryKey: [ , params ] }) => { + return await systemServ.role.list(params as SearchParams) + }, + select: res => { + const data = res.data + data.rows = data.rows?.map(row => { + return { + ...row, + status: convertToBool(row.status) + } + }) + return data } + } }) //saveOrUpdateRoleAtom -export const saveOrUpdateRoleAtom = atomWithMutation>((get) => { +export const saveOrUpdateRoleAtom = atomWithMutation((get) => { - return { - mutationKey: [ 'updateMenu' ], - mutationFn: async (data: IRole) => { - if (data.id === 0) { - return await systemServ.menus.add(data) - } - return await systemServ.menus.update(data) - }, - onSuccess: (res) => { - const isAdd = !!res.data?.id - message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) + return { + mutationKey: [ 'updateMenu' ], + mutationFn: async (data) => { + data.status = data.status ? '1' : '0' + if (data.id === 0) { + return await systemServ.role.add(data) + } + return await systemServ.role.update(data) + }, + onSuccess: (res) => { + const isAdd = !!res.data?.id + message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) - //更新列表 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore fix - get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) + //更新列表 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore fix + get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) - return res - } + return res } + } }) export const deleteRoleAtom = atomWithMutation((get) => { - return { - mutationKey: [ 'deleteMenu' ], - mutationFn: async (ids: number[]) => { - return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom)) - }, - onSuccess: (res) => { - message.success('删除成功') - //更新列表 - get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) - return res - } + return { + mutationKey: [ 'deleteMenu' ], + mutationFn: async (ids: number[]) => { + return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom)) + }, + onSuccess: (res) => { + message.success('message.deleteSuccess') + //更新列表 + get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) + return res } + } }) \ No newline at end of file diff --git a/src/request.ts b/src/request.ts index 0f0aa56..e70f5e9 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,99 +1,140 @@ import { getToken, setToken } from '@/store/system.ts' +import { IApiResult } from '@/types' +import { Record } from '@icon-park/react' import { message } from 'antd' -import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import axios, { + AxiosRequestConfig, + AxiosInstance, AxiosResponse, +} from 'axios' export type { AxiosRequestConfig } +type FetchMethod = (url: string, data?: D, config?: AxiosRequestConfig) => Promise> +interface RequestMethods extends Pick { +} -export const request = axios.create({ - baseURL: '/api/v1', - // timeout: 1000, - headers: { - 'Content-Type': 'application/json', - }, + +const axiosInstance = axios.create({ + baseURL: '/api/v1', + // timeout: 1000, + headers: { + 'Content-Type': 'application/json', + }, }) //拦截request,添加token -request.interceptors.request.use((config) => { +axiosInstance.interceptors.request.use((config) => { - const token = getToken() - if (token) { - config.headers.Authorization = `Bearer ${token}` - } + const token = getToken() + if (token) { + config.headers.Authorization = `Bearer ${token}` + } - return config + return config }, (error) => { - console.log('error', error) - return Promise.reject(error) + console.log('error', error) + return Promise.reject(error) }) //拦截response,返回data -request.interceptors.response.use((response: AxiosResponse) => { +axiosInstance.interceptors.response.use( + (response) => { // console.log('response', response.data) message.destroy() - switch (response.data.code) { - case 200: - //login - if (response.config.url?.includes('/sys/login')) { - setToken(response.data.data.token) - } - return response.data - case 401: - setToken('') - if (window.location.pathname === '/login') { - return - } - - - // 401: 未登录 - message.error('登录失败,跳转重新登录') - // eslint-disable-next-line no-case-declarations - const search = new URLSearchParams(window.location.search) - // eslint-disable-next-line no-case-declarations - let redirect = window.location.pathname - if (search.toString() !== '') { - redirect = window.location.pathname + '?=' + search.toString() - } - window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` - return - default: - message.error(response.data.message) - return Promise.reject(response) + const result = response.data as IApiResult + + switch (result.code) { + case 200: + //login + if (response.config.url?.includes('/sys/login')) { + setToken(result.data.token) + } + return response + case 401: + setToken('') + if (window.location.pathname === '/login') { + return Promise.reject(new Error('to login')) + } + + + // 401: 未登录 + message.error('登录失败,跳转重新登录') + // eslint-disable-next-line no-case-declarations + const search = new URLSearchParams(window.location.search) + // eslint-disable-next-line no-case-declarations + let redirect = window.location.pathname + if (search.toString() !== '') { + redirect = window.location.pathname + '?=' + search.toString() + } + window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` + return Promise.reject(new Error('to login')) + default: + message.error(result.message ?? '请求失败') + return Promise.reject(response) } -}, (error) => { + }, (error) => { // console.log('error', error) const { response } = error if (response) { - switch (response.status) { - case 401: - if (window.location.pathname === '/login') { - return - } - - setToken('') - // 401: 未登录 - message.error('登录失败,跳转重新登录') - // eslint-disable-next-line no-case-declarations - const search = new URLSearchParams(window.location.search) - // eslint-disable-next-line no-case-declarations - let redirect = window.location.pathname - if (search.toString() !== '') { - redirect = window.location.pathname + '?=' + search.toString() - } - window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` - return - default: - message.error(response.data.message) - return Promise.reject(response) - } + switch (response.status) { + case 401: + if (window.location.pathname === '/login') { + return + } + + setToken('') + // 401: 未登录 + message.error('登录失败,跳转重新登录') + // eslint-disable-next-line no-case-declarations + const search = new URLSearchParams(window.location.search) + // eslint-disable-next-line no-case-declarations + let redirect = window.location.pathname + if (search.toString() !== '') { + redirect = window.location.pathname + '?=' + search.toString() + } + window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` + return + default: + message.error(response.data.message) + return Promise.reject(response) + } } return Promise.reject(error) -}) + }) + + +export const createFetchMethods = () => { + const methods = {} + + for (const method of Object.keys(axiosInstance)) { + methods[method] = (url: string, data?: D, config?: AxiosRequestConfig) => { + if (config && data) { + config = { + ...config, + data, + } + } + return axiosInstance[method](url, config) + .then((response: AxiosResponse>) => { + if (response.data.code !== 200) { + throw new Error(response.data.message) + } + return response.data as IApiResult + }) + .catch((err) => { + throw err + }) + } + } + + return methods as Record +} +export const request = createFetchMethods() export default request \ No newline at end of file diff --git a/src/service/base.ts b/src/service/base.ts index ebabceb..2bddb7b 100644 --- a/src/service/base.ts +++ b/src/service/base.ts @@ -6,19 +6,19 @@ export const createCURD = (api: string, options?: AxiosRequest return { list: (params?: TParams & IPage) => { - return request.post>(`${api}/list`, { ...options, ...params }).then(data => data.data) + return request.post>(`${api}/list`, { ...options, ...params }) }, add: (data: TParams) => { return request.post(`${api}/add`, data, options) }, update: (data: TParams) => { - return request.post(`${api}/edit`, data, options) + return request.post(`${api}/edit`, data, options) }, delete: (id: number) => { - return request.post(`${api}/delete`, { id }, options) + return request.post(`${api}/delete`, { id }, options) }, batchDelete: (ids: number[]) => { - return request.post(`${api}/deletes`, { ids }, options ) + return request.post(`${api}/deletes`, { ids }, options) }, info: (id: number) => { return request.get(`${api}/${id}`, options) diff --git a/src/types.d.ts b/src/types.d.ts index ce397e3..a21d1db 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -91,4 +91,4 @@ declare module '@tanstack/react-router' { } } -} \ No newline at end of file +} diff --git a/src/types/roles.d.ts b/src/types/roles.d.ts index cb6a9f3..286cd6c 100644 --- a/src/types/roles.d.ts +++ b/src/types/roles.d.ts @@ -1,13 +1,12 @@ - export interface IRole { - id: number, - sort: number, - code: string, - name: string, - description: string, - sequence: number, - status: string, - menu_ids: number[] + id: number, + sort: number, + code: string, + name: string, + description: string, + sequence: number, + status: string | boolean, + menu_ids: number[] } export interface RoleRequest extends IRole { @@ -15,17 +14,17 @@ export interface RoleRequest extends IRole { } export interface RoleListResponse { - key: string, - order: string, - prop: string, - page: number, - pageSize: number + key: string, + order: string, + prop: string, + page: number, + pageSize: number } export interface RoleListResponse { - page: number, - pageSize: number, - total: number, - rows: IRole[] + page: number, + pageSize: number, + total: number, + rows: IRole[] } \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 6759fc1..0d8a09b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,87 +8,119 @@ export const isDev = import.meta.env.MODE === 'development' // 格式化菜单数据, 把children转换成routes export const formatMenuData = (data: IMenu[]) => { - const result: MenuItem[] = [] - for (const item of data) { - if (item.icon && typeof item.icon === 'string') { - item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' }) - } - if (!item.children || !item.children.length) { - result.push({ - ...item, - key: item.name, - name: item.title - }) - } else { - const { children, name, ...other } = item - result.push({ - ...other, - key: name, - name: other.title, - children: formatMenuData(children), - routes: formatMenuData(children), - }) - - } + const result: MenuItem[] = [] + for (const item of data) { + if (item.icon && typeof item.icon === 'string') { + item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' }) } - return result + if (!item.children || !item.children.length) { + result.push({ + ...item, + key: item.name, + name: item.title + }) + } else { + const { children, name, ...other } = item + result.push({ + ...other, + key: name, + name: other.title, + children: formatMenuData(children), + routes: formatMenuData(children), + }) + + } + } + return result } //把MenuItem[]转换成antd树形结构 export const formatterMenuData = (data: MenuItem[]): TreeDataNode[] => { - const result: TreeDataNode[] = [] - for (const item of data) { - if (item.children && item.children.length) { - const { children, ...other } = item - result.push({ - ...other, - key: item.id!, - title: item.name!, - children: formatterMenuData(children), - }) - } else { - result.push({ - ...item, - key: item.id!, - title: item.name!, - }) - } + const result: TreeDataNode[] = [] + for (const item of data) { + if (item.children && item.children.length) { + const { children, ...other } = item + result.push({ + ...other, + key: item.id!, + title: item.name!, + children: formatterMenuData(children), + }) + } else { + result.push({ + ...item, + key: item.id!, + title: item.name!, + }) } - return result + } + return result } //把tree转成平铺数组 const defaultTreeFieldNames: FiledNames = { - key: 'id', - title: 'title', - children: 'children' + key: 'id', + title: 'title', + children: 'children' } export function flattenTree(tree: TreeItem[], fieldNames?: FiledNames) { - const result: FlattenData[] = [] + const result: FlattenData[] = [] + + if (!fieldNames) { + fieldNames = defaultTreeFieldNames + } - if (!fieldNames) { - fieldNames = defaultTreeFieldNames + function flattenRecursive(item: TreeItem, level: number, fieldNames: FiledNames) { + + const data: FlattenData = { + ...item, + key: item[fieldNames.key!], + title: item[fieldNames.title!], + level, + } + const children = item[fieldNames.children!] + if (children) { + children.forEach((child) => flattenRecursive(child, level + 1, fieldNames)) + data.children = children } + result.push(data) + } - function flattenRecursive(item: TreeItem, level: number, fieldNames: FiledNames) { - - const data: FlattenData = { - ...item, - key: item[fieldNames.key!], - title: item[fieldNames.title!], - level, - } - const children = item[fieldNames.children!] - if (children) { - children.forEach((child) => flattenRecursive(child, level + 1, fieldNames)) - data.children = children - } - result.push(data) + tree.forEach((item) => flattenRecursive(item, 0, fieldNames)) + + return result +} + + +export const convertToBool = (value: any): boolean => { + // 特殊处理字符串 '0'、'true' 和 'false' + if (typeof value === 'string') { + switch (value.toLowerCase()) { + case '0': + return false + case 'true': + return true + case 'false': + return false + default: + // 对于其他非空字符串,转换为 true + return Boolean(value) } + } + + // 处理常见 falsy 值 + if (value === undefined || value === null || + value === false || value === 0 || value === '' || Number.isNaN(value)) { + return false + } - tree.forEach((item) => flattenRecursive(item, 0, fieldNames)) + // 对于对象或数组,我们通常认为非空即为 true + if (Array.isArray(value) || typeof value === 'object') { + return !!Object.keys(value).length + } - return result + // 其他情况,包括数字(非零)、字符串(已经被上述逻辑处理)和其他 truthy 值 + return Boolean(value) } \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..5f5df73 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,9 @@ /// +interface ImportMetaEnv { + readonly API_URL: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index aeee389..3a0462d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,36 +1,43 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import { viteMockServe } from 'vite-plugin-mock' //import { TanStackRouterVite } from '@tanstack/router-vite-plugin' + // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ mode }) => { + + // 根据当前工作目录中的 `mode` 加载 .env 文件 + // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。 + const env = loadEnv(mode, process.cwd(), '') + return { //定义别名的路径 resolve: { - alias: { - '@': '/src', - }, + alias: { + '@': '/src', + }, }, server: { - proxy: { - '/api': { - target: 'http://47.113.117.106:8000', - changeOrigin: true, - rewrite: (path) => path - } + proxy: { + '/api': { + target: env.API_URL, + changeOrigin: true, + rewrite: (path) => path } + } }, plugins: [ - react(), - viteMockServe({ - // 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production') - enable: false, + react(), + viteMockServe({ + // 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production') + enable: false, - // mock 文件的根路径,默认值:'mocks' - mockPath: 'mock', - logger: true, - }), - //TanStackRouterVite(), + // mock 文件的根路径,默认值:'mocks' + mockPath: 'mock', + logger: true, + }), + //TanStackRouterVite(), ], + } })