diff --git a/admin/.eslintrc b/admin/.eslintrc index 8067077..0d310d3 100644 --- a/admin/.eslintrc +++ b/admin/.eslintrc @@ -26,7 +26,11 @@ "version": "16.8" } }, - "plugins": ["react", "babel", "@typescript-eslint/eslint-plugin"], + "plugins": [ + "react", + "babel", + "@typescript-eslint/eslint-plugin" + ], "rules": { "react/display-name": 0, "react/prop-types": 0 diff --git a/admin/src/enum/dict.tsx b/admin/src/enum/dict.tsx new file mode 100644 index 0000000..9bdcf35 --- /dev/null +++ b/admin/src/enum/dict.tsx @@ -0,0 +1,24 @@ +import type {LabeledValue} from "@arco-design/web-react/es/Select/interface"; +import {Badge} from "@arco-design/web-react"; +import React from "react"; + +export const statusDict: LabeledValue[] = [ + {label: '启用', value: 1}, + {label: '禁用', value: 0} +] + +export const tableStatusDict: LabeledValue[] = [ + {label: , value: 1}, + {label: , value: 0} +] + +export const genderDict: LabeledValue[] = [ + {label: '男', value: 1}, + {label: '女', value: 0}, + {label: '未知', value: 2} +] + +export const getDictValue = (dict: LabeledValue[], value: number | string) => { + const findOne = dict.find(item => item.value === value); + return findOne ? findOne.label : ''; +} diff --git a/admin/src/enum/index.ts b/admin/src/enum/index.ts index 2dd5976..e69de29 100644 --- a/admin/src/enum/index.ts +++ b/admin/src/enum/index.ts @@ -1,7 +0,0 @@ -export const enum Gender { - MALE, - FEMALE, - UNKNOWN -} - -export const GenderDict = ['女', '男', '未知']; diff --git a/admin/src/main.tsx b/admin/src/main.tsx index 60319d9..fc88d0d 100644 --- a/admin/src/main.tsx +++ b/admin/src/main.tsx @@ -1,101 +1,103 @@ import './style/global.less'; -import React, { useEffect } from 'react'; +import React, {useEffect} from 'react'; import ReactDOM from 'react-dom'; -import { ConfigProvider } from '@arco-design/web-react'; +import {ConfigProvider} from '@arco-design/web-react'; import zhCN from '@arco-design/web-react/es/locale/zh-CN'; import enUS from '@arco-design/web-react/es/locale/en-US'; -import { BrowserRouter, Switch, Route } from 'react-router-dom'; +import {BrowserRouter, Switch, Route} from 'react-router-dom'; import PageLayout from './layout'; -import { GlobalContext } from './context'; +import {GlobalContext} from './context'; import Login from './pages/login'; import checkLogin from './utils/checkLogin'; import changeTheme from './utils/changeTheme'; import useStorage from './utils/useStorage'; -import { QueryClientProvider, QueryClient } from 'react-query'; -import { useStore } from '@/store'; +import {QueryClientProvider, QueryClient} from 'react-query'; +import {useStore} from '@/store'; const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 0 - } - } + defaultOptions: { + queries: { + retry: 0, + refetchOnWindowFocus: false, + keepPreviousData: true + }, + } }); function Index() { - const [lang, setLang] = useStorage('arco-lang', 'zh-CN'); - const [theme, setTheme] = useStorage('arco-theme', 'light'); - const { fetchUserInfo, init } = useStore(); + const [lang, setLang] = useStorage('arco-lang', 'zh-CN'); + const [theme, setTheme] = useStorage('arco-theme', 'light'); + const {fetchUserInfo, init} = useStore(); - function getArcoLocale() { - switch (lang) { - case 'zh-CN': - return zhCN; - case 'en-US': - return enUS; - default: - return zhCN; - } - } + function getArcoLocale() { + switch (lang) { + case 'zh-CN': + return zhCN; + case 'en-US': + return enUS; + default: + return zhCN; + } + } - useEffect(() => { - init(); - if (checkLogin()) { - fetchUserInfo(); - } else if (window.location.pathname.replace(/\//g, '') !== 'login') { - window.location.pathname = '/login'; - } - }, []); + useEffect(() => { + init(); + if (checkLogin()) { + fetchUserInfo(); + } else if (window.location.pathname.replace(/\//g, '') !== 'login') { + window.location.pathname = '/login'; + } + }, []); - useEffect(() => { - changeTheme(theme); - }, [theme]); + useEffect(() => { + changeTheme(theme); + }, [theme]); - const contextValue = { - lang, - setLang, - theme, - setTheme - }; + const contextValue = { + lang, + setLang, + theme, + setTheme + }; - return ( - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + ); } -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(, document.getElementById('root')); diff --git a/admin/src/pages/menu/form.tsx b/admin/src/pages/menu/form.tsx index bb917ab..22190d8 100644 --- a/admin/src/pages/menu/form.tsx +++ b/admin/src/pages/menu/form.tsx @@ -1,101 +1,101 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, Form, Input, Message, Select } from '@arco-design/web-react'; -import { createMenu, getMenu, updateMenu } from '@/api/menu'; +import React, {useEffect, useState} from 'react'; +import {Modal, Form, Input, Message, Select, InputNumber} from '@arco-design/web-react'; +import {createMenu, getMenu, updateMenu} from '@/api/menu'; interface Props { - record: { [key: string]: any } | null, - visible: boolean, - handleConfirm: () => void, - handleCancel: () => void + record: { [key: string]: any } | null, + visible: boolean, + handleConfirm: () => void, + handleCancel: () => void } -function FormComponent(props: Props) { - const { visible, record, handleConfirm, handleCancel } = props; - const [confirmLoading, setConfirmLoading] = useState(false); +export default function FormComponent(props: Props) { + const {visible, record, handleConfirm, handleCancel} = props; + const [confirmLoading, setConfirmLoading] = useState(false); - const onOk = async () => { - try { - setConfirmLoading(true); - const values = await form.validate(); - if (record?.id) { - await updateMenu(values); - } else { - await createMenu(values); - } - Message.success(`${record?.id ? '更新' : '新增'}成功!`); - handleConfirm(); - } catch (e) { - console.log(e); - } finally { - setConfirmLoading(false); - } - }; + const onOk = async () => { + try { + setConfirmLoading(true); + const values = await form.validate(); + if (record?.id) { + await updateMenu(values); + } else { + await createMenu(values); + } + Message.success(`${record?.id ? '更新' : '新增'}成功!`); + handleConfirm(); + } catch (e) { + console.log(e); + } finally { + setConfirmLoading(false); + } + }; - const [form] = Form.useForm(); - const [menuList, setMenuList] = useState([]); + const [form] = Form.useForm(); + const [menuList, setMenuList] = useState([]); - const fetchMenuList = async () => { - const { data } = await getMenu(); - const mergeMenu = [ - { - id: 0, - name: '无父级' - }, - ...data - ]; - setMenuList(mergeMenu); - }; + const fetchMenuList = async () => { + const {data} = await getMenu(); + const mergeMenu = [ + { + id: 0, + name: '无父级' + }, + ...data + ]; + setMenuList(mergeMenu); + }; - useEffect(() => { - record && form.setFieldsValue({ - ...record - }); - }, [record]); + useEffect(() => { + record && form.setFieldsValue({ + ...record + }); + }, [record]); - useEffect(() => { - if (visible) { - fetchMenuList(); - } else { - form.resetFields(); - } - }, [visible]); + useEffect(() => { + if (visible) { + fetchMenuList(); + } else { + form.resetFields(); + } + }, [visible]); - return ( -
- {`${record?.id ? '更新' : '新增'}菜单信息`}} - visible={visible} - onOk={onOk} - onCancel={handleCancel} - confirmLoading={confirmLoading} - > -
- - - - - - - - - - - -
-
-
- ); + return ( +
+ {`${record?.id ? '更新' : '新增'}菜单信息`}} + visible={visible} + onOk={onOk} + onCancel={handleCancel} + confirmLoading={confirmLoading} + > +
+ + + + + + + + + + + + + +
+
+
+ ); } - -export default FormComponent; diff --git a/admin/src/pages/menu/index.tsx b/admin/src/pages/menu/index.tsx index 5d40848..5fdc552 100644 --- a/admin/src/pages/menu/index.tsx +++ b/admin/src/pages/menu/index.tsx @@ -35,6 +35,11 @@ function ConfigManage() { dataIndex: 'url', align: 'center', }, + { + title: '排序', + dataIndex: 'sort', + align: 'center', + }, { title: '操作', dataIndex: 'operations', diff --git a/admin/src/pages/user/constants.tsx b/admin/src/pages/user/constants.tsx new file mode 100644 index 0000000..cbf18ff --- /dev/null +++ b/admin/src/pages/user/constants.tsx @@ -0,0 +1,86 @@ +import React from 'react'; + +import { + Typography, + TableColumnProps, + Button, + Popconfirm, + Space, + Avatar, +} from '@arco-design/web-react'; +import {genderDict, getDictValue, tableStatusDict} from "@/enum/dict"; +import {dayjs} from "@arco-design/web-react/es/_util/dayjs"; + +const {Text} = Typography; + +export const enum TableOptions { + EDIT = 'edit', + DELETE = 'delete', + ASSIGN_ROLE = 'assign_role', +} + +export const reactQueryKey = 'user' + +export function getColumns(callback: (type: TableOptions, record: any) => Promise): TableColumnProps[] { + return [ + { + title: 'id', + dataIndex: 'id', + render: (value) => {value}, + }, + { + title: '头像', + dataIndex: 'avatar', + render: (value) => ( + {value ? avatar : 未上传} + ), + }, + { + title: '账号', + dataIndex: 'account', + render: (value) => {value}, + }, + { + title: '昵称', + dataIndex: 'name', + }, + { + title: '性别', + dataIndex: 'gender', + render: (value) => {getDictValue(genderDict, value)} + }, + { + title: '创建时间', + dataIndex: 'createdAt', + width: '15%', + render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: '状态', + dataIndex: 'status', + width: '10%', + render: (x) => getDictValue(tableStatusDict, x) + }, + { + title: '操作', + dataIndex: 'operations', + align: 'center', + width: '300px', + render: (_, record) => ( + + + + callback(TableOptions.DELETE, record)} + > + + + + ) + } + ] +} diff --git a/admin/src/pages/user/form.tsx b/admin/src/pages/user/form.tsx index d70fe1b..2d34030 100644 --- a/admin/src/pages/user/form.tsx +++ b/admin/src/pages/user/form.tsx @@ -1,94 +1,94 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, Form, Input, Select, Message } from '@arco-design/web-react'; +import React, {useEffect, useState} from 'react'; +import {Modal, Form, Input, Select, Message} from '@arco-design/web-react'; import UploadFile from '@/components/Upload'; -import { createUser, updateUser } from '@/api/user'; +import {createUser, updateUser} from '@/api/user'; +import {genderDict, statusDict} from "@/enum/dict"; interface Props { - record: { [key: string]: any } | null, - visible: boolean, - handleConfirm: () => void, - handleCancel: () => void + record: { [key: string]: any } | null, + visible: boolean, + handleConfirm: () => void, + handleCancel: () => void } function FormComponent(props: Props) { - const { visible, record, handleConfirm, handleCancel } = props; - const [confirmLoading, setConfirmLoading] = useState(false); + const {visible, record, handleConfirm, handleCancel} = props; + const [confirmLoading, setConfirmLoading] = useState(false); - const onOk = async () => { - try { - setConfirmLoading(true); - const values = await form.validate(); - const data = { - ...values, - avatarId: values.avatar?.[0]?.response ?? null - }; - if (record?.id) { - await updateUser(data); - } else { - await createUser(data); - } - Message.success(`${record?.id ? '更新' : '新增'}成功!`); - handleConfirm(); - } catch (e) { - console.log(e); - } finally { - setConfirmLoading(false); - } - }; + const onOk = async () => { + try { + setConfirmLoading(true); + const values = await form.validate(); + const data = { + ...values, + avatarId: values.avatar?.[0]?.response ?? null + }; + if (record?.id) { + await updateUser(data); + } else { + await createUser(data); + } + Message.success(`${record?.id ? '更新' : '新增'}成功!`); + handleConfirm(); + } catch (e) { + console.log(e); + } finally { + setConfirmLoading(false); + } + }; - const [form] = Form.useForm(); + const [form] = Form.useForm(); - useEffect(() => { - record && form.setFieldsValue({ - ...record, - avatar: record.avatar ? [{ - uid: record.avatar.id, - response: record.avatar.id, - url: record.avatar.path - }] : undefined - }); - }, [record]); + useEffect(() => { + record && form.setFieldsValue({ + ...record, + avatar: record.avatar ? [{ + uid: record.avatar.id, + response: record.avatar.id, + url: record.avatar.path + }] : undefined + }); + }, [record]); - useEffect(() => { - !visible && form.resetFields() - }, [visible]); + useEffect(() => { + !visible && form.resetFields() + }, [visible]); - return ( - {`${record?.id ? '更新' : '新增'}用户信息`}} - visible={visible} - onOk={onOk} - onCancel={handleCancel} - confirmLoading={confirmLoading} - > -
- - - - - - - - - - - {!record?.id && - + return ( + {`${record?.id ? '更新' : '新增'}数据`}} + visible={visible} + onOk={onOk} + onCancel={handleCancel} + confirmLoading={confirmLoading} + > + + + + + + + + + + + + {!record?.id && + } - - - - - - - ); + + + + +
+ ); } export default FormComponent; diff --git a/admin/src/pages/user/index.tsx b/admin/src/pages/user/index.tsx index 83093e2..c57ae28 100644 --- a/admin/src/pages/user/index.tsx +++ b/admin/src/pages/user/index.tsx @@ -1,208 +1,167 @@ -import React, { useState } from 'react'; +import React, {useEffect, useState} from 'react'; import { - Card, - Table, - Typography, - TableColumnProps, - Badge, - PaginationProps, - Button, - Popconfirm, - Divider, - Avatar, - Message + Button, + Card, + Message, + PaginationProps, + Table, + TableColumnProps, + Typography } from '@arco-design/web-react'; -import { useQuery } from 'react-query'; +import {useQuery} from 'react-query'; import Form from './form'; import GrantRolesForm from './grantRoles'; -import { GenderDict } from '@/enum'; -import { deleteUser, getUsers } from '@/api/user'; +import {deleteUser, getUsers} from '@/api/user'; +import SearchForm from "./searchForm"; +import {IconPlus} from '@arco-design/web-react/icon'; +import {getColumns, TableOptions, reactQueryKey} from './constants'; -const { Title, Text } = Typography; +const {Title} = Typography; function UserManage() { - const [visible, setVisible] = useState(false); - const [visibleAssignRoles, setVisibleAssignRoles] = useState(false); - const [record, setRecord] = useState(null); - const columns: TableColumnProps[] = [ - { - title: 'id', - dataIndex: 'id', - render: (value) => {value}, - align: 'center' - }, - { - title: '头像', - dataIndex: 'avatar', - align: 'center', - render: (value) => ( - {value ? avatar : 未上传} - ) - }, - { - title: '账号', - dataIndex: 'account', - align: 'center', - render: (value) => {value}, - ellipsis: true - }, - { - title: '昵称', - dataIndex: 'name', - align: 'center' - }, - { - title: '性别', - dataIndex: 'gender', - align: 'center', - render: (value) => {GenderDict[value]} - }, - { - title: '状态', - dataIndex: 'status', - align: 'center', - render: (x) => { - if (x === 0) { - return ; - } - return ; - } - }, - { - title: '操作', - dataIndex: 'operations', - width: 300, - render: (_, record) => ( - <> - - - - - onDelete(record)} - > - - - - ), - align: 'center' - } - ]; + const [visible, setVisible] = useState(false); + const [visibleAssignRoles, setVisibleAssignRoles] = useState(false); + const [record, setRecord] = useState(null); + const [pagination, setPagination] = useState({ + sizeCanChange: true, + showTotal: true, + pageSize: 10, + current: 1, + pageSizeChangeResetCurrent: true, + total: 0 + }); - const [pagination, setPagination] = useState({ - sizeCanChange: true, - showTotal: true, - pageSize: 10, - current: 1, - pageSizeChangeResetCurrent: true, - total: 0 - }); + const [searchQuery, setSearchQuery] = useState>({ + page: pagination.current, + pageSize: pagination.pageSize, + }); - const onDelete = async ({ id }) => { - await deleteUser(id); - Message.success('删除成功!'); - await refetch(); - }; + const showAssignRolesModal = (payload) => { + setRecord(payload); + setVisibleAssignRoles(true); + }; - const onChangeTable = async ({ current, pageSize }) => { - setPagination({ - ...pagination, - current, - pageSize - }); - }; + const showModal = (payload?) => { + payload && setRecord(payload); + setVisible(true); + }; - const fetchData = async ({ queryKey }) => { - const [, { page, pageSize }] = queryKey; - const { data } = await getUsers({ page, pageSize }); - setPagination(prevState => { - prevState.total = data.count; - return prevState; - }); - if (page > 1 && data.list.length === 0) { - setPagination({ - ...pagination, - current: page - 1 - }); - } - return data; - }; + const onDelete = async ({id}) => { + await deleteUser(id); + Message.success('删除成功!'); + await refetch(); + }; - const { - data, - isLoading, - isFetching, - refetch - } = useQuery( - ['user', { page: pagination.current, pageSize: pagination.pageSize }], - fetchData, - { - refetchOnWindowFocus: false, - keepPreviousData: true - }); + const tableCallback = async (type: TableOptions, record: any) => { + switch (type) { + case TableOptions.EDIT: + showModal(record) + break; + case TableOptions.DELETE: + await onDelete(record); + break + case TableOptions.ASSIGN_ROLE: + showAssignRolesModal(record); + break + } + } - const showAssignRolesModal = (payload) => { - setRecord(payload); - setVisibleAssignRoles(true); - }; + const columns: TableColumnProps[] = getColumns(tableCallback) - const showModal = (payload?) => { - payload && setRecord(payload); - setVisible(true); - }; + const handleSearch = async (params) => { + setSearchQuery(prev => ({...prev, page: 1, ...params})) + } - const handleConfirm = async () => { - setRecord(null); - setVisible(false); - setVisibleAssignRoles(false); - await refetch(); - }; + const onChangeTable = async ({current, pageSize}) => { + setSearchQuery(prev => ({...prev, page: current, pageSize})); + }; - const handleCancel = () => { - setRecord(null); - setVisible(false); - setVisibleAssignRoles(false); - }; + const fetchData = async ({queryKey}) => { + const [, params] = queryKey; + console.log(params) + const {data} = await getUsers(params); + setPagination(prev => ({ + ...prev, + total: data.count + })) + // 当前列表不存在数据回退1页 + if (params.page > 1 && data.list.length === 0) { + setSearchQuery({ + ...searchQuery, + page: params.page - 1 + }); + } + return data; + }; - return ( - <> - -
- - 用户管理 + const {data, isLoading, isFetching, refetch} = useQuery( + [reactQueryKey, { + page: pagination.current, + pageSize: pagination.pageSize, + ...searchQuery + }], + fetchData); - + const handleConfirm = async () => { + setRecord(null); + setVisible(false); + setVisibleAssignRoles(false); + await refetch(); + }; - - - - ); + const handleCancel = () => { + setRecord(null); + setVisible(false); + setVisibleAssignRoles(false); + }; + + useEffect(() => { + setPagination(prev => ({ + ...prev, + current: searchQuery.page, + pageSize: pagination.pageSize, + })) + }, [searchQuery]) + + return ( + <> + + + + 用户管理 + + + + + +
+ + + ); } diff --git a/admin/src/pages/user/searchForm.tsx b/admin/src/pages/user/searchForm.tsx new file mode 100644 index 0000000..572b803 --- /dev/null +++ b/admin/src/pages/user/searchForm.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { + Form, + Input, + Button, + Grid, + Space, Divider, Select +} from '@arco-design/web-react'; +import {IconRefresh, IconSearch} from '@arco-design/web-react/icon'; +import {statusDict} from "@/enum/dict"; + +const {Row, Col} = Grid; +const {useForm} = Form; + +function SearchForm(props: { + onSearch: (values: Record) => void; +}) { + const [form] = useForm(); + + const handleSubmit = () => { + const values = form.getFieldsValue(); + props.onSearch(values); + }; + + const handleReset = () => { + form.resetFields(); + props.onSearch({}); + }; + + const colSpan = 6 + + return ( + <> + + + + + + + + + + + + + + +