merge: import helix-web history into helix-web/
git-subtree-dir: helix-web git-subtree-mainline:21f14f8c05git-subtree-split:83f3120fdc
This commit is contained in:
commit
c732624580
82 files changed
+6483
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import React from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
/**
|
||||
* 携带错误信息的通用错误展示页
|
||||
*
|
||||
* @param message 自定义的错误信息,默认为`发生了未知错误,请稍后再试`
|
||||
* @constructor
|
||||
*/
|
||||
export default function ErrorPage({ message = "发生了未知错误,请稍后再试" }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-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>
|
||||
<div className="absolute bottom-0 left-1/2 w-72 h-72 bg-pink-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||
<div className="text-center bg-white p-10 lg:p-14 rounded-xl shadow-lg md:shadow-xl max-w-xl w-full">
|
||||
<h1 className="text-5xl md:text-6xl mb-5 leading-none">⚠️</h1>
|
||||
<h2 className="text-3xl md:text-4xl text-red-600 m-0 font-bold mb-4">
|
||||
系统出现了一些异常。
|
||||
</h2>
|
||||
<p className="text-lg text-gray-700 leading-relaxed mb-4">{message}</p>
|
||||
<p className="text-base text-gray-600 mb-8">我们已经知晓该问题并正在进行处理。</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-block bg-blue-600 text-white py-3 px-7 rounded-lg no-underline
|
||||
text-lg font-semibold transition-all duration-300 ease-in-out
|
||||
shadow-md shadow-blue-500/20 hover:bg-blue-700 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-blue-500/30">
|
||||
返回主页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useAppSelector } from "@/store"
|
||||
|
||||
export default function HomePage() {
|
||||
const user = useAppSelector((store) => store.auth.user!)
|
||||
|
||||
return <>Welcome to Helix, {user.fullName}</>
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
import { type MouseEvent, useCallback, useEffect, useState } from "react"
|
||||
import { Link, useNavigate } from "react-router"
|
||||
import { Form, Input, Button, Card, message, Divider, App } from "antd"
|
||||
import dayjs from "dayjs"
|
||||
import type { AxiosError } from "axios"
|
||||
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"
|
||||
import {
|
||||
DingTalkFilled,
|
||||
GoogleFilled,
|
||||
LarkFilled,
|
||||
MicrosoftFilled,
|
||||
SlackFilled,
|
||||
WeComFilled,
|
||||
GitlabFilled,
|
||||
DiscordFilled,
|
||||
EmailFilled,
|
||||
} from "@/components/icon"
|
||||
import { fetchRegisterEnabled } from "@/api/auth"
|
||||
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
||||
import type { CaptchaResponse, GeneralErrorResponse } from "@/types/web/response"
|
||||
|
||||
export default function LoginPage() {
|
||||
const registrationEnabled = useAppSelector((state) => state.auth.registrationEnabled)
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm<UsernamePasswordLoginRequest>()
|
||||
|
||||
// const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [hasCaptcha, setHasCaptcha] = useState<boolean>(false)
|
||||
const [captchaData, setCaptchaData] = useState<CaptchaResponse | null>()
|
||||
|
||||
const fetchCaptcha = useCallback(async () => {
|
||||
try {
|
||||
const response = await AuthApi.getCaptcha()
|
||||
if (response) {
|
||||
setHasCaptcha(true)
|
||||
setCaptchaData(response)
|
||||
form.setFieldValue("uuid", response.uuid)
|
||||
} else {
|
||||
setHasCaptcha(false)
|
||||
setCaptchaData(null)
|
||||
form.setFieldValue("uuid", null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch captcha due to an error:", error)
|
||||
setHasCaptcha(false)
|
||||
setCaptchaData(null)
|
||||
form.setFieldValue("uuid", null)
|
||||
}
|
||||
}, [form])
|
||||
|
||||
useEffect(() => {
|
||||
void fetchCaptcha()
|
||||
}, [fetchCaptcha])
|
||||
|
||||
useEffect(() => {
|
||||
void fetchRegisterEnabled().then((enabled) => {
|
||||
dispatch(updateRegistrationEnabled(enabled))
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
/**
|
||||
* 用户名密码登录
|
||||
*/
|
||||
const performLogin = useCallback(
|
||||
async (values: UsernamePasswordLoginRequest) => {
|
||||
try {
|
||||
console.log("Login values:", values)
|
||||
|
||||
const loginResponse = await AuthApi.usernamePasswordLogin(values)
|
||||
|
||||
if (loginResponse == null) {
|
||||
message.error("登录失败:服务器响应异常。")
|
||||
return
|
||||
}
|
||||
|
||||
if ('mfaToken' in loginResponse) {
|
||||
|
||||
} else {
|
||||
dispatch(
|
||||
loginSuccess({
|
||||
user: loginResponse
|
||||
})
|
||||
)
|
||||
message.success("登录成功", dayjs.duration({ seconds: 3 }).asSeconds())
|
||||
await navigate("/")
|
||||
}
|
||||
} catch (errorInfo: unknown) {
|
||||
const error = errorInfo as AxiosError<GeneralErrorResponse>
|
||||
console.log(error)
|
||||
message.error(
|
||||
error.response?.data.message ?? "登录失败,请稍后再试",
|
||||
dayjs.duration({ seconds: 3 }).asSeconds()
|
||||
)
|
||||
}
|
||||
},
|
||||
[dispatch, navigate]
|
||||
)
|
||||
|
||||
/**
|
||||
* 刷新验证码图片
|
||||
*/
|
||||
const refreshCaptcha = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
void fetchCaptcha()
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Microsoft Entra ID 登录
|
||||
*/
|
||||
const performMsalLogin = () => {
|
||||
console.log("使用 Microsoft 账号登录")
|
||||
// void doMsalLogin(msalContext.instance, dispatch, () => void navigate("/"))
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 DingTalk 登录
|
||||
*/
|
||||
const performDingTalkLogin = () => {
|
||||
console.log("使用钉钉登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 WeCom 登录
|
||||
*/
|
||||
const performWeComLogin = () => {
|
||||
console.log("使用企业微信登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Lark 登录
|
||||
*/
|
||||
const performLarkLogin = () => {
|
||||
console.log("使用飞书登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Slack 登录
|
||||
*/
|
||||
const performSlackLogin = () => {
|
||||
console.log("使用 Slack 登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Github 登录
|
||||
*/
|
||||
const performGithubLogin = () => {
|
||||
console.log("使用 GitHub 登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Google 登录
|
||||
*/
|
||||
const performGoogleLogin = () => {
|
||||
console.log("使用 Google 登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Gitlab 登录
|
||||
*/
|
||||
const performGitlabLogin = () => {
|
||||
console.log("使用 Gitlab 登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Discord 登录
|
||||
*/
|
||||
const performDiscordLogin = () => {
|
||||
console.log("使用 Discord 登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Email 登录
|
||||
*/
|
||||
const performEmailLogin = () => {
|
||||
console.log("使用 Email 登录")
|
||||
// todo implement this
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="absolute bottom-0 left-1/2 w-72 h-72 bg-pink-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||
|
||||
<Card
|
||||
title={
|
||||
<div className="text-center py-4">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">欢迎回来</h1>
|
||||
<p className="text-gray-600 text-sm">请登录您的账户</p>
|
||||
</div>
|
||||
}
|
||||
className="w-full max-w-md shadow-2xl border-0 backdrop-blur-sm bg-white/90 relative z-10"
|
||||
styles={{
|
||||
body: {
|
||||
padding: "32px",
|
||||
},
|
||||
}}>
|
||||
<Form<UsernamePasswordLoginRequest>
|
||||
name="usernamePasswordLoginForm"
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
void performLogin(values)
|
||||
}}
|
||||
layout="vertical"
|
||||
className="space-y-4">
|
||||
<Form.Item<UsernamePasswordLoginRequest>
|
||||
label={<span className="text-gray-700 font-medium">用户名</span>}
|
||||
name="username"
|
||||
rules={[{ required: true, message: "请输入用户名!" }]}>
|
||||
<Input
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
className="rounded-lg border-gray-300 hover:border-blue-400 focus:border-blue-500 transition-colors"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UsernamePasswordLoginRequest>
|
||||
label={<span className="text-gray-700 font-medium">密码</span>}
|
||||
name="password"
|
||||
rules={[{ required: true, message: "请输入密码!" }]}>
|
||||
<Input.Password
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
className="rounded-lg border-gray-300 hover:border-blue-400 focus:border-blue-500 transition-colors"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{hasCaptcha ? (
|
||||
<>
|
||||
<Form.Item<UsernamePasswordLoginRequest>
|
||||
label={<span className="text-gray-700 font-medium">验证码</span>}
|
||||
name="captcha"
|
||||
rules={[{ required: true, message: "请输入验证码!" }]}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
className="rounded-lg border-gray-300 hover:border-blue-400 focus:border-blue-500 transition-colors"
|
||||
/>
|
||||
{captchaData?.captcha && (
|
||||
<img
|
||||
src={captchaData.captcha}
|
||||
alt="验证码"
|
||||
onClick={refreshCaptcha}
|
||||
className="cursor-pointer h-10 border border-gray-300 rounded-lg transition-transform hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item<UsernamePasswordLoginRequest> name="uuid" hidden>
|
||||
<div>Placeholder</div>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Form.Item className="mb-0">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
size="large"
|
||||
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>
|
||||
</Form>
|
||||
|
||||
{registrationEnabled ? (
|
||||
<div className="mt-4 flex justify-end">
|
||||
还没有账号?<Link to="/register">立即注册</Link>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Divider className="my-6 text-gray-400">
|
||||
<span className="text-gray-500 text-sm font-medium">第三方帐号登录</span>
|
||||
</Divider>
|
||||
|
||||
<div className="grid grid-cols-5 gap-4 justify-items-center w-full">
|
||||
<Button
|
||||
icon={<DingTalkFilled className="text-blue-600" />}
|
||||
size="large"
|
||||
onClick={performDingTalkLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-blue-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="钉钉登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<DiscordFilled className="text-indigo-600" />}
|
||||
size="large"
|
||||
onClick={performDiscordLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-indigo-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="Discord登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<EmailFilled className="text-red-600" />}
|
||||
size="large"
|
||||
onClick={performEmailLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-red-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="邮箱登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<GithubFilled className="text-gray-800" />}
|
||||
size="large"
|
||||
onClick={performGithubLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-gray-400 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="GitHub登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<GitlabFilled className="text-orange-600" />}
|
||||
size="large"
|
||||
onClick={performGitlabLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-orange-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="GitLab登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<GoogleFilled className="text-red-500" />}
|
||||
size="large"
|
||||
onClick={performGoogleLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-red-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="Google登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<LarkFilled className="text-blue-700" />}
|
||||
size="large"
|
||||
onClick={performLarkLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-blue-400 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="飞书登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<MicrosoftFilled className="text-blue-600" />}
|
||||
size="large"
|
||||
onClick={performMsalLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-blue-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="Microsoft登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<SlackFilled className="text-purple-600" />}
|
||||
size="large"
|
||||
onClick={performSlackLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-purple-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="Slack登录"
|
||||
/>
|
||||
<Button
|
||||
icon={<WeComFilled className="text-green-600" />}
|
||||
size="large"
|
||||
onClick={performWeComLogin}
|
||||
className="w-14 h-12 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 hover:border-green-300 transition-all duration-300 shadow-sm hover:shadow-md flex items-center justify-center"
|
||||
title="企业微信登录"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-500 text-xs">© 2024 Your Company. All rights reserved.</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function MenuPage() {
|
||||
return (
|
||||
<>
|
||||
<div>菜单管理</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Link } from "react-router"
|
||||
|
||||
/**
|
||||
* General page not found page.
|
||||
* @constructor
|
||||
*/
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-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>
|
||||
<div className="absolute bottom-0 left-1/2 w-72 h-72 bg-pink-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||
|
||||
<div className="text-center bg-white p-10 lg:p-14 rounded-xl shadow-lg md:shadow-xl max-w-xl w-full">
|
||||
<h1 className="text-7xl lg:text-8xl text-amber-500 m-0 font-bold tracking-wider">404</h1>
|
||||
<h2 className="text-3xl md:text-4xl text-gray-700 mt-4 mb-6">找不到页面</h2>
|
||||
<p className="text-lg text-gray-600 leading-relaxed mb-9">
|
||||
找不到您需要的页面,您是否输入了错误的地址?
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-block bg-blue-600 text-white py-3 px-7 rounded-lg no-underline
|
||||
text-lg font-semibold transition-all duration-300 ease-in-out
|
||||
shadow-md shadow-blue-500/20 hover:bg-blue-700 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-blue-500/30">
|
||||
返回主页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from "react"
|
||||
import { useNavigate } from "react-router"
|
||||
import dayjs from "dayjs"
|
||||
import type { AxiosError } from "axios"
|
||||
import { message } from "antd"
|
||||
|
||||
import { AuthApi } from "@/api"
|
||||
import type { GeneralErrorResponse } from "@/types/web/response"
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
AuthApi.fetchRegisterEnabled()
|
||||
.then((enabled) => {
|
||||
if (!enabled && !cancelled) {
|
||||
void messageApi
|
||||
.error("注册功能暂未开放", dayjs.duration({ seconds: 3 }).asSeconds())
|
||||
.then(() => {
|
||||
if (!cancelled) {
|
||||
void navigate("/login")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
if (cancelled) return
|
||||
const error = reason as AxiosError<GeneralErrorResponse>
|
||||
void messageApi.error(error.response?.data.message ?? "发生未知错误,请稍后再试")
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [messageApi, navigate])
|
||||
|
||||
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="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>
|
||||
<div className="absolute bottom-0 left-1/2 w-72 h-72 bg-pink-200 rounded-full mix-blend-multiply filter blur-xl opacity-70"></div>
|
||||
<div>注册页</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import { useEffect, useState } from "react"
|
||||
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 } from "@/types/web/response"
|
||||
import {
|
||||
DeleteOutlined,
|
||||
ExportOutlined,
|
||||
ImportOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UndoOutlined,
|
||||
} 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, 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 = {
|
||||
code: queryRoleForm?.qCode ?? null,
|
||||
name: queryRoleForm?.qName ?? null,
|
||||
status: queryRoleForm?.qStatus ?? null,
|
||||
pageNum,
|
||||
pageSize,
|
||||
}
|
||||
|
||||
RoleApi.fetchRoles(queryRoleRequest)
|
||||
.then((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>
|
||||
void message.error(err.response?.data.message ?? "获取角色失败")
|
||||
})
|
||||
}
|
||||
|
||||
const changeRoleStatus = (roleId: number | string, checked: boolean) => {
|
||||
const status: Status = checked ? "ACTIVE" : "INACTIVE"
|
||||
setRoles((prevRoles) =>
|
||||
prevRoles.map((role) => (role.id === roleId ? { ...role, status } : role))
|
||||
)
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 flex-col">
|
||||
<Form<QueryRoleForm>
|
||||
form={queryForm}
|
||||
className="mt-0 mb-6"
|
||||
layout="inline"
|
||||
labelAlign="right"
|
||||
onFinish={(values) => {
|
||||
queryRoles(pageNum, pageSize, values)
|
||||
}}
|
||||
onReset={() => {
|
||||
queryRoles(pageNum, pageSize, null)
|
||||
}}>
|
||||
<Form.Item<QueryRoleForm> label="角色名称" name="qName">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item<QueryRoleForm> label="角色编码" name="qCode">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item<QueryRoleForm> label="角色状态" name="qStatus">
|
||||
<Select<Status>
|
||||
className="w-26"
|
||||
options={[
|
||||
{
|
||||
label: "已启用",
|
||||
value: "ACTIVE",
|
||||
},
|
||||
{
|
||||
label: "未启用",
|
||||
value: "INACTIVE",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<QueryRoleForm>>
|
||||
<Space>
|
||||
<Button color="primary" variant="solid" htmlType="submit" icon={<SearchOutlined />}>
|
||||
查询
|
||||
</Button>
|
||||
<Button color="orange" variant="solid" htmlType="reset" icon={<UndoOutlined />}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Space size={8}>
|
||||
<Button variant="solid" type="primary" icon={<PlusOutlined />} onClick={handleAddRole}>
|
||||
新增
|
||||
</Button>
|
||||
<Button variant="solid" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
<Button variant="solid" color="green" icon={<ImportOutlined />}>
|
||||
导入
|
||||
</Button>
|
||||
<Button variant="solid" color="green" icon={<ExportOutlined />}>
|
||||
导出
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table<Role>
|
||||
columns={[
|
||||
{ title: "角色编号", dataIndex: "id" },
|
||||
{ title: "角色名称", dataIndex: "name" },
|
||||
{ title: "角色编码", dataIndex: "code" },
|
||||
{ title: "显示顺序", dataIndex: "sort" },
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
render: (role: Role) => (
|
||||
<>
|
||||
<Switch
|
||||
value={role.status == "ACTIVE"}
|
||||
onChange={(checked) => changeRoleStatus(role.id, checked)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "默认角色",
|
||||
key: "defaultValue",
|
||||
render: (role: Role) => (
|
||||
<>
|
||||
<Switch value={role.defaultValue} disabled />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ title: "创建时间", dataIndex: "createdAt" },
|
||||
{
|
||||
title: "操作",
|
||||
render: (role: Role) => (
|
||||
<>
|
||||
<Space.Compact>
|
||||
<Button variant="solid" onClick={() => handleEditRole(role)}>
|
||||
修改
|
||||
</Button>
|
||||
<Button variant="solid" danger onClick={() => handleDeleteRole(role)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</>
|
||||
),
|
||||
key: "operations",
|
||||
},
|
||||
]}
|
||||
dataSource={roles}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
current: pageNum,
|
||||
total: totalElementCount,
|
||||
pageSize: pageSize,
|
||||
defaultCurrent: 1,
|
||||
defaultPageSize: 10,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: (pageNum, pageSize) => {
|
||||
console.log(`onSizeChange ==> pageNum = ${pageNum}, pageSize = ${pageSize}`)
|
||||
setPageNum(pageNum)
|
||||
setPageSize(pageSize)
|
||||
},
|
||||
onChange: (pageNum, pageSize) => {
|
||||
console.log(`onChange ==> pageNum = ${pageNum}, pageSize = ${pageSize}`)
|
||||
setPageNum(pageNum)
|
||||
setPageSize(pageSize)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
import { type Key, useEffect, useState } from "react"
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tree,
|
||||
type TreeDataNode,
|
||||
Typography,
|
||||
} from "antd"
|
||||
import { App } from "antd"
|
||||
import { type AxiosError } from "axios"
|
||||
import { getCountries, getCountryCallingCode } from "libphonenumber-js"
|
||||
import { DeptApi, UserApi } from "@/api"
|
||||
import { PhoneNumberUtils, DepartmentUtils } from "@/utils"
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ExportOutlined,
|
||||
ImportOutlined,
|
||||
KeyOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UndoOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import AddUserDialogue from "@/components/add-user-dialogue"
|
||||
import EditUserDialogue from "@/components/edit-user-dialogue"
|
||||
import type { QueryUserRequest } from "@/types/web/request"
|
||||
import type { QueryUserForm, UserFormValues } from "@/types/form"
|
||||
import type { GeneralErrorResponse, UserDetailResponse } from "@/types/web/response"
|
||||
import type { Department } from "@/types/entity"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import { useAppSelector } from "@/store"
|
||||
|
||||
export default function UserPage() {
|
||||
const { message, modal } = App.useApp()
|
||||
|
||||
const user = useAppSelector((state) => state.auth.user!)
|
||||
|
||||
const [queryForm] = Form.useForm<QueryUserForm>()
|
||||
const [addUserForm] = Form.useForm<UserFormValues>()
|
||||
const [editUserForm] = Form.useForm<UserFormValues>()
|
||||
const [department, setDepartment] = useState<TreeNode<Department>>()
|
||||
const [departmentTree, setDepartmentTree] = useState<TreeDataNode[]>([])
|
||||
const [selectedDepartment, setSelectedDepartment] = useState<number>(1)
|
||||
const [users, setUsers] = useState<UserDetailResponse[]>([])
|
||||
const [pageNum, setPageNum] = useState<number>(1)
|
||||
const [totalElementCount, setTotalElementCount] = useState<number>(0)
|
||||
const [pageSize, setPageSize] = useState<number>(10)
|
||||
|
||||
const onAddUserFinish = async () => {
|
||||
try {
|
||||
const values = await addUserForm.validateFields()
|
||||
|
||||
await UserApi.addUser(values)
|
||||
|
||||
void message.success(`用户 ${values.username} 创建成功`)
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
console.log(error)
|
||||
if (error instanceof Error && error.message.includes("Validation Failed")) {
|
||||
return false
|
||||
}
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
void message.error(err.response?.data.message ?? "创建失败,请稍后再试")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddUser = () => {
|
||||
modal
|
||||
.confirm({
|
||||
title: "添加用户",
|
||||
content: <AddUserDialogue form={addUserForm} />,
|
||||
width: 600,
|
||||
onOk: onAddUserFinish,
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
const formValues = queryForm.getFieldsValue()
|
||||
queryUsers(pageNum, pageSize, selectedDepartment!, formValues)
|
||||
},
|
||||
() => {
|
||||
console.error("用户取消添加用户")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const onEditUserFinish = async () => {
|
||||
try {
|
||||
const values = await editUserForm.validateFields()
|
||||
|
||||
await UserApi.editUser(values)
|
||||
|
||||
void message.success(`用户更新成功`)
|
||||
return true
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && error.message.includes("Validation Failed")) {
|
||||
return false
|
||||
}
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
void message.error(err.response?.data.message ?? "更新失败,请稍后再试")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditUser = (user: UserDetailResponse) => {
|
||||
modal
|
||||
.confirm({
|
||||
title: `编辑用户: ${user.username}`,
|
||||
content: <EditUserDialogue form={editUserForm} user={user} />,
|
||||
width: 600,
|
||||
onOk: onEditUserFinish,
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
const formValues = queryForm.getFieldsValue()
|
||||
queryUsers(pageNum, pageSize, selectedDepartment, formValues)
|
||||
},
|
||||
() => {
|
||||
console.error("用户取消添加用户")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const handleDeleteUser = (user: UserDetailResponse) => {
|
||||
const isLastElementOnPage = users.length === 1 && pageNum > 1
|
||||
modal.confirm({
|
||||
title: (
|
||||
<>
|
||||
确认删除用户 <Typography.Text code>{user.username}</Typography.Text> 吗?
|
||||
</>
|
||||
),
|
||||
content: "删除后数据将无法恢复。",
|
||||
okText: "删除",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
await UserApi.deleteUser(user.id)
|
||||
void message.success(`用户 ${user.username} 删除成功`)
|
||||
setTotalElementCount((count) => count - 1)
|
||||
if (isLastElementOnPage) {
|
||||
setPageNum((prevPage) => prevPage - 1)
|
||||
} else {
|
||||
queryUsers(pageNum, pageSize, selectedDepartment, queryForm.getFieldsValue())
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
void message.error(err.response?.data.message ?? "删除失败,请稍后再试")
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Query users
|
||||
const queryUsers = (
|
||||
pageNum: number,
|
||||
pageSize: number,
|
||||
departmentId: number,
|
||||
formValues: QueryUserForm | null
|
||||
) => {
|
||||
const queryUserRequest: QueryUserRequest = {
|
||||
createdAtEnd: null,
|
||||
createdAtStart: null,
|
||||
phoneNumber: null,
|
||||
regionAbbreviation: null,
|
||||
status: null,
|
||||
username: null,
|
||||
pageNum,
|
||||
pageSize,
|
||||
departmentId,
|
||||
}
|
||||
|
||||
if (formValues && formValues.qUsername) {
|
||||
queryUserRequest.username = formValues.qUsername
|
||||
}
|
||||
|
||||
if (formValues && formValues.qRegionAbbreviation) {
|
||||
queryUserRequest.regionAbbreviation = formValues.qRegionAbbreviation
|
||||
}
|
||||
|
||||
if (formValues && formValues.qPhoneNumber) {
|
||||
queryUserRequest.phoneNumber = formValues.qPhoneNumber
|
||||
}
|
||||
|
||||
if (formValues && formValues.qStatus) {
|
||||
queryUserRequest.status = formValues.qStatus
|
||||
}
|
||||
|
||||
if (formValues && formValues.qCreatedAtStart) {
|
||||
queryUserRequest.createdAtStart = formValues.qCreatedAtStart.format("YYYY-MM-DD HH:mm:ss")
|
||||
}
|
||||
|
||||
if (formValues && formValues.qCreatedAtEnd) {
|
||||
queryUserRequest.createdAtEnd = formValues.qCreatedAtEnd.format("YYYY-MM-DD HH:mm:ss")
|
||||
}
|
||||
|
||||
console.log(`queryUsers ==> `, queryUserRequest)
|
||||
|
||||
UserApi.fetchUsers(queryUserRequest)
|
||||
.then((userResponse) => {
|
||||
console.log(userResponse)
|
||||
setUsers(userResponse.content)
|
||||
setPageNum(userResponse.pageable.pageNumber + 1)
|
||||
setPageSize(userResponse.pageable.pageSize)
|
||||
setTotalElementCount(userResponse.totalElements)
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
void message.error(err.response?.data.message ?? "发生未知错误,请稍后再试")
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
DeptApi.fetchDepartmentTree()
|
||||
.then((departmentResponse) => {
|
||||
setDepartment(departmentResponse)
|
||||
setDepartmentTree(DepartmentUtils.transformDepartmentData([departmentResponse]))
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
void message.error(err.response?.data.message ?? "发生未知错误,请稍后再试")
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const formValues = queryForm.getFieldsValue()
|
||||
queryUsers(pageNum, pageSize, selectedDepartment, formValues)
|
||||
}, [pageNum, pageSize, selectedDepartment])
|
||||
|
||||
const regionOptions = getCountries().map((country) => {
|
||||
const callingCode = getCountryCallingCode(country)
|
||||
return {
|
||||
label: `${country} (+${callingCode})`,
|
||||
value: country,
|
||||
key: country,
|
||||
}
|
||||
})
|
||||
|
||||
const userStatusOptions = [
|
||||
{
|
||||
label: "已启用",
|
||||
value: "ACTIVE",
|
||||
key: "ACTIVE",
|
||||
},
|
||||
{
|
||||
label: "已停用",
|
||||
value: "INACTIVE",
|
||||
key: "INACTIVE",
|
||||
},
|
||||
{
|
||||
label: "已禁用",
|
||||
value: "LOCKED",
|
||||
key: "LOCKED",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex h-full gap-2">
|
||||
<Tree
|
||||
multiple={false}
|
||||
treeData={departmentTree}
|
||||
classNames={{
|
||||
item: "w-64",
|
||||
}}
|
||||
onSelect={([selectedKey]) => {
|
||||
if (typeof selectedKey == "number") {
|
||||
setSelectedDepartment(selectedKey)
|
||||
}
|
||||
}}
|
||||
selectedKeys={[selectedDepartment]}
|
||||
/>
|
||||
<div className="flex flex-col w-full">
|
||||
<Form<QueryUserForm>
|
||||
className="mt-0 mb-6"
|
||||
form={queryForm}
|
||||
layout="inline"
|
||||
labelAlign="right"
|
||||
onFinish={(values) => {
|
||||
console.log("Query User Form ==> ", values)
|
||||
queryUsers(pageNum, pageSize, Number(selectedDepartment), values)
|
||||
}}
|
||||
onReset={() => {
|
||||
queryUsers(pageNum, pageSize, Number(selectedDepartment), null)
|
||||
}}>
|
||||
<Form.Item<QueryUserForm> label="用户名" name="qUsername">
|
||||
<Input className="w-40" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<QueryUserForm> label="用户状态" name="qStatus">
|
||||
<Select
|
||||
options={userStatusOptions}
|
||||
className="w-24"
|
||||
allowClear
|
||||
onSelect={(item: unknown, option) => {
|
||||
console.log(`selectedItem = ${item} ${option}`)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="手机号码">
|
||||
<Space.Compact>
|
||||
<Form.Item<QueryUserForm> noStyle name="qRegionAbbreviation">
|
||||
<Select
|
||||
options={regionOptions}
|
||||
className="w-28"
|
||||
allowClear
|
||||
showSearch={{
|
||||
optionFilterProp: "label",
|
||||
}}
|
||||
placeholder="国家/地区"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<QueryUserForm> noStyle name="qPhoneNumber">
|
||||
<Input className="w-40" />
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="创建时间">
|
||||
<Space.Compact className="w-full align-baseline">
|
||||
<Form.Item<QueryUserForm> name="qCreatedAtStart">
|
||||
<DatePicker showTime={true} showNow={true} format="YYYY-MM-DD HH:mm:ss" />
|
||||
</Form.Item>
|
||||
<Form.Item<QueryUserForm> name="qCreatedAtEnd">
|
||||
<DatePicker showTime={true} showNow={true} format="YYYY-MM-DD HH:mm:ss" />
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space size={8}>
|
||||
<Button htmlType="submit" type="primary" icon={<SearchOutlined />}>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
htmlType="reset"
|
||||
type="primary"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => {
|
||||
if (department) {
|
||||
// Set department to first root department
|
||||
const { item } = department
|
||||
setSelectedDepartment(item.id)
|
||||
} else {
|
||||
void message.error("未获取到任何部门信息")
|
||||
}
|
||||
}}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Space size={8} className="mb-6">
|
||||
<Button variant="solid" type="primary" icon={<PlusOutlined />} onClick={handleAddUser}>
|
||||
新增
|
||||
</Button>
|
||||
<Button variant="solid" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
<Button variant="solid" color="green" icon={<ImportOutlined />}>
|
||||
导入
|
||||
</Button>
|
||||
<Button variant="solid" color="green" icon={<ExportOutlined />}>
|
||||
导出
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table<UserDetailResponse>
|
||||
rowKey="id"
|
||||
dataSource={users}
|
||||
pagination={{
|
||||
current: pageNum,
|
||||
total: totalElementCount,
|
||||
pageSize: pageSize,
|
||||
defaultCurrent: 1,
|
||||
defaultPageSize: 10,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: (pageNum, pageSize) => {
|
||||
console.log(`onSizeChange ==> pageNum = ${pageNum}, pageSize = ${pageSize}`)
|
||||
setPageNum(pageNum)
|
||||
setPageSize(pageSize)
|
||||
},
|
||||
onChange: (pageNum, pageSize) => {
|
||||
console.log(`onChange ==> pageNum = ${pageNum}, pageSize = ${pageSize}`)
|
||||
setPageNum(pageNum)
|
||||
setPageSize(pageSize)
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{ title: "用户 ID", dataIndex: "id" },
|
||||
{ title: "用户名称", dataIndex: "username" },
|
||||
{ title: "姓名", dataIndex: "fullName" },
|
||||
{ title: "电子邮箱", dataIndex: "email" },
|
||||
{
|
||||
title: "联系电话",
|
||||
render: (user: UserDetailResponse) => {
|
||||
return PhoneNumberUtils.formatInternationalPhoneNumber(
|
||||
user.regionAbbreviation,
|
||||
user.phoneNumber
|
||||
)
|
||||
},
|
||||
key: "phoneNumber",
|
||||
},
|
||||
{
|
||||
title: "头像",
|
||||
render: (user: UserDetailResponse) => {
|
||||
return <Avatar src={user.avatarUrl} alt="用户头像" />
|
||||
},
|
||||
key: "avatarUrl",
|
||||
},
|
||||
{
|
||||
title: "用户状态",
|
||||
render: (user: UserDetailResponse) => {
|
||||
const colour = user.status == "ACTIVE" ? "green" : "red"
|
||||
return (
|
||||
<Tag color={colour} key={`${user.id}-${user.status}`} variant="outlined">
|
||||
{user.status}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
key: "status",
|
||||
},
|
||||
{ title: "所在部门", dataIndex: "departmentName" },
|
||||
{ title: "用户岗位", dataIndex: "positionName" },
|
||||
{
|
||||
title: "操作",
|
||||
render: (value: UserDetailResponse) => {
|
||||
return String(value.id) == String(user.id) ? (
|
||||
<></>
|
||||
) : (
|
||||
<Space.Compact>
|
||||
<Button icon={<EditOutlined />} onClick={() => handleEditUser(value)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button icon={<KeyOutlined />} onClick={() => {}}>
|
||||
重置密码
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteUser(value)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
)
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user