Browse Source

1、完善菜单,角色模块

2、修正type类型提示
main
李金 7 months ago
parent
commit
0f855a931f
  1. 1
      package.json
  2. 12
      src/components/switch/index.tsx
  3. 5
      src/locales/lang/zh-CN.ts
  4. 52
      src/pages/system/menus/index.tsx
  5. 4
      src/pages/system/menus/store.ts
  6. 10
      src/pages/system/menus/style.ts
  7. 23
      src/pages/system/roles/index.tsx
  8. 26
      src/pages/system/roles/store.ts
  9. 61
      src/request.ts
  10. 8
      src/service/base.ts
  11. 3
      src/types/roles.d.ts
  12. 32
      src/utils/index.ts
  13. 8
      src/vite-env.d.ts
  14. 13
      vite.config.ts

1
package.json

@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host --port 3000", "dev": "vite --host --port 3000",
"dev:proxy": "vite --mode=proxy --host --port 3000",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"

12
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 (
<AntSwitch {...props} value={convertToBool(value)}/>
)
}
export default Switch

5
src/locales/lang/zh-CN.ts

@ -45,7 +45,7 @@ export default {
roles roles
}, },
actions: { actions: {
news: '新增',
news: '新增',
add: '添加', add: '添加',
edit: '编辑', edit: '编辑',
cancel: '取消', cancel: '取消',
@ -72,6 +72,9 @@ export default {
emptyDataAdd: '暂无数据,点击添加', emptyDataAdd: '暂无数据,点击添加',
required: '此项为必填项', required: '此项为必填项',
}, },
rules: {
required: '此项为必填项',
},
tabs: { tabs: {
refresh: '刷新', refresh: '刷新',
maximize: '最大化', maximize: '最大化',

52
src/pages/system/menus/index.tsx

@ -1,9 +1,10 @@
import Glass from '@/components/glass' import Glass from '@/components/glass'
import { useTranslation } from '@/i18n.ts' import { useTranslation } from '@/i18n.ts'
import { PlusOutlined } from '@ant-design/icons'
import { PageContainer, ProCard } from '@ant-design/pro-components' 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 IconPicker from '@/components/icon/picker'
import ButtonTable from './components/ButtonTable.tsx' import ButtonTable from './components/ButtonTable.tsx'
import { Flexbox } from 'react-layout-kit' import { Flexbox } from 'react-layout-kit'
@ -12,7 +13,7 @@ import { useStyle } from './style.ts'
import { MenuItem } from '@/types' import { MenuItem } from '@/types'
import MenuTree from './components/MenuTree.tsx' import MenuTree from './components/MenuTree.tsx'
import BatchButton from '@/pages/system/menus/components/BatchButton.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' import { createLazyFileRoute } from '@tanstack/react-router'
@ -23,7 +24,8 @@ const Menus = () => {
const [ form ] = Form.useForm() const [ form ] = Form.useForm()
const { mutate, isPending, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom) const { mutate, isPending, isSuccess, error, isError } = useAtomValue(saveOrUpdateMenuAtom)
const { data = [] } = useAtomValue(menuDataAtom) const { data = [] } = useAtomValue(menuDataAtom)
const currentMenu = useAtomValue<MenuItem>(selectedMenuAtom) ?? {}
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined)
useEffect(() => { useEffect(() => {
@ -35,6 +37,12 @@ const Menus = () => {
} }
}, [ isError, isSuccess ]) }, [ isError, isSuccess ])
useEffect(() => {
if (currentMenu.id === 0 && menuInputRef.current) {
menuInputRef.current.focus()
}
}, [ currentMenu ])
return ( return (
<PageContainer breadcrumbRender={false} title={false} className={styles.container}> <PageContainer breadcrumbRender={false} title={false} className={styles.container}>
@ -43,15 +51,33 @@ const Menus = () => {
placement="left" placement="left"
defaultSize={{ width: 300 }} defaultSize={{ width: 300 }}
maxWidth={500} maxWidth={500}
style={{ position: 'relative' }}
> >
<ProCard title={t('system.menus.title', '菜单')} <ProCard title={t('system.menus.title', '菜单')}
extra={ extra={
<>
<BatchButton/> <BatchButton/>
</>
} }
> >
<MenuTree form={form}/> <MenuTree form={form}/>
</ProCard> </ProCard>
<div className={styles.treeActions}>
<Divider style={{ flex: 1, margin: '8px 0' }}/>
<Button style={{ flex: 1 }} size={'small'}
block={true} type={'dashed'}
icon={<PlusOutlined/>}
onClick={() => {
const menu = {
...defaultMenu,
parent_id: 0,
}
setMenuData(menu)
form.setFieldsValue(menu)
}}
>{t('actions.news')}</Button>
</div>
</DraggablePanel> </DraggablePanel>
<Flexbox className={styles.box}> <Flexbox className={styles.box}>
<Glass <Glass
@ -83,8 +109,12 @@ const Menus = () => {
<Form.Item hidden={true} label={'id'} name={'id'}> <Form.Item hidden={true} label={'id'} name={'id'}>
<Input disabled={true}/> <Input disabled={true}/>
</Form.Item> </Form.Item>
<Form.Item label={t('system.menus.form.title', '菜单名称')} name={'title'}>
<Input/>
<Form.Item
rules={[
{ required: true, message: t('rules.required') }
]}
label={t('system.menus.form.title', '菜单名称')} name={'title'}>
<Input ref={menuInputRef as any} placeholder={t('system.menus.form.title', '菜单名称')}/>
</Form.Item> </Form.Item>
<Form.Item label={t('system.menus.form.parent', '上级菜单')} name={'parent_id'}> <Form.Item label={t('system.menus.form.parent', '上级菜单')} name={'parent_id'}>
<TreeSelect <TreeSelect
@ -121,8 +151,12 @@ const Menus = () => {
buttonStyle="solid" buttonStyle="solid"
/> />
</Form.Item> </Form.Item>
<Form.Item label={t('system.menus.form.name', '别名')} name={'name'}>
<Input/>
<Form.Item
rules={[
{ required: true, message: t('rules.required') }
]}
label={t('system.menus.form.name', '别名')} name={'name'}>
<Input placeholder={t('system.menus.form.name', '别名')}/>
</Form.Item> </Form.Item>
<Form.Item label={t('system.menus.form.icon', '图标')} name={'icon'}> <Form.Item label={t('system.menus.form.icon', '图标')} name={'icon'}>
<IconPicker placement={'left'}/> <IconPicker placement={'left'}/>

4
src/pages/system/menus/store.ts

@ -1,5 +1,5 @@
import systemServ from '@/service/system.ts' 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 { IMenu } from '@/types/menus'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { atom, createStore } from 'jotai' import { atom, createStore } from 'jotai'
@ -49,7 +49,7 @@ export const byIdMenuAtom = atomWithQuery((get) => ({
})) }))
export const saveOrUpdateMenuAtom = atomWithMutation<IApiResult<IMenu>>((get) => {
export const saveOrUpdateMenuAtom = atomWithMutation<any, IMenu>((get) => {
return { return {
mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ], mutationKey: [ 'updateMenu', get(selectedMenuIdAtom) ],

10
src/pages/system/menus/style.ts

@ -26,8 +26,6 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
background: ${token.colorBgContainer}; background: ${token.colorBgContainer};
` `
const emptyForm = css` const emptyForm = css`
backdrop-filter: ${token.backdropFilter};
color: red;
` `
const form = css` const form = css`
@ -62,7 +60,13 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => {
` `
const treeActions = css` const treeActions = css`
padding: 0 10px 10px;
display: flex;
flex-direction: column;
position: sticky;
bottom: 0;
z-index: 10;
background: ${token.colorBgContainer};
` `
return { return {

23
src/pages/system/roles/index.tsx

@ -1,9 +1,10 @@
import Switch from '@/components/switch'
import { import {
ActionType, ActionType,
PageContainer, PageContainer,
ProColumns, ProColumns,
ProTable, ProTable,
BetaSchemaForm,
BetaSchemaForm, ProFormColumnsType,
} from '@ant-design/pro-components' } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router' import { createLazyFileRoute } from '@tanstack/react-router'
import { useStyle } from './style.ts' import { useStyle } from './style.ts'
@ -57,7 +58,7 @@ const Roles = memo(() => {
const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom) const [ roleIds, setRoleIds ] = useAtom(roleIdsAtom)
const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom) const { data, isLoading, isFetching, refetch } = useAtomValue(rolesAtom)
const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom) const { isPending, mutate, isSuccess } = useAtomValue(saveOrUpdateRoleAtom)
const { mutate: deleteRole, isPending: isDeleteing } = useAtomValue(deleteRoleAtom)
const { mutate: deleteRole, isPending: isDeleting } = useAtomValue(deleteRoleAtom)
const [ , setRole ] = useAtom(roleAtom) const [ , setRole ] = useAtom(roleAtom)
const [ open, setOpen ] = useState(false) const [ open, setOpen ] = useState(false)
@ -84,7 +85,12 @@ const Roles = memo(() => {
rules: [ { required: true, message: t('message.required') } ] rules: [ { required: true, message: t('message.required') } ]
} }
}, },
{ title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch', },
{
title: t('system.roles.columns.status'), dataIndex: 'status', valueType: 'switch',
render: (text) => {
return <Switch value={!!text} size={'small'}/>
}
},
{ {
title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit', title: t('system.roles.columns.sort'), dataIndex: 'sort', valueType: 'digit',
}, },
@ -102,7 +108,7 @@ const Roles = memo(() => {
{ {
title: t('system.roles.columns.option'), valueType: 'option', title: t('system.roles.columns.option'), valueType: 'option',
key: 'option', key: 'option',
render: (text, record, _, action) => [
render: (_, record) => [
<a key="editable" <a key="editable"
onClick={() => { onClick={() => {
setRole(record) setRole(record)
@ -160,7 +166,7 @@ const Roles = memo(() => {
deleteRole(roleIds) deleteRole(roleIds)
}} }}
title={t('message.batchDelete')}> title={t('message.batchDelete')}>
<Button type={'link'} loading={isDeleteing}>{t('actions.batchDel')}</Button>
<Button type={'link'} loading={isDeleting}>{t('actions.batchDel')}</Button>
</Popconfirm> </Popconfirm>
</Space> </Space>
) )
@ -222,11 +228,12 @@ const Roles = memo(() => {
setOpen(open) setOpen(open)
}} }}
loading={isPending} loading={isPending}
onFinish={(values) => {
onFinish={async (values) => {
// console.log('values', values) // console.log('values', values)
return mutate(values)
mutate(values)
return true
}} }}
columns={columns}/>
columns={columns as ProFormColumnsType[]}/>
</PageContainer> </PageContainer>
) )
}) })

26
src/pages/system/roles/store.ts

@ -1,7 +1,8 @@
import { convertToBool } from '@/utils'
import { atom } from 'jotai/index' import { atom } from 'jotai/index'
import { IRole } from '@/types/roles' import { IRole } from '@/types/roles'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' 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 systemServ from '@/service/system.ts'
import { message } from 'antd' import { message } from 'antd'
import { t } from '@/i18n.ts' import { t } from '@/i18n.ts'
@ -23,26 +24,37 @@ export const pageAtom = atom<IPage>({
page: 1, page: 1,
}) })
export const rolesAtom = atomWithQuery<any, IPageResult<IRole>>((get) => {
export const rolesAtom = atomWithQuery((get) => {
return { return {
queryKey: [ 'roles', get(searchAtom) ], queryKey: [ 'roles', get(searchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => { queryFn: async ({ queryKey: [ , params ] }) => {
return await systemServ.role.list(params as SearchParams) 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 //saveOrUpdateRoleAtom
export const saveOrUpdateRoleAtom = atomWithMutation<IApiResult<IRole>>((get) => {
export const saveOrUpdateRoleAtom = atomWithMutation<any, IRole>((get) => {
return { return {
mutationKey: [ 'updateMenu' ], mutationKey: [ 'updateMenu' ],
mutationFn: async (data: IRole) => {
mutationFn: async (data) => {
data.status = data.status ? '1' : '0'
if (data.id === 0) { if (data.id === 0) {
return await systemServ.menus.add(data)
return await systemServ.role.add(data)
} }
return await systemServ.menus.update(data)
return await systemServ.role.update(data)
}, },
onSuccess: (res) => { onSuccess: (res) => {
const isAdd = !!res.data?.id const isAdd = !!res.data?.id
@ -65,7 +77,7 @@ export const deleteRoleAtom = atomWithMutation((get) => {
return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom)) return await systemServ.role.batchDelete(ids ?? get(roleIdsAtom))
}, },
onSuccess: (res) => { onSuccess: (res) => {
message.success('删除成功')
message.success('message.deleteSuccess')
//更新列表 //更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] }) get(queryClientAtom).invalidateQueries({ queryKey: [ 'roles', get(searchAtom) ] })
return res return res

61
src/request.ts

@ -1,12 +1,21 @@
import { getToken, setToken } from '@/store/system.ts' import { getToken, setToken } from '@/store/system.ts'
import { IApiResult } from '@/types'
import { Record } from '@icon-park/react'
import { message } from 'antd' import { message } from 'antd'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, {
AxiosRequestConfig,
AxiosInstance, AxiosResponse,
} from 'axios'
export type { AxiosRequestConfig } export type { AxiosRequestConfig }
type FetchMethod = <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<IApiResult<T>>
interface RequestMethods extends Pick<AxiosInstance, 'get' | 'post' | 'put' | 'delete' | 'request' | 'postForm' | 'patch' | 'patchForm' | 'putForm' | 'options'> {
}
export const request = axios.create({
const axiosInstance = axios.create({
baseURL: '/api/v1', baseURL: '/api/v1',
// timeout: 1000, // timeout: 1000,
headers: { headers: {
@ -15,7 +24,7 @@ export const request = axios.create({
}) })
//拦截request,添加token //拦截request,添加token
request.interceptors.request.use((config) => {
axiosInstance.interceptors.request.use((config) => {
const token = getToken() const token = getToken()
if (token) { if (token) {
@ -30,22 +39,25 @@ request.interceptors.request.use((config) => {
//拦截response,返回data //拦截response,返回data
request.interceptors.response.use((response: AxiosResponse) => {
axiosInstance.interceptors.response.use(
(response) => {
// console.log('response', response.data) // console.log('response', response.data)
message.destroy() message.destroy()
switch (response.data.code) {
const result = response.data as IApiResult
switch (result.code) {
case 200: case 200:
//login //login
if (response.config.url?.includes('/sys/login')) { if (response.config.url?.includes('/sys/login')) {
setToken(response.data.data.token)
setToken(result.data.token)
} }
return response.data
return response
case 401: case 401:
setToken('') setToken('')
if (window.location.pathname === '/login') { if (window.location.pathname === '/login') {
return
return Promise.reject(new Error('to login'))
} }
@ -59,9 +71,9 @@ request.interceptors.response.use((response: AxiosResponse) => {
redirect = window.location.pathname + '?=' + search.toString() redirect = window.location.pathname + '?=' + search.toString()
} }
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
return Promise.reject(new Error('to login'))
default: default:
message.error(response.data.message)
message.error(result.message ?? '请求失败')
return Promise.reject(response) return Promise.reject(response)
} }
@ -96,4 +108,33 @@ request.interceptors.response.use((response: AxiosResponse) => {
return Promise.reject(error) return Promise.reject(error)
}) })
export const createFetchMethods = () => {
const methods = {}
for (const method of Object.keys(axiosInstance)) {
methods[method] = <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => {
if (config && data) {
config = {
...config,
data,
}
}
return axiosInstance[method](url, config)
.then((response: AxiosResponse<IApiResult<T>>) => {
if (response.data.code !== 200) {
throw new Error(response.data.message)
}
return response.data as IApiResult<T>
})
.catch((err) => {
throw err
})
}
}
return methods as Record<keyof RequestMethods, FetchMethod>
}
export const request = createFetchMethods()
export default request export default request

8
src/service/base.ts

@ -6,19 +6,19 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
return { return {
list: (params?: TParams & IPage) => { list: (params?: TParams & IPage) => {
return request.post<IPageResult<TResult>>(`${api}/list`, { ...options, ...params }).then(data => data.data)
return request.post<IPageResult<TResult>>(`${api}/list`, { ...options, ...params })
}, },
add: (data: TParams) => { add: (data: TParams) => {
return request.post<TResult>(`${api}/add`, data, options) return request.post<TResult>(`${api}/add`, data, options)
}, },
update: (data: TParams) => { update: (data: TParams) => {
return request.post(`${api}/edit`, data, options)
return request.post<TResult>(`${api}/edit`, data, options)
}, },
delete: (id: number) => { delete: (id: number) => {
return request.post(`${api}/delete`, { id }, options)
return request.post<TResult>(`${api}/delete`, { id }, options)
}, },
batchDelete: (ids: number[]) => { batchDelete: (ids: number[]) => {
return request.post(`${api}/deletes`, { ids }, options )
return request.post<TResult>(`${api}/deletes`, { ids }, options)
}, },
info: (id: number) => { info: (id: number) => {
return request.get<TResult>(`${api}/${id}`, options) return request.get<TResult>(`${api}/${id}`, options)

3
src/types/roles.d.ts

@ -1,4 +1,3 @@
export interface IRole { export interface IRole {
id: number, id: number,
sort: number, sort: number,
@ -6,7 +5,7 @@ export interface IRole {
name: string, name: string,
description: string, description: string,
sequence: number, sequence: number,
status: string,
status: string | boolean,
menu_ids: number[] menu_ids: number[]
} }

32
src/utils/index.ts

@ -92,3 +92,35 @@ export function flattenTree<T>(tree: TreeItem<T>[], fieldNames?: FiledNames) {
return result 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
}
// 对于对象或数组,我们通常认为非空即为 true
if (Array.isArray(value) || typeof value === 'object') {
return !!Object.keys(value).length
}
// 其他情况,包括数字(非零)、字符串(已经被上述逻辑处理)和其他 truthy 值
return Boolean(value)
}

8
src/vite-env.d.ts

@ -1 +1,9 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly API_URL: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

13
vite.config.ts

@ -1,11 +1,17 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock' import { viteMockServe } from 'vite-plugin-mock'
//import { TanStackRouterVite } from '@tanstack/router-vite-plugin' //import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({
export default defineConfig(({ mode }) => {
// 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
const env = loadEnv(mode, process.cwd(), '')
return {
//定义别名的路径 //定义别名的路径
resolve: { resolve: {
alias: { alias: {
@ -15,7 +21,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://47.113.117.106:8000',
target: env.API_URL,
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path rewrite: (path) => path
} }
@ -33,4 +39,5 @@ export default defineConfig({
}), }),
//TanStackRouterVite(), //TanStackRouterVite(),
], ],
}
}) })
Loading…
Cancel
Save