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,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