Compare commits
13 Commits
main
...
83f3120fdc
| Author | SHA1 | Date | |
|---|---|---|---|
| 83f3120fdc | |||
|
9b642c85d0
|
|||
| d5074a92a2 | |||
| d5109e1333 | |||
| 0d3b0bd28d | |||
| a00488be5d | |||
| 29ce5e45f1 | |||
| d3723ba485 | |||
| 944d176240 | |||
| 1ae17823ef | |||
| b0ccd8f832 | |||
| 3b0bd56001 | |||
| 8aaad677b6 |
@@ -10,3 +10,5 @@ VITE_MSAL_TENANT_ID=msal-tenant-id
|
||||
# - US: United States
|
||||
# - DE: Germany
|
||||
VITE_DEFAULT_REGION_ABBREVIATION=GB
|
||||
# Application title
|
||||
VITE_APP_TITLE='OnixByte Technology Co., Ltd'
|
||||
|
||||
+17
-17
@@ -10,33 +10,33 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^2.0.1",
|
||||
"@ant-design/cssinjs": "^2.0.3",
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@azure/msal-browser": "^4.27.0",
|
||||
"@azure/msal-react": "^3.0.23",
|
||||
"@reduxjs/toolkit": "^2.11.1",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"antd": "^6.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"@azure/msal-browser": "^4.28.1",
|
||||
"@azure/msal-react": "^3.0.25",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"antd": "^6.2.2",
|
||||
"axios": "^1.13.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"libphonenumber-js": "^1.12.31",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"libphonenumber-js": "^1.12.35",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.10.1",
|
||||
"react-router-dom": "^7.10.1",
|
||||
"react-router": "^7.13.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"tailwindcss": "^4.1.17"
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.26",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^20.19.30",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"globals": "^16.5.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.7",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+880
-853
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import webClient from "@/client/web-client"
|
||||
import { HttpStatus } from "@/constant"
|
||||
import type { CaptchaResponse, UserAuthResponse } from "@/types/web/response"
|
||||
import type { CaptchaResponse, MfaAuthResponse, UserAuthResponse } from "@/types/web/response"
|
||||
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
||||
|
||||
/**
|
||||
@@ -21,8 +21,8 @@ async function getCaptcha(): Promise<CaptchaResponse | null> {
|
||||
*/
|
||||
async function usernamePasswordLogin(
|
||||
request: UsernamePasswordLoginRequest
|
||||
): Promise<UserAuthResponse | null> {
|
||||
const { data } = await webClient.post<UserAuthResponse>("/auth/login", request)
|
||||
): Promise<UserAuthResponse | MfaAuthResponse | null> {
|
||||
const { data } = await webClient.post<UserAuthResponse | MfaAuthResponse>("/auth/login", request)
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import webClient from "@/client/web-client"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import type { Department } from "@/types/entity"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import webClient from "@/client/web-client"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import type { MenuItem } from "@/types/entity"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import webClient from "@/client/web-client"
|
||||
import type { QueryPositionRequest } from "@/types/web/request"
|
||||
import type { PageResponse } from "@/types/web/response"
|
||||
import type { Position } from "@/types/entity"
|
||||
|
||||
+14
-1
@@ -1,6 +1,7 @@
|
||||
import type { QueryRoleRequest } from "@/types/web/request"
|
||||
import webClient from "@/service/web-client"
|
||||
import webClient from "@/client/web-client"
|
||||
import type { PageResponse, RoleResponse } from "@/types/web/response"
|
||||
import type { RoleFormValues } from "@/components/role-display-form"
|
||||
|
||||
export async function fetchRoles(
|
||||
request: QueryRoleRequest | null
|
||||
@@ -24,3 +25,15 @@ export async function fetchRoles(
|
||||
const { data } = await webClient.get<RoleResponse>(`/roles?${params.toString()}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function addRole(request: RoleFormValues) {
|
||||
return await webClient.post("/roles", request)
|
||||
}
|
||||
|
||||
export async function editRole(request: RoleFormValues) {
|
||||
return await webClient.put("/roles", request)
|
||||
}
|
||||
|
||||
export async function deleteRole(id: number | string) {
|
||||
return await webClient.delete(`/roles/${id}`)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import webClient from "@/client/web-client"
|
||||
import { getCountryCallingCode } from "libphonenumber-js"
|
||||
import { getDefaultCountryCode } from "@/utils/phone-number-utils"
|
||||
import type { AddUserRequest, EditUserRequest, QueryUserRequest } from "@/types/web/request"
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import axios, { type AxiosError } from "axios"
|
||||
import dayjs from "dayjs"
|
||||
import store from "@/store"
|
||||
import type { GeneralErrorResponse } from "@/types"
|
||||
import { HttpStatus } from "@/constant"
|
||||
import { logout } from "@/store/auth-slice"
|
||||
import type { GeneralErrorResponse } from "@/types/web/response"
|
||||
|
||||
const webClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(),
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
})
|
||||
|
||||
webClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const state = store.getState()
|
||||
if (state.auth.isAuthenticated) {
|
||||
config.headers["Authorization"] = `Bearer ${state.auth.token!}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error: unknown) => {
|
||||
@@ -25,14 +22,17 @@ webClient.interceptors.request.use(
|
||||
}
|
||||
)
|
||||
|
||||
webClient.interceptors.response.use((response) => {
|
||||
webClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
}, (error: unknown) => {
|
||||
},
|
||||
(error: unknown) => {
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
if (err.response?.status == HttpStatus.UNAUTHORIZED) {
|
||||
store.dispatch(logout())
|
||||
}
|
||||
return Promise.reject(error as AxiosError)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default webClient
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FormInstance } from "antd"
|
||||
import RoleDisplayForm, { type RoleFormValues } from "@/components/role-display-form"
|
||||
|
||||
export interface AddRoleDialogueProps {
|
||||
form: FormInstance<RoleFormValues>
|
||||
}
|
||||
|
||||
export default function AddRoleDialogue({ form }: AddRoleDialogueProps) {
|
||||
return (
|
||||
<RoleDisplayForm
|
||||
form={form}
|
||||
initialValues={{
|
||||
id: null,
|
||||
name: "",
|
||||
code: "",
|
||||
sort: 0,
|
||||
description: null,
|
||||
defaultValue: false,
|
||||
status: "ACTIVE",
|
||||
}}
|
||||
mode="add"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { FormInstance } from "antd"
|
||||
import RoleDisplayForm, { type RoleFormValues } from "@/components/role-display-form"
|
||||
import type { Role } from "@/types/entity"
|
||||
|
||||
export interface EditRoleDialogueProps {
|
||||
form: FormInstance<RoleFormValues>
|
||||
initialValues: RoleFormValues
|
||||
}
|
||||
|
||||
export default function EditRoleDialogue({ form, initialValues }: EditRoleDialogueProps) {
|
||||
return <RoleDisplayForm form={form} initialValues={initialValues} mode="edit" />
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { App, Form, type FormInstance, Input, InputNumber, Select, Switch } from "antd"
|
||||
import { type Status, StatusOptions } from "@/types/constant"
|
||||
import type { FormMode } from "@/types/form"
|
||||
import { useEffect, useMemo } from "react"
|
||||
|
||||
/**
|
||||
* Role form values.
|
||||
*/
|
||||
export interface RoleFormValues {
|
||||
id: number | string | null
|
||||
name: string
|
||||
code: string
|
||||
sort: number
|
||||
defaultValue: boolean
|
||||
description: string | null
|
||||
status: Status
|
||||
}
|
||||
|
||||
/**
|
||||
* Component props.
|
||||
*/
|
||||
export interface RoleDisplayFormProps {
|
||||
initialValues?: RoleFormValues
|
||||
form: FormInstance<RoleFormValues>
|
||||
mode: FormMode
|
||||
}
|
||||
|
||||
export default function RoleDisplayForm({ initialValues, form, mode }: RoleDisplayFormProps) {
|
||||
const isEditing = useMemo<boolean>(() => mode == "edit", [mode])
|
||||
|
||||
// Initialise form values
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
form.setFieldsValue(initialValues)
|
||||
} else {
|
||||
form.resetFields()
|
||||
}
|
||||
}, [initialValues, form])
|
||||
|
||||
return (
|
||||
<Form<RoleFormValues>
|
||||
form={form}
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
labelAlign="right"
|
||||
validateTrigger="onBlur">
|
||||
<Form.Item<RoleFormValues> label="角色编号" hidden={!isEditing} name="id">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item<RoleFormValues>
|
||||
label="角色名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "角色名称不能为空" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item<RoleFormValues>
|
||||
label="角色编码"
|
||||
name="code"
|
||||
rules={[
|
||||
{ required: true, message: "角色编码不能为空" },
|
||||
{ pattern: /^[a-z-]+$/, message: "角色编码格式错误,仅支持小写英文字母及 '-'" },
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item<RoleFormValues>
|
||||
label="排序编码"
|
||||
name="sort"
|
||||
rules={[
|
||||
{ required: true, message: "排序不能为空" },
|
||||
{ type: "number", min: 0, max: Number.MAX_VALUE, message: "排序必须是正数" },
|
||||
]}>
|
||||
<InputNumber type="number" />
|
||||
</Form.Item>
|
||||
<Form.Item<RoleFormValues> label="是否为默认角色" name="defaultValue">
|
||||
<Switch disabled={isEditing} />
|
||||
</Form.Item>
|
||||
<Form.Item<RoleFormValues> label="角色状态" name="status">
|
||||
<Select<Status> options={StatusOptions} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router"
|
||||
import { Avatar, Breadcrumb, Dropdown, Layout, Menu, type MenuProps, Modal, Space } from "antd"
|
||||
import { type ReactNode, useEffect, useMemo, useState } from "react"
|
||||
import { Link, Navigate, useNavigate } from "react-router"
|
||||
import { App, Avatar, Breadcrumb, Dropdown, Layout, Menu, type MenuProps, Space } from "antd"
|
||||
import { DownOutlined } from "@ant-design/icons"
|
||||
import { ApplicationLogo } from "@/components/icon"
|
||||
import { useAppDispatch, useAppSelector } from "@/store"
|
||||
import { useAntBreadcrumbs } from "@/hooks"
|
||||
import { logout } from "@/store/auth-slice"
|
||||
import { MenuApi } from "@/api"
|
||||
import type { AxiosError } from "axios"
|
||||
import axios from "axios"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import type { MenuItem } from "@/types/entity"
|
||||
import { AppUtils } from "@/utils"
|
||||
import type { GeneralErrorResponse } from "@/types/web/response"
|
||||
|
||||
const { Header, Footer, Sider, Content } = Layout
|
||||
type AntMenuItem = Required<MenuProps>["items"][number]
|
||||
|
||||
export interface DashboardLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function transformMenuData(nodes: TreeNode<MenuItem>[]): AntMenuItem[] {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return []
|
||||
@@ -25,21 +30,33 @@ function transformMenuData(nodes: TreeNode<MenuItem>[]): AntMenuItem[] {
|
||||
const { item, children } = node
|
||||
const hasChildren = children && children.length > 0
|
||||
|
||||
if (item.isVisible) {
|
||||
const menuItem: AntMenuItem = {
|
||||
key: item.code,
|
||||
label: item.name,
|
||||
}
|
||||
|
||||
if (item.path) {
|
||||
if (item.isExternalLink) {
|
||||
menuItem.extra = <a href={item.path} target="_blank" rel="noopener noreferrer" />
|
||||
} else {
|
||||
menuItem.extra = <Link to={item.path} />
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
// Append children
|
||||
return { ...menuItem, children: transformMenuData(children) }
|
||||
}
|
||||
|
||||
return menuItem
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const { modal, message } = App.useApp()
|
||||
const user = useAppSelector((store) => store.auth.user!)
|
||||
const dispatch = useAppDispatch()
|
||||
const breadcrumbItems = useAntBreadcrumbs()
|
||||
@@ -47,7 +64,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
|
||||
const onLogout = ({ key }: { key: string }) => {
|
||||
if (key == "logout") {
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: "确定要注销吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
@@ -69,8 +86,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
setMenuItems(transformMenuData(response))
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const err = error as AxiosError
|
||||
console.log(err)
|
||||
console.log(error)
|
||||
const errorMessage =
|
||||
axios.isAxiosError<GeneralErrorResponse>(error) && error.response?.data.message
|
||||
? error.response?.data.message
|
||||
: "无法读取菜单数据"
|
||||
void message.error({
|
||||
content: errorMessage,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -82,12 +105,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
},
|
||||
]
|
||||
|
||||
const appTitle = useMemo<string>(() => AppUtils.getAppTitle(), [])
|
||||
|
||||
return (
|
||||
<Layout className="h-[100%]">
|
||||
<Header className="flex items-center justify-between bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<Layout className="h-full">
|
||||
<Layout.Header className="flex items-center justify-between bg-linear-to-br from-blue-50 to-indigo-100">
|
||||
<div className="flex gap-4 items-center">
|
||||
<ApplicationLogo className="text-4xl" />
|
||||
<span className="text-xl">Onixbyte Hi-Tech Co., Ltd</span>
|
||||
<span className="text-xl">{appTitle}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 items-center">
|
||||
@@ -101,32 +126,18 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
</Dropdown>
|
||||
<Avatar src={user.avatarUrl} alt="用户头像" />
|
||||
</div>
|
||||
</Header>
|
||||
</Layout.Header>
|
||||
<Layout>
|
||||
<Sider width={200} className="bg-white">
|
||||
<Layout.Sider width={200} className="bg-white">
|
||||
<Menu
|
||||
mode="inline"
|
||||
className="h-full max-h-full border-e-0"
|
||||
items={menuItems}
|
||||
onSelect={({ key }) => {
|
||||
console.log(`key = ${key}`)
|
||||
switch (key) {
|
||||
case "user-mgmt":
|
||||
void navigate("/users")
|
||||
break
|
||||
case "role-mgmt":
|
||||
void navigate("/roles")
|
||||
break
|
||||
case "menu-mgmt":
|
||||
void navigate("/menus")
|
||||
break
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Sider>
|
||||
</Layout.Sider>
|
||||
<Layout className="pt-0 px-6 pb-6">
|
||||
<Breadcrumb items={breadcrumbItems} className="my-4 mx-0" />
|
||||
<Content className="p-6 m-0 min-h-70 bg-white">{children}</Content>
|
||||
<Layout.Content className="p-6 m-0 min-h-70 bg-white">{children}</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
+19
-19
@@ -1,9 +1,10 @@
|
||||
import { type MouseEvent, useCallback, useEffect, useState } from "react"
|
||||
import { Link, useNavigate } from "react-router"
|
||||
import { Form, Input, Button, Card, message, Divider } from "antd"
|
||||
import { Form, Input, Button, Card, message, Divider, App } from "antd"
|
||||
import dayjs from "dayjs"
|
||||
import type { AxiosError } from "axios"
|
||||
// import { useMsal } from "@azure/msal-react"
|
||||
import { GithubFilled } from "@ant-design/icons"
|
||||
import { useMsal } from "@azure/msal-react"
|
||||
import { AuthApi } from "@/api"
|
||||
import { useAppDispatch, useAppSelector } from "@/store"
|
||||
import { loginSuccess, updateRegistrationEnabled } from "@/store/auth-slice"
|
||||
@@ -18,8 +19,6 @@ import {
|
||||
DiscordFilled,
|
||||
EmailFilled,
|
||||
} from "@/components/icon"
|
||||
|
||||
import { GithubFilled } from "@ant-design/icons"
|
||||
import { fetchRegisterEnabled } from "@/api/auth"
|
||||
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
||||
import type { CaptchaResponse, GeneralErrorResponse } from "@/types/web/response"
|
||||
@@ -29,9 +28,7 @@ export default function LoginPage() {
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// const msalContext = useMsal()
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm<UsernamePasswordLoginRequest>()
|
||||
|
||||
// const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
@@ -77,28 +74,33 @@ export default function LoginPage() {
|
||||
console.log("Login values:", values)
|
||||
|
||||
const loginResponse = await AuthApi.usernamePasswordLogin(values)
|
||||
if (loginResponse) {
|
||||
|
||||
if (loginResponse == null) {
|
||||
message.error("登录失败:服务器响应异常。")
|
||||
return
|
||||
}
|
||||
|
||||
if ('mfaToken' in loginResponse) {
|
||||
|
||||
} else {
|
||||
dispatch(
|
||||
loginSuccess({
|
||||
user: loginResponse.user,
|
||||
token: loginResponse.accessToken,
|
||||
user: loginResponse
|
||||
})
|
||||
)
|
||||
messageApi.success("登录成功", dayjs.duration({ seconds: 3 }).asSeconds())
|
||||
message.success("登录成功", dayjs.duration({ seconds: 3 }).asSeconds())
|
||||
await navigate("/")
|
||||
} else {
|
||||
messageApi.error("登录失败:服务器响应异常。")
|
||||
}
|
||||
} catch (errorInfo: unknown) {
|
||||
const error = errorInfo as AxiosError<GeneralErrorResponse>
|
||||
console.log(error)
|
||||
messageApi.error(
|
||||
message.error(
|
||||
error.response?.data.message ?? "登录失败,请稍后再试",
|
||||
dayjs.duration({ seconds: 3 }).asSeconds()
|
||||
)
|
||||
}
|
||||
},
|
||||
[dispatch, navigate, messageApi]
|
||||
[dispatch, navigate]
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -190,9 +192,7 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
||||
{contextHolder}
|
||||
|
||||
<div className="min-h-screen bg-linear-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
||||
{/* 背景装饰元素 */}
|
||||
<div className="absolute top-0 left-0 w-72 h-72 bg-blue-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||
@@ -275,7 +275,7 @@ export default function LoginPage() {
|
||||
htmlType="submit"
|
||||
block
|
||||
size="large"
|
||||
className="h-12 rounded-lg font-semibold text-base bg-gradient-to-r from-blue-500 to-purple-600 border-0 hover:from-blue-600 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl">
|
||||
className="h-12 rounded-lg font-semibold text-base bg-linear-to-r from-blue-500 to-purple-600 border-0 hover:from-blue-600 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl">
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
+139
-9
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import type { AxiosError } from "axios"
|
||||
import axios, { type AxiosError } from "axios"
|
||||
import { App, Button, Form, Input, Select, Space, Switch, Table } from "antd"
|
||||
import type { Role } from "@/types/entity"
|
||||
import { RoleApi } from "@/api"
|
||||
import type { QueryRoleRequest } from "@/types/web/request"
|
||||
import type { GeneralErrorResponse, RoleResponse } from "@/types/web/response"
|
||||
import type { GeneralErrorResponse } from "@/types/web/response"
|
||||
import {
|
||||
DeleteOutlined,
|
||||
ExportOutlined,
|
||||
@@ -15,15 +15,22 @@ import {
|
||||
} from "@ant-design/icons"
|
||||
import type { QueryRoleForm } from "@/types/form"
|
||||
import type { Status } from "@/types/constant"
|
||||
import AddRoleDialogue from "@/components/add-role-dialogue"
|
||||
import type { RoleFormValues } from "@/components/role-display-form"
|
||||
import EditRoleDialogue from "@/components/edit-role-dialogue"
|
||||
|
||||
export default function RolePage() {
|
||||
const { message } = App.useApp()
|
||||
const { message, modal } = App.useApp()
|
||||
|
||||
const [queryForm] = Form.useForm<QueryRoleForm>()
|
||||
const [addRoleForm] = Form.useForm<RoleFormValues>()
|
||||
const [editRoleForm] = Form.useForm<RoleFormValues>()
|
||||
const [roles, setRoles] = useState<Role[]>([])
|
||||
const [pageNum, setPageNum] = useState<number>(1)
|
||||
const [pageSize, setPageSize] = useState<number>(10)
|
||||
const [totalElementCount, setTotalElementCount] = useState<number>(0)
|
||||
const [isLastPage, setIsLastPage] = useState<boolean>(false)
|
||||
const [isFirstPage, setIsFirstPage] = useState<boolean>(false)
|
||||
|
||||
const queryRoles = (pageNum: number, pageSize: number, queryRoleForm: QueryRoleForm | null) => {
|
||||
const queryRoleRequest: QueryRoleRequest = {
|
||||
@@ -36,11 +43,12 @@ export default function RolePage() {
|
||||
|
||||
RoleApi.fetchRoles(queryRoleRequest)
|
||||
.then((response) => {
|
||||
console.log("role response", response)
|
||||
setPageNum(response.pageable.pageNumber + 1)
|
||||
setPageSize(response.pageable.pageSize)
|
||||
setTotalElementCount(response.totalElements)
|
||||
setRoles(response.content.sort((role1, role2) => role1.sort - role2.sort))
|
||||
setIsLastPage(response.last)
|
||||
setIsFirstPage(response.first)
|
||||
})
|
||||
.catch((error) => {
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
@@ -55,6 +63,126 @@ export default function RolePage() {
|
||||
)
|
||||
}
|
||||
|
||||
const onAddRoleFinish = async () => {
|
||||
try {
|
||||
const values = await addRoleForm.validateFields()
|
||||
console.log(values)
|
||||
await RoleApi.addRole(values)
|
||||
void message.success(`角色 ${values.name} 创建成功`)
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError<GeneralErrorResponse>(error)) {
|
||||
// 处理 Axios 请求错误
|
||||
const serverMsg = error.response?.data?.message
|
||||
void message.error(serverMsg ?? "服务异常,请稍后再试")
|
||||
} else {
|
||||
// 处理其他预期外的错误(如代码逻辑 Bug)
|
||||
console.error("未知错误:", error)
|
||||
void message.error("发生系统错误,请联系管理员")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddRole = () => {
|
||||
modal
|
||||
.confirm({
|
||||
title: "添加用户",
|
||||
content: <AddRoleDialogue form={addRoleForm} />,
|
||||
width: 600,
|
||||
onOk: onAddRoleFinish,
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
const formValues = queryForm.getFieldsValue()
|
||||
queryRoles(pageNum, pageSize, formValues)
|
||||
},
|
||||
() => {
|
||||
console.error("用户取消添加角色")
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
addRoleForm.resetFields()
|
||||
})
|
||||
}
|
||||
|
||||
const onEditRoleFinish = async () => {
|
||||
try {
|
||||
const values = await editRoleForm.validateFields()
|
||||
console.log(values)
|
||||
await RoleApi.editRole(values)
|
||||
void message.success(`角色 ${values.name} 更新成功`)
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError<GeneralErrorResponse>(error)) {
|
||||
const serverMsg = error.response?.data?.message
|
||||
void message.error(serverMsg ?? "后端服务异常,请稍后再试")
|
||||
} else {
|
||||
console.error("未知错误:", error)
|
||||
void message.error("发生系统错误,请联系管理员")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditRole = (role: Role) => {
|
||||
modal
|
||||
.confirm({
|
||||
title: "修改用户",
|
||||
content: <EditRoleDialogue form={editRoleForm} initialValues={role} />,
|
||||
width: 600,
|
||||
onOk: onEditRoleFinish,
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
const formValues = queryForm.getFieldsValue()
|
||||
queryRoles(pageNum, pageSize, formValues)
|
||||
},
|
||||
() => {
|
||||
console.error("用户取消添加角色")
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
editRoleForm.resetFields()
|
||||
})
|
||||
}
|
||||
|
||||
const onDeleteRoleFinish = async (role: Role) => {
|
||||
try {
|
||||
await RoleApi.deleteRole(role.id)
|
||||
void message.info(`角色 ${role.name} 删除成功`)
|
||||
|
||||
// Refresh roles
|
||||
if (roles.length == 1 && isLastPage && !isFirstPage) {
|
||||
setPageNum(pageNum - 1)
|
||||
} else {
|
||||
// Execute refresh manually
|
||||
const searchParams = queryForm.getFieldsValue()
|
||||
queryRoles(pageNum, pageSize, searchParams)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError<GeneralErrorResponse>(error)) {
|
||||
const serverMsg = error.response?.data?.message
|
||||
void message.error(serverMsg ?? "服务异常,请稍后再试")
|
||||
} else {
|
||||
console.error("未知错误:", error)
|
||||
void message.error("发生系统错误,请联系管理员")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteRole = async (role: Role) => {
|
||||
modal
|
||||
.confirm({
|
||||
title: "删除角色确认",
|
||||
content: <>删除角色会同步解除该角色与用户的绑定关系,确定要删除吗?</>,
|
||||
onOk: () => onDeleteRoleFinish(role),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
queryRoles(pageNum, pageSize, null)
|
||||
}, [pageNum, pageSize])
|
||||
@@ -94,19 +222,19 @@ export default function RolePage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<QueryRoleForm>>
|
||||
<Space.Compact>
|
||||
<Space>
|
||||
<Button color="primary" variant="solid" htmlType="submit" icon={<SearchOutlined />}>
|
||||
查询
|
||||
</Button>
|
||||
<Button color="orange" variant="solid" htmlType="reset" icon={<UndoOutlined />}>
|
||||
重置
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Space size={8}>
|
||||
<Button variant="solid" type="primary" icon={<PlusOutlined />} onClick={() => {}}>
|
||||
<Button variant="solid" type="primary" icon={<PlusOutlined />} onClick={handleAddRole}>
|
||||
新增
|
||||
</Button>
|
||||
<Button variant="solid" danger icon={<DeleteOutlined />}>
|
||||
@@ -153,8 +281,10 @@ export default function RolePage() {
|
||||
render: (role: Role) => (
|
||||
<>
|
||||
<Space.Compact>
|
||||
<Button variant="solid">修改</Button>
|
||||
<Button variant="solid" danger>
|
||||
<Button variant="solid" onClick={() => handleEditRole(role)}>
|
||||
修改
|
||||
</Button>
|
||||
<Button variant="solid" danger onClick={() => handleDeleteRole(role)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
@@ -4,14 +4,12 @@ import type { User } from "@/types/entity"
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
user: User | null
|
||||
token: string | null,
|
||||
registrationEnabled: boolean
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
registrationEnabled: false
|
||||
}
|
||||
|
||||
@@ -23,18 +21,15 @@ const authSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
user: User
|
||||
token: string
|
||||
}>
|
||||
) {
|
||||
console.log("更新用户信息:", action.payload.user)
|
||||
state.isAuthenticated = true
|
||||
state.user = action.payload.user
|
||||
state.token = action.payload.token
|
||||
},
|
||||
logout(state) {
|
||||
state.isAuthenticated = false
|
||||
state.user = null
|
||||
state.token = null
|
||||
},
|
||||
updateRegistrationEnabled(state, action: PayloadAction<boolean>) {
|
||||
state.registrationEnabled = action.payload
|
||||
|
||||
@@ -12,3 +12,23 @@ export interface AntTreeSelectOption<T> {
|
||||
selectable?: boolean
|
||||
checkable?: boolean
|
||||
}
|
||||
|
||||
export interface AntSelectOptionItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type AntSelectOption = AntSelectOptionItem[]
|
||||
|
||||
export interface AntFieldError {
|
||||
errors: string[]
|
||||
name: string[]
|
||||
warnings: unknown[]
|
||||
}
|
||||
|
||||
export interface AntFormValidationError<T> {
|
||||
values: T
|
||||
message: string
|
||||
outOfDate: boolean
|
||||
errorFields: AntFieldError[]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import type { AntSelectOption } from "@/types/antd"
|
||||
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
export type Status = "ACTIVE" | "INACTIVE"
|
||||
|
||||
export type UserStatus = Status | "LOCKED"
|
||||
|
||||
export const StatusOptions: AntSelectOption = [
|
||||
{ label: "已启用", value: "ACTIVE" },
|
||||
{ label: "已停用", value: "INACTIVE" },
|
||||
]
|
||||
|
||||
export const UserStatusOptions: AntSelectOption = [
|
||||
...StatusOptions,
|
||||
{ label: "已禁用", value: "LOCKED" },
|
||||
]
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface MenuItem {
|
||||
parentId: number | null
|
||||
code: string
|
||||
sort: number
|
||||
path: string | null
|
||||
isExternalLink: boolean
|
||||
isVisible: boolean
|
||||
status: Status
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { CountryCode as RegionAbbreviation } from "libphonenumber-js"
|
||||
import type { Status } from "@/types/constant"
|
||||
import type { Dayjs } from "dayjs"
|
||||
|
||||
export type FormMode = "add" | "edit"
|
||||
|
||||
export interface UserFormValues extends Omit<
|
||||
User,
|
||||
"id" | "password" | "regionAbbreviation" | "departmentId" | "positionId" | "createdAt" | "updatedAt"
|
||||
|
||||
@@ -14,9 +14,11 @@ export interface PageResponse<T> {
|
||||
pageable: Pageable
|
||||
}
|
||||
|
||||
export interface UserAuthResponse {
|
||||
user: User
|
||||
accessToken: string
|
||||
export interface UserAuthResponse extends User {
|
||||
}
|
||||
|
||||
export interface MfaAuthResponse {
|
||||
mfaToken: string
|
||||
}
|
||||
|
||||
export interface UserDetailResponse extends User {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { AntFormValidationError } from "@/types/antd"
|
||||
|
||||
export function getFieldErrorMessage(error: AntFormValidationError<unknown>): string {
|
||||
return error.errorFields.map((errorField) => errorField.errors.join(",")).join(";")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Get application title.
|
||||
*/
|
||||
export function getAppTitle(): string {
|
||||
return import.meta.env.VITE_APP_TITLE ?? "Helix"
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export * as PhoneNumberUtils from "./phone-number-utils"
|
||||
export * as DepartmentUtils from "./department-utils"
|
||||
export * as AntUtils from "./ant-utils"
|
||||
export * as AppUtils from "./app-utils"
|
||||
|
||||
Vendored
+8
@@ -19,7 +19,15 @@ interface ImportMetaEnv {
|
||||
*/
|
||||
readonly VITE_MSAL_TENANT_ID: string
|
||||
|
||||
/**
|
||||
* Default region for new users
|
||||
*/
|
||||
readonly VITE_DEFAULT_REGION_ABBREVIATION: RegionAbbreviation
|
||||
|
||||
/**
|
||||
* Application title, can be set to any preferred string as your wish.
|
||||
*/
|
||||
readonly VITE_APP_TITLE: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
Reference in New Issue
Block a user