feat: 初始提交
@@ -0,0 +1,68 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import { HttpStatus } from "@/constant"
|
||||
import type { CaptchaResponse, UserAuthResponse } from "@/types/web/response"
|
||||
import type { UsernamePasswordLoginRequest } from "@/types/web/request"
|
||||
|
||||
/**
|
||||
* 获取验证码图片及验证码 UUID
|
||||
*/
|
||||
async function getCaptcha(): Promise<CaptchaResponse | null> {
|
||||
const { data, status } = await webClient.get<CaptchaResponse | null>("/captcha")
|
||||
if (status == HttpStatus.OK) {
|
||||
return data as CaptchaResponse
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用用户名密码登录
|
||||
* @param request 用户名密码
|
||||
*/
|
||||
async function usernamePasswordLogin(
|
||||
request: UsernamePasswordLoginRequest
|
||||
): Promise<UserAuthResponse | null> {
|
||||
const { data } = await webClient.post<UserAuthResponse>("/auth/login", request)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用企业微信登录
|
||||
* @param code 由企业微信提供的身份验证 code
|
||||
*/
|
||||
async function wecomLogin(code: string): Promise<UserAuthResponse> {
|
||||
const urlSearchParams = new URLSearchParams()
|
||||
urlSearchParams.append("code", code)
|
||||
|
||||
|
||||
const { data } = await webClient.get<UserAuthResponse>(`/auth/wecom/login?${urlSearchParams.toString()}`)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Microsoft Entra 登录
|
||||
* @param msalToken 由 Microsoft Entra 提供的用户身份令牌
|
||||
*/
|
||||
async function msalLogin(msalToken: string): Promise<UserAuthResponse> {
|
||||
const { data, headers } = await webClient.post<UserAuthResponse>(`/auth/msal/login`, {
|
||||
msalToken,
|
||||
})
|
||||
|
||||
const token = (headers as Record<string, string>).authorization ?? ""
|
||||
|
||||
if (!token) {
|
||||
return Promise.reject(new Error("未获取到身份令牌,登录失败"))
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册功能是否启用
|
||||
*/
|
||||
async function fetchRegisterEnabled() {
|
||||
const { data } = await webClient.get<boolean>("/auth/register-enabled")
|
||||
return data
|
||||
}
|
||||
|
||||
export { usernamePasswordLogin, wecomLogin, msalLogin, getCaptcha, fetchRegisterEnabled }
|
||||
@@ -0,0 +1,13 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import type { Department } from "@/types/entity"
|
||||
|
||||
export async function fetchDepartmentTree(): Promise<TreeNode<Department>> {
|
||||
const { data } = await webClient.get<TreeNode<Department>>("/departments/tree")
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchDepartments(): Promise<Department[]> {
|
||||
const { data } = await webClient.get<Department[]>("/departments")
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * as AuthApi from "./auth"
|
||||
export * as MenuApi from "./menu"
|
||||
export * as DeptApi from "./department"
|
||||
export * as UserApi from "./user"
|
||||
export * as PositionApi from "./position"
|
||||
export * as RoleApi from "./role"
|
||||
@@ -0,0 +1,8 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import type { MenuItem } from "@/types/entity"
|
||||
|
||||
export async function fetchMenuTree() {
|
||||
const { data } = await webClient.get<TreeNode<MenuItem>[]>("/menus")
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import type { QueryPositionRequest } from "@/types/web/request"
|
||||
import type { PageResponse } from "@/types/web/response"
|
||||
import type { Position } from "@/types/entity"
|
||||
|
||||
export async function fetchPositions(request: QueryPositionRequest): Promise<PageResponse<Position>> {
|
||||
const { data } = await webClient.get<PageResponse<Position>>("/positions")
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { QueryRoleRequest } from "@/types/web/request"
|
||||
import webClient from "@/service/web-client"
|
||||
import type { PageResponse, RoleResponse } from "@/types/web/response"
|
||||
|
||||
export async function fetchRoles(
|
||||
request: QueryRoleRequest | null
|
||||
): Promise<RoleResponse> {
|
||||
const params = new URLSearchParams()
|
||||
params.append("pageNum", `${request?.pageNum ?? 1}`)
|
||||
params.append("pageSize", `${request?.pageSize ?? 1}`)
|
||||
|
||||
if (request?.name) {
|
||||
params.append("name", request.name)
|
||||
}
|
||||
|
||||
if (request?.code) {
|
||||
params.append("code", request.code)
|
||||
}
|
||||
|
||||
if (request?.status) {
|
||||
params.append("status", request.status)
|
||||
}
|
||||
|
||||
const { data } = await webClient.get<RoleResponse>(`/roles?${params.toString()}`)
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import webClient from "@/service/web-client"
|
||||
import { getCountryCallingCode } from "libphonenumber-js"
|
||||
import { getDefaultCountryCode } from "@/utils/phone-number-utils"
|
||||
import type { AddUserRequest, EditUserRequest, QueryUserRequest } from "@/types/web/request"
|
||||
import type { PageResponse, UserDetailResponse } from "@/types/web/response"
|
||||
import type { UserFormValues } from "@/types/form"
|
||||
|
||||
export async function fetchUsers(
|
||||
request?: QueryUserRequest
|
||||
): Promise<PageResponse<UserDetailResponse>> {
|
||||
const urlSearchParam = new URLSearchParams()
|
||||
urlSearchParam.append("pageNum", `${request?.pageNum ?? 1}`)
|
||||
urlSearchParam.append("pageSize", `${request?.pageSize ?? 10}`)
|
||||
|
||||
if (request?.departmentId) {
|
||||
urlSearchParam.append("departmentId", `${request.departmentId}`)
|
||||
}
|
||||
|
||||
if (request?.username) {
|
||||
urlSearchParam.append("username", request.username)
|
||||
}
|
||||
|
||||
if (request?.regionAbbreviation) {
|
||||
urlSearchParam.append("regionAbbreviation", request.regionAbbreviation)
|
||||
}
|
||||
|
||||
if (request?.phoneNumber) {
|
||||
urlSearchParam.append("phoneNumber", request.phoneNumber)
|
||||
}
|
||||
|
||||
if (request?.status) {
|
||||
urlSearchParam.append("status", request.status)
|
||||
}
|
||||
|
||||
if (request?.createdAtStart) {
|
||||
urlSearchParam.append("createdAtStart", request.createdAtStart)
|
||||
}
|
||||
|
||||
if (request?.createdAtEnd) {
|
||||
urlSearchParam.append("createdAtEnd", request.createdAtEnd)
|
||||
}
|
||||
|
||||
const { data } = await webClient.get<PageResponse<UserDetailResponse>>(
|
||||
`/users?${urlSearchParam.toString()}`
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchUserById(userId: number): Promise<UserDetailResponse> {
|
||||
const { data } = await webClient.get<UserDetailResponse>(`/users/${userId}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function addUser(values: UserFormValues) {
|
||||
await webClient.post("/users", {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
fullName: values.fullName,
|
||||
email: values.email,
|
||||
regionAbbreviation: values.regionAbbreviation ?? getDefaultCountryCode(),
|
||||
phoneNumber: values.phoneNumber,
|
||||
avatarUrl: values.avatarUrl,
|
||||
status: values.status,
|
||||
departmentId: values.departmentId,
|
||||
positionId: values.positionId,
|
||||
roleIds: [],
|
||||
} as AddUserRequest)
|
||||
}
|
||||
|
||||
export async function editUser(values: UserFormValues) {
|
||||
await webClient.put("/users", {
|
||||
id: values.id,
|
||||
username: values.username,
|
||||
fullName: values.fullName,
|
||||
email: values.email,
|
||||
regionAbbreviation: values.regionAbbreviation ?? getDefaultCountryCode(),
|
||||
phoneNumber: values.phoneNumber,
|
||||
avatarUrl: values.avatarUrl,
|
||||
status: values.status,
|
||||
departmentId: values.departmentId,
|
||||
positionId: values.positionId,
|
||||
roleIds: [],
|
||||
} as EditUserRequest)
|
||||
}
|
||||
|
||||
export async function deleteUser(userId: number | string) {
|
||||
await webClient.delete(`/users/${userId}`)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755053408771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3503" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M908.474 381.92c-1.767 7.47-6.128 18.419-12.246 31.624h0.132l-0.7 1.223c-35.695 76.492-128.894 226.512-128.894 226.512s-0.119-0.354-0.486-0.938l-27.235 47.485h131.254L619.61 1021.705l56.903-227.129H573.236l35.886-150.19c-29.033 7.011-63.35 16.654-103.993 29.74 0 0-54.982 32.249-158.382-62.037 0 0-69.736-61.533-29.304-76.909 17.196-6.538 83.49-14.832 135.645-21.894 70.487-9.543 113.848-14.596 113.848-14.596s-217.316 3.255-268.87-4.866c-51.557-8.12-116.959-94.275-130.89-170.019 0 0-21.547-41.58 46.334-21.894s348.855 76.632 348.855 76.632-365.423-112.204-389.74-139.578c-24.312-27.374-71.557-149.423-65.41-224.42 0 0 2.665-18.698 21.804-13.687 0 0 270.149 123.64 454.875 191.323 184.724 67.683 345.33 102.108 324.58 189.739z" fill="#3296FA" p-id="3504"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg height="1828" viewBox="-10.63 -.07077792 823.87 610.06955549" width="2500" xmlns="http://www.w3.org/2000/svg"><path d="m678.27 51.62c90.35 132.84 134.97 282.68 118.29 455.18-.07.73-.45 1.4-1.05 1.84-68.42 50.24-134.71 80.73-200.07 100.95a2.55 2.55 0 0 1 -2.81-.95c-15.1-21.01-28.82-43.16-40.84-66.42-.69-1.37-.06-3.02 1.36-3.56 21.79-8.21 42.51-18.05 62.44-29.7 1.57-.92 1.67-3.17.22-4.25-4.23-3.14-8.42-6.44-12.43-9.74-.75-.61-1.76-.73-2.61-.32-129.39 59.75-271.13 59.75-402.05 0-.85-.38-1.86-.25-2.59.35-4 3.3-8.2 6.57-12.39 9.71-1.45 1.08-1.33 3.33.25 4.25 19.93 11.43 40.65 21.49 62.41 29.74 1.41.54 2.08 2.15 1.38 3.52-11.76 23.29-25.48 45.44-40.86 66.45-.67.85-1.77 1.24-2.81.92-65.05-20.22-131.34-50.71-199.76-100.95-.57-.44-.98-1.14-1.04-1.87-13.94-149.21 14.47-300.29 118.18-455.18.25-.41.63-.73 1.07-.92 51.03-23.42 105.7-40.65 162.84-50.49 1.04-.16 2.08.32 2.62 1.24 7.06 12.5 15.13 28.53 20.59 41.63 60.23-9.2 121.4-9.2 182.89 0 5.46-12.82 13.25-29.13 20.28-41.63a2.47 2.47 0 0 1 2.62-1.24c57.17 9.87 111.84 27.1 162.83 50.49.45.19.82.51 1.04.95zm-339.04 283.7c.63-44.11-31.53-80.61-71.9-80.61-40.04 0-71.89 36.18-71.89 80.61 0 44.42 32.48 80.6 71.89 80.6 40.05 0 71.9-36.18 71.9-80.6zm265.82 0c.63-44.11-31.53-80.61-71.89-80.61-40.05 0-71.9 36.18-71.9 80.61 0 44.42 32.48 80.6 71.9 80.6 40.36 0 71.89-36.18 71.89-80.6z" fill="#5865f2"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762846300029" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8286" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M874.666667 181.333333H149.333333c-40.533333 0-74.666667 34.133333-74.666666 74.666667v512c0 40.533333 34.133333 74.666667 74.666666 74.666667h725.333334c40.533333 0 74.666667-34.133333 74.666666-74.666667V256c0-40.533333-34.133333-74.666667-74.666666-74.666667z m-725.333334 64h725.333334c6.4 0 10.666667 4.266667 10.666666 10.666667v25.6L512 516.266667l-373.333333-234.666667V256c0-6.4 4.266667-10.666667 10.666666-10.666667z m725.333334 533.333334H149.333333c-6.4 0-10.666667-4.266667-10.666666-10.666667V356.266667l356.266666 224c4.266667 4.266667 10.666667 4.266667 17.066667 4.266666s12.8-2.133333 17.066667-4.266666l356.266666-224V768c0 6.4-4.266667 10.666667-10.666666 10.666667z" fill="#666666" p-id="8287"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762845900662" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6418" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M932.317184 567.76704L885.10464 422.46144l-93.57312-287.997952c-4.8128-14.81728-25.776128-14.81728-30.590976 0L667.36128 422.459392H356.62848L263.051264 134.46144c-4.8128-14.81728-25.776128-14.81728-30.593024 0l-93.57312 287.997952-47.210496 145.309696a32.165888 32.165888 0 0 0 11.68384 35.96288l408.6272 296.890368L920.61696 603.734016c11.272192-8.192 15.990784-22.71232 11.68384-35.964928" fill="#FC6D26" p-id="6419"></path><path d="M512.002048 900.62848l155.365376-478.171136H356.634624z" fill="#E24329" p-id="6420"></path><path d="M512.004096 900.62848L356.63872 422.47168H138.901504z" fill="#FC6D26" p-id="6421"></path><path d="M138.891264 422.465536l-47.214592 145.309696a32.16384 32.16384 0 0 0 11.685888 35.96288L511.991808 900.62848z" fill="#FCA326" p-id="6422"></path><path d="M138.893312 422.459392h217.737216L263.053312 134.46144c-4.8128-14.819328-25.778176-14.819328-30.590976 0z" fill="#E24329" p-id="6423"></path><path d="M512.002048 900.62848l155.365376-478.154752H885.10464z" fill="#FC6D26" p-id="6424"></path><path d="M885.11488 422.465536l47.214592 145.309696a32.16384 32.16384 0 0 1-11.685888 35.96288L512.014336 900.62848z" fill="#FCA326" p-id="6425"></path><path d="M885.096448 422.459392H667.36128l93.577216-287.997952c4.814848-14.819328 25.778176-14.819328 30.590976 0z" fill="#E24329" p-id="6426"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762845811623" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5438" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M214.101333 512c0-32.512 5.546667-63.701333 15.36-92.928L57.173333 290.218667A491.861333 491.861333 0 0 0 4.693333 512c0 79.701333 18.858667 154.88 52.394667 221.610667l172.202667-129.066667A290.56 290.56 0 0 1 214.101333 512" fill="#FBBC05" p-id="5439"></path><path d="M516.693333 216.192c72.106667 0 137.258667 25.002667 188.458667 65.962667L854.101333 136.533333C763.349333 59.178667 646.997333 11.392 516.693333 11.392c-202.325333 0-376.234667 113.28-459.52 278.826667l172.373334 128.853333c39.68-118.016 152.832-202.88 287.146666-202.88" fill="#EA4335" p-id="5440"></path><path d="M516.693333 807.808c-134.357333 0-247.509333-84.864-287.232-202.88l-172.288 128.853333c83.242667 165.546667 257.152 278.826667 459.52 278.826667 124.842667 0 244.053333-43.392 333.568-124.757333l-163.584-123.818667c-46.122667 28.458667-104.234667 43.776-170.026666 43.776" fill="#34A853" p-id="5441"></path><path d="M1005.397333 512c0-29.568-4.693333-61.44-11.648-91.008H516.650667V614.4h274.602666c-13.696 65.962667-51.072 116.650667-104.533333 149.632l163.541333 123.818667c93.994667-85.418667 155.136-212.650667 155.136-375.850667" fill="#4285F4" p-id="5442"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762844620229" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5184" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0h1024v1024H0V0z m244.622222 193.422222c6.6048 19.820089 11.593956 22.840889 28.091734 34.844445C302.887822 251.044978 330.763378 275.717689 358.4 301.511111l13.641956 12.515556C435.325156 374.328889 496.116622 450.2528 534.755556 529.066667v22.755555c-12.686222 11.576889-12.686222 11.576889-29.513956 23.113956l-16.594489 11.576889L472.177778 597.333333l-14.153956 10.643911C443.733333 614.4 443.733333 614.4 427.554133 611.202844c-107.679289-49.043911-207.9232-121.400889-289.934222-206.648888C126.674489 391.224889 126.674489 391.224889 113.777778 392.533333a18880.682667 18880.682667 0 0 0-1.479111 140.942223c-0.142222 21.816889-0.341333 43.633778-0.6656 65.444977a5641.563022 5641.563022 0 0 0-0.551823 63.186489c-0.056889 8.027022-0.159289 16.054044-0.312888 24.081067a1867.036444 1867.036444 0 0 0-0.221867 33.780622l-0.187733 19.410489c5.125689 25.856 20.997689 38.269156 42.183111 52.736l9.568711 4.886756 10.934044 5.597866 11.491556 5.575111 12.026311 5.927823c114.346667 54.516622 245.731556 61.513956 365.550933 20.718933 151.614578-55.153778 240.497778-162.9184 312.888889-302.290489l8.391111-16.145067c5.461333-10.552889 10.882844-21.122844 16.270222-31.715555 12.982044-25.2416 25.673956-48.014222 44.691912-69.381689-4.881067-12.8-4.881067-12.8-19.626667-17.464889C859.704889 376.638578 799.857778 374.328889 733.866667 392.533333l-1.553067-19.933866c-3.345067-26.447644-12.481422-47.286044-24.758044-70.729956L701.44 289.905778c-18.875733-35.9424-41.216-67.726222-69.973333-96.483556-16.816356-1.672533-16.816356-1.672533-36.807111-1.291378h-11.434667c-12.498489 0-24.991289 0.091022-37.489778 0.182045l-25.969778 0.045511c-22.806756 0.045511-45.607822 0.159289-68.408889 0.284444-23.256178 0.113778-46.518044 0.170667-69.779911 0.227556-45.653333 0.113778-91.306667 0.312889-136.954311 0.551822z" fill="#FEFEFE" p-id="5185"></path><path d="M113.777778 392.533333c16.611556 7.953067 27.653689 15.251911 40.180622 28.802845 126.765511 129.991111 331.172978 252.683378 514.844444 268.8 49.237333-0.762311 98.9184-17.590044 133.330489-52.980622l11.377778 5.688888c-86.971733 112.878933-195.4816 189.2352-337.755022 213.799823C354.833067 870.570667 224.398222 848.2816 125.155556 773.688889c-13.448533-20.1728-12.765867-28.529778-12.669156-52.4288v-22.084267l0.182044-23.864889 0.045512-24.416711c0.045511-21.407289 0.159289-42.814578 0.284444-64.221866 0.113778-21.845333 0.170667-43.702044 0.227556-65.547378A46936.746667 46936.746667 0 0 1 113.777778 392.533333z" fill="#3171FE" p-id="5186"></path><path d="M244.622222 193.422222c49.749333-1.058133 99.498667-1.865956 149.253689-2.3552 23.108267-0.238933 46.205156-0.5632 69.307733-1.080889 22.317511-0.494933 44.629333-0.762311 66.946845-0.881777 8.4992-0.085333 16.992711-0.250311 25.486222-0.494934 66.417778-1.848889 66.417778-1.848889 89.759289 14.637511A216.558933 216.558933 0 0 1 671.288889 238.933333l11.798755 17.379556c52.718933 92.16 52.718933 92.16 50.779023 136.220444l11.377777 5.688889-10.643911 3.777422-14.597689 5.467023-14.205155 5.199644c-46.432711 22.124089-81.891556 53.327644-117.356089 90.089245l-12.452978 12.686222A5490.0224 5490.0224 0 0 0 546.133333 546.133333a1010.961067 1010.961067 0 0 1-23.131022-36.443022C493.983289 461.909333 463.701333 417.547378 426.666667 375.466667c-4.152889-4.778667-8.305778-9.545956-12.572445-14.466845-44.987733-50.898489-91.022222-97.934222-146.067911-137.864533C260.039111 217.258667 252.302222 211.057778 244.622222 204.8v-11.377778z" fill="#01D6BA" p-id="5187"></path><path d="M938.666667 403.911111l5.688889 11.377778-7.4752 9.984c-27.022222 37.182578-47.775289 75.531378-67.549867 116.952178a7167.106844 7167.106844 0 0 1-17.3056 35.84l-7.537778 15.786666c-26.168889 47.314489-68.1984 78.984533-119.864889 94.151111C629.105778 712.533333 529.789156 662.272 443.733333 625.777778v-11.377778c8.664178-5.802667 17.618489-11.167289 26.669511-16.355556 47.866311-29.622044 87.552-68.437333 126.464-108.845511C688.651378 394.183111 811.121778 348.171378 938.666667 403.911111z" fill="#0C3A9B" p-id="5188"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,66 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500"
|
||||
zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500"
|
||||
preserveAspectRatio="xMidYMid meet" version="1.0">
|
||||
<metadata/>
|
||||
<defs>
|
||||
<g/>
|
||||
</defs>
|
||||
<rect x="-37.5" width="450" fill="#ffffff" y="-37.499999" height="449.999989" fill-opacity="1"/>
|
||||
<rect x="-37.5" width="450" fill="#ffffff" y="-37.499999" height="449.999989" fill-opacity="1"/>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(27.506677, 211.874986)">
|
||||
<g>
|
||||
<path d="M 12.359375 -0.453125 C 8.003906 -1.816406 5.003906 -4.3125 3.359375 -7.9375 C 1.710938 -11.5625 1.550781 -16.253906 2.875 -22.015625 C 4.195312 -27.773438 6.519531 -32.46875 9.84375 -36.09375 C 13.175781 -39.71875 17.34375 -42.210938 22.34375 -43.578125 Z M 27.578125 -34.109375 L 30.015625 -44.796875 L 31.875 -44.796875 C 39.46875 -44.796875 44.882812 -42.851562 48.125 -38.96875 C 51.363281 -35.09375 52.128906 -29.441406 50.421875 -22.015625 C 48.722656 -14.585938 45.351562 -8.929688 40.3125 -5.046875 C 35.28125 -1.171875 28.96875 0.765625 21.375 0.765625 C 20.519531 0.765625 19.898438 0.742188 19.515625 0.703125 L 21.953125 -9.921875 C 22.722656 -9.835938 23.34375 -9.796875 23.8125 -9.796875 C 26.84375 -9.796875 29.367188 -10.710938 31.390625 -12.546875 C 33.421875 -14.378906 34.796875 -16.851562 35.515625 -19.96875 L 36.421875 -24.0625 C 37.140625 -27.175781 36.910156 -29.648438 35.734375 -31.484375 C 34.566406 -33.316406 32.46875 -34.234375 29.4375 -34.234375 C 28.96875 -34.234375 28.347656 -34.191406 27.578125 -34.109375 Z M 27.578125 -34.109375 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(80.815405, 211.874986)">
|
||||
<g>
|
||||
<path d="M 31.171875 -34.5625 C 28.816406 -34.5625 26.363281 -34.003906 23.8125 -32.890625 L 21.953125 -24.890625 C 22.460938 -25.066406 23.0625 -25.15625 23.75 -25.15625 C 25.15625 -25.15625 26.125 -24.71875 26.65625 -23.84375 C 27.1875 -22.96875 27.28125 -21.804688 26.9375 -20.359375 L 22.265625 0 L 35 0 L 40.0625 -21.890625 C 41 -25.984375 40.726562 -29.117188 39.25 -31.296875 C 37.78125 -33.472656 35.085938 -34.5625 31.171875 -34.5625 Z M 17.28125 -19.96875 L 17.796875 -28.671875 L 18.171875 -33.796875 L 7.75 -33.796875 L -0.0625 0 L 12.671875 0 L 17.21875 -19.84375 Z M 17.28125 -19.96875 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(123.500788, 211.874986)">
|
||||
<g>
|
||||
<path d="M 19.515625 -33.796875 L 11.703125 0 L -1.03125 0 L 6.78125 -33.796875 Z M 22.46875 -46.40625 L 20.484375 -37.765625 L 7.75 -37.765625 L 9.71875 -46.40625 Z M 22.46875 -46.40625 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(144.811483, 211.874986)">
|
||||
<g>
|
||||
<path d="M 45.5625 -33.796875 L 29.953125 -17.59375 L 38.53125 0 L 23.671875 0 L 19.703125 -10.625 L 19.453125 -10.625 L 10.5625 0 L -3.578125 0 L 13.5625 -17.921875 L 5.5 -33.796875 L 20.484375 -33.796875 L 23.8125 -24.828125 L 24.0625 -24.828125 L 31.484375 -33.796875 Z M 45.5625 -33.796875 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(187.496867, 211.874986)">
|
||||
<g>
|
||||
<path d="M 24 -44.03125 L 13.890625 0 L -0.25 0 L 9.859375 -44.03125 Z M 29.25 -34.375 L 31.421875 -44.03125 L 39.171875 -44.03125 C 42.578125 -44.03125 45.21875 -43.003906 47.09375 -40.953125 C 48.976562 -38.910156 49.554688 -36.332031 48.828125 -33.21875 C 47.585938 -27.800781 44.347656 -24.382812 39.109375 -22.96875 L 39.109375 -22.71875 C 44.390625 -21.4375 46.328125 -17.769531 44.921875 -11.71875 C 44.109375 -8.257812 42.21875 -5.441406 39.25 -3.265625 C 36.289062 -1.085938 33.019531 0 29.4375 0 L 21.3125 0 L 23.671875 -10.234375 L 27 -10.234375 C 27.601562 -10.234375 28.179688 -10.382812 28.734375 -10.6875 C 29.285156 -10.988281 29.765625 -11.40625 30.171875 -11.9375 C 30.578125 -12.46875 30.863281 -13.054688 31.03125 -13.703125 L 31.171875 -14.34375 C 31.421875 -15.320312 31.300781 -16.140625 30.8125 -16.796875 C 30.320312 -17.460938 29.628906 -17.796875 28.734375 -17.796875 L 25.40625 -17.796875 L 27.515625 -26.875 L 29.828125 -26.875 C 30.722656 -26.875 31.5625 -27.203125 32.34375 -27.859375 C 33.132812 -28.523438 33.640625 -29.351562 33.859375 -30.34375 L 33.984375 -30.96875 C 34.191406 -31.914062 34.0625 -32.71875 33.59375 -33.375 C 33.125 -34.039062 32.441406 -34.375 31.546875 -34.375 Z M 29.25 -34.375 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(237.285815, 211.874986)">
|
||||
<g>
|
||||
<path d="M 44.609375 -33.796875 L 25.015625 -1.03125 C 24.847656 -0.769531 24.703125 -0.507812 24.578125 -0.25 L 23.671875 -16.890625 L 32.0625 -33.796875 Z M 21.25 4.609375 C 19.03125 7.460938 16.585938 9.644531 13.921875 11.15625 C 11.253906 12.675781 8.148438 13.4375 4.609375 13.4375 C 1.835938 13.4375 -0.53125 13.050781 -2.5 12.28125 L -0.765625 4.671875 L 4.34375 4.671875 C 7.550781 4.671875 9.816406 3.113281 11.140625 0 L 5.5 -33.796875 L 19.203125 -33.796875 Z M 21.25 4.609375 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(276.387414, 211.874986)">
|
||||
<g>
|
||||
<path d="M 24.390625 -44.03125 L 22.015625 -33.796875 L 29.1875 -33.796875 L 27.203125 -25.15625 L 20.03125 -25.15625 L 17.03125 -12.28125 C 16.644531 -10.832031 16.628906 -9.734375 16.984375 -8.984375 C 17.347656 -8.242188 18.210938 -7.875 19.578125 -7.875 L 23.171875 -7.875 L 21.4375 -0.390625 C 18.96875 0.378906 16.148438 0.765625 12.984375 0.765625 C 9.273438 0.765625 6.554688 0.0820312 4.828125 -1.28125 C 3.097656 -2.644531 2.617188 -4.96875 3.390625 -8.25 L 7.296875 -25.15625 L 2.5625 -25.15625 L 4.546875 -33.796875 L 9.796875 -33.796875 L 14.90625 -44.03125 Z M 24.390625 -44.03125 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#324370" fill-opacity="1">
|
||||
<g transform="translate(304.801673, 211.874986)">
|
||||
<g>
|
||||
<path d="M 25.40625 -34.5625 C 38.125 -34.5625 43.117188 -28.671875 40.390625 -16.890625 L 39.875 -14.71875 L 18.109375 -14.71875 L 19.515625 -20.734375 L 28.421875 -20.734375 C 28.796875 -22.484375 28.597656 -23.867188 27.828125 -24.890625 C 27.066406 -25.921875 25.789062 -26.4375 24 -26.4375 C 22.84375 -26.4375 21.753906 -26.265625 20.734375 -25.921875 L 22.65625 -34.4375 C 23.894531 -34.519531 24.8125 -34.5625 25.40625 -34.5625 Z M 18.9375 -33.921875 L 14.53125 -14.71875 C 13.414062 -9.8125 15.117188 -7.359375 19.640625 -7.359375 C 21.773438 -7.359375 23.441406 -7.804688 24.640625 -8.703125 C 25.835938 -9.597656 26.601562 -10.789062 26.9375 -12.28125 L 39.296875 -12.28125 C 38.359375 -8.1875 36.0625 -4.988281 32.40625 -2.6875 C 28.757812 -0.382812 23.953125 0.765625 17.984375 0.765625 C 11.710938 0.765625 7.1875 -0.691406 4.40625 -3.609375 C 1.632812 -6.535156 0.929688 -10.960938 2.296875 -16.890625 C 3.367188 -21.546875 5.351562 -25.3125 8.25 -28.1875 C 11.15625 -31.070312 14.71875 -32.984375 18.9375 -33.921875 Z M 18.9375 -33.921875 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 2499.6 2500" viewBox="0 0 2499.6 2500" xmlns="http://www.w3.org/2000/svg"><path d="m1187.9 1187.9h-1187.9v-1187.9h1187.9z" fill="#f1511b"/><path d="m2499.6 1187.9h-1188v-1187.9h1187.9v1187.9z" fill="#80cc28"/><path d="m1187.9 2500h-1187.9v-1187.9h1187.9z" fill="#00adef"/><path d="m2499.6 2500h-1188v-1187.9h1187.9v1187.9z" fill="#fbbc09"/></svg>
|
||||
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755065235771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7343" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333 362.667a42.667 42.667 0 1 0 85.334 0h-85.334zM464 597.333A42.667 42.667 0 1 0 464 512v85.333z m304 0a42.667 42.667 0 1 0 85.333 0H768z m-170.667 0a42.667 42.667 0 1 0 85.334 0h-85.334z m-384-384h597.334V128H213.333v85.333z m-42.666 256V256H85.333v213.333h85.334zM853.333 256v106.667h85.334V256h-85.334z m-256 384h256v-85.333h-256V640z m256 0v170.667h85.334V640h-85.334z m0 170.667h-256V896h256v-85.333z m-256 0V640H512v170.667h85.333z m0 0H512A85.333 85.333 0 0 0 597.333 896v-85.333z m256 0V896a85.333 85.333 0 0 0 85.334-85.333h-85.334z m0-170.667h85.334a85.333 85.333 0 0 0-85.334-85.333V640z m-256-85.333A85.333 85.333 0 0 0 512 640h85.333v-85.333zM768 512v85.333h85.333V512H768z m-85.333 85.333V512h-85.334v85.333h85.334z m42.666-128A42.667 42.667 0 0 1 768 512h85.333a128 128 0 0 0-128-128v85.333z m0-85.333a128 128 0 0 0-128 128h85.334a42.667 42.667 0 0 1 42.666-42.667V384z m-640 85.333a128 128 0 0 0 128 128V512a42.667 42.667 0 0 1-42.666-42.667H85.333z m725.334-256A42.667 42.667 0 0 1 853.333 256h85.334a128 128 0 0 0-128-128v85.333zM213.333 128a128 128 0 0 0-128 128h85.334a42.667 42.667 0 0 1 42.666-42.667V128zM464 512H213.333v85.333H464V512zM256 362.667a64 64 0 1 0 128 0 64 64 0 1 0-128 0zM448 362.667a64 64 0 1 0 128 0 64 64 0 1 0-128 0z" p-id="7344"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762844995065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7045" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M244.224 643.84c0 59.221333-45.098667 107.264-100.778667 107.264C87.808 751.104 42.666667 703.061333 42.666667 643.84c0-59.221333 45.141333-107.264 100.778666-107.264h100.778667v107.264zM294.613333 643.84c0-59.306667 45.141333-107.306667 100.821334-107.306667 55.637333 0 100.778667 48.042667 100.778666 107.264v268.288c0 59.264-45.141333 107.306667-100.778666 107.306667-55.68 0-100.821333-48.042667-100.821334-107.306667v-268.288z" fill="#E01E5A" p-id="7046"></path><path d="M395.392 214.613333c-55.637333 0-100.778667-48.042667-100.778667-107.306666C294.613333 48.042667 339.754667 0 395.392 0c55.68 0 100.821333 48.042667 100.821333 107.306667V214.613333H395.392zM395.392 268.245333c55.68 0 100.821333 48.085333 100.821333 107.306667 0 59.306667-45.141333 107.306667-100.821333 107.306667H143.445333C87.808 482.858667 42.666667 434.816 42.666667 375.552c0-59.221333 45.141333-107.306667 100.778666-107.306667h251.946667z" fill="#36C5F0" p-id="7047"></path><path d="M798.549333 375.552c0-59.221333 45.098667-107.306667 100.778667-107.306667 55.637333 0 100.778667 48.085333 100.778667 107.306667 0 59.306667-45.141333 107.306667-100.778667 107.306667h-100.778667V375.552zM748.16 375.552c0 59.306667-45.141333 107.306667-100.821333 107.306667-55.637333 0-100.778667-48.042667-100.778667-107.306667V107.306667C546.56 48.042667 591.701333 0 647.338667 0c55.68 0 100.821333 48.042667 100.821333 107.306667v268.245333z" fill="#2EB67D" p-id="7048"></path><path d="M647.381333 804.778667c55.637333 0 100.778667 48.042667 100.778667 107.306666 0 59.264-45.141333 107.306667-100.778667 107.306667-55.68 0-100.821333-48.042667-100.821333-107.306667v-107.306666h100.821333zM647.381333 751.104c-55.68 0-100.821333-48.042667-100.821333-107.306667 0-59.221333 45.141333-107.306667 100.821333-107.306666h251.904c55.68 0 100.778667 48.085333 100.778667 107.306666 0 59.306667-45.098667 107.306667-100.778667 107.306667h-251.904z" fill="#ECB22E" p-id="7049"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755053393715" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2527" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M665.856 748.330667a13.653333 13.653333 0 0 0 1.706667 20.821333 175.189333 175.189333 0 0 1 53.973333 105.216 57.045333 57.045333 0 1 0 60.970667-71.722667 175.061333 175.061333 0 0 1-97.365334-54.314666 13.653333 13.653333 0 0 0-19.285333 0z" fill="#FB6500" p-id="2528"></path><path d="M897.450667 658.048c-9.258667 9.216-15.061333 21.333333-16.426667 34.346667a175.061333 175.061333 0 0 1-54.101333 97.493333 13.653333 13.653333 0 1 0 20.821333 17.408 175.061333 175.061333 0 0 1 105.173333-53.930667 57.045333 57.045333 0 1 0-55.296-95.317333h-0.213333z" fill="#0082EF" p-id="2529"></path><path d="M736.725333 497.109333a57.002667 57.002667 0 0 0 34.346667 97.024c37.546667 6.912 71.765333 25.941333 97.493333 54.101334a13.653333 13.653333 0 1 0 17.450667-20.821334A175.189333 175.189333 0 0 1 832 522.24a57.088 57.088 0 0 0-95.317333-25.130667z" fill="#2DBC00" p-id="2530"></path><path d="M708.693333 586.24l-0.981333 1.024a174.890667 174.890667 0 0 1-106.752 55.893333 56.661333 56.661333 0 0 0-25.258667 95.274667 57.045333 57.045333 0 0 0 96.981334-34.346667c6.997333-37.546667 26.069333-71.808 54.314666-97.493333a13.653333 13.653333 0 1 0-18.261333-20.352z" fill="#FFCC00" p-id="2531"></path><path d="M364.074667 93.866667c-102.272 11.306667-194.986667 55.04-261.632 123.306666A345.472 345.472 0 0 0 38.570667 307.072a312.874667 312.874667 0 0 0 22.058666 315.946667 490.666667 490.666667 0 0 0 74.965334 85.76l-12.288 96.64-1.365334 4.096c-0.341333 1.194667-0.341333 2.56-0.512 3.754666l-0.341333 3.072 0.341333 3.072a31.104 31.104 0 0 0 46.762667 24.064h0.512l1.877333-1.365333 29.397334-14.677333L287.573333 783.36c41.642667 11.946667 84.778667 17.92 128.085334 17.621333a459.349333 459.349333 0 0 0 157.141333-27.306666 56.832 56.832 0 0 1-38.784-59.648 385.749333 385.749333 0 0 1-161.536 16.085333l-8.746667-1.194667a390.528 390.528 0 0 1-58.026666-12.16 39.68 39.68 0 0 0-31.104 3.285334l-2.389334 1.152-72.106666 42.368-3.029334 1.877333c-1.706667 1.024-2.56 1.365333-3.413333 1.365333a4.949333 4.949333 0 0 1-4.608-5.12l2.730667-11.093333 3.242666-12.117333 5.12-20.010667 5.973334-22.186667a30.208 30.208 0 0 0-10.922667-33.621333 315.050667 315.050667 0 0 1-72.746667-75.861333 246.4 246.4 0 0 1-17.792-248.96 284.672 284.672 0 0 1 51.242667-71.722667C210.56 209.706667 287.402667 173.824 372.48 164.608a403.370667 403.370667 0 0 1 88.448 0c84.522667 9.728 161.024 46.08 215.338667 102.144 21.034667 21.674667 38.101333 46.08 50.730666 72.064 16.896 34.474667 25.429333 71.04 25.429334 108.416 0 3.925333-0.341333 7.850667-0.512 11.648a56.874667 56.874667 0 0 1 70.016 8.192l2.56 3.072a311.168 311.168 0 0 0-31.061334-161.92 348.757333 348.757333 0 0 0-63.189333-89.813333 428.629333 428.629333 0 0 0-260.608-124.16A480.384 480.384 0 0 0 364.074667 93.866667z" fill="#0082EF" p-id="2532"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,30 @@
|
||||
import { type FormInstance } from "antd"
|
||||
import UserDisplayForm from "@/components/user-display-form"
|
||||
import type { UserFormValues } from "@/types/form"
|
||||
|
||||
interface AddUserProps {
|
||||
form: FormInstance<UserFormValues>
|
||||
}
|
||||
|
||||
export default function AddUserDialogue({ form }: AddUserProps) {
|
||||
return (
|
||||
<UserDisplayForm
|
||||
isEditing={true}
|
||||
form={form}
|
||||
initialValues={{
|
||||
id: null,
|
||||
username: "",
|
||||
password: "",
|
||||
regionAbbreviation: import.meta.env.VITE_DEFAULT_REGION_CODE,
|
||||
fullName: "",
|
||||
email: "",
|
||||
phoneNumber: "",
|
||||
avatarUrl: "",
|
||||
status: "ACTIVE",
|
||||
departmentId: null,
|
||||
positionId: null,
|
||||
}}
|
||||
isAdding
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FormInstance } from "antd"
|
||||
import UserDisplayForm from "@/components/user-display-form"
|
||||
import type { User } from "@/types/entity"
|
||||
import type { UserFormValues } from "@/types/form"
|
||||
|
||||
interface EditUserProps {
|
||||
user: User
|
||||
form: FormInstance<UserFormValues>
|
||||
}
|
||||
|
||||
export default function EditUserDialogue({ user, form }: EditUserProps) {
|
||||
return (
|
||||
<UserDisplayForm
|
||||
initialValues={{
|
||||
...user,
|
||||
password: null
|
||||
}}
|
||||
isEditing={true}
|
||||
form={form}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { ForwardRefExoticComponent } from "react"
|
||||
import Icon from "@ant-design/icons"
|
||||
|
||||
import BrandMicrosoft from "@/assets/microsoft.svg?react"
|
||||
import BrandDingTalk from "@/assets/dingtalk.svg?react"
|
||||
import BrandWeCom from "@/assets/wecom.svg?react"
|
||||
import BrandLark from "@/assets/lark.svg?react"
|
||||
import BrandSlack from "@/assets/slack.svg?react"
|
||||
import BrandGoogle from "@/assets/google.svg?react"
|
||||
import BrandGitlab from "@/assets/gitlab.svg?react"
|
||||
import BrandEmail from "@/assets/email.svg?react"
|
||||
import BrandDiscord from "@/assets/discord.svg?react"
|
||||
import Logo from "@/assets/logo.svg?react"
|
||||
import type { AntIconComponentProps } from "@/types/antd"
|
||||
|
||||
/**
|
||||
* Microsoft 图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function MicrosoftFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandMicrosoft as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 钉钉图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function DingTalkFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandDingTalk as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业微信图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function WeComFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandWeCom as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function LarkFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandLark as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack 图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function SlackFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandSlack as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Google 图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function GoogleFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandGoogle as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* GitLab 图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function GitlabFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandGitlab as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Email 图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function EmailFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandEmail as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord 图标
|
||||
* @param props Icon 属性
|
||||
* @constructor
|
||||
*/
|
||||
export function DiscordFilled(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={BrandDiscord as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
|
||||
export function ApplicationLogo(props: Partial<AntIconComponentProps>) {
|
||||
return <Icon component={Logo as ForwardRefExoticComponent<unknown>} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Spin } from "antd"
|
||||
|
||||
export default function LoadingFallback() {
|
||||
return <Spin fullscreen />
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Navigate, Outlet, useLocation } from "react-router"
|
||||
import { useAppSelector } from "@/store"
|
||||
import DashboardLayout from "@/layouts/dashboard-layout"
|
||||
|
||||
/**
|
||||
* 需要身份验证的前置组件
|
||||
* @constructor
|
||||
*/
|
||||
export default function ProtectedRoute() {
|
||||
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated)
|
||||
const location = useLocation()
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardLayout >
|
||||
<Outlet />
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { App, Checkbox, Form, type FormInstance, Input, Select, Space, TreeSelect } from "antd"
|
||||
import type { AxiosError } from "axios"
|
||||
import { getCountries, getCountryCallingCode } from "libphonenumber-js"
|
||||
import { DeptApi, PositionApi } from "@/api"
|
||||
import type { UserFormValues } from "@/types/form"
|
||||
import type { Department, Position } from "@/types/entity"
|
||||
import type { GeneralErrorResponse } from "@/types/web/response"
|
||||
import type { AntTreeSelectOption } from "@/types/antd"
|
||||
|
||||
interface UserDisplayFormProps {
|
||||
initialValues?: UserFormValues
|
||||
isEditing: boolean
|
||||
isAdding?: boolean
|
||||
form: FormInstance<UserFormValues>
|
||||
}
|
||||
|
||||
const countryOptions = getCountries().map((country) => {
|
||||
const callingCode = getCountryCallingCode(country)
|
||||
return {
|
||||
label: `${country} (+${callingCode})`,
|
||||
value: country,
|
||||
key: country,
|
||||
}
|
||||
})
|
||||
|
||||
function transformDepartmentToTree(departments: Department[]): AntTreeSelectOption<number>[] {
|
||||
const sortedDepartments = [...departments].sort((a, b) => a.sort - b.sort)
|
||||
|
||||
const map: Record<number, AntTreeSelectOption<number>> = {}
|
||||
const tree: AntTreeSelectOption<number>[] = []
|
||||
|
||||
sortedDepartments.forEach((dept) => {
|
||||
map[dept.id] = {
|
||||
label: dept.name,
|
||||
value: dept.id,
|
||||
children: [],
|
||||
}
|
||||
})
|
||||
|
||||
sortedDepartments.forEach((dept) => {
|
||||
const node = map[dept.id]
|
||||
if (dept.parentId !== null && map[dept.parentId]) {
|
||||
map[dept.parentId].children!.push(node)
|
||||
} else {
|
||||
tree.push(node)
|
||||
}
|
||||
})
|
||||
return tree
|
||||
}
|
||||
|
||||
export default function UserDisplayForm({
|
||||
initialValues,
|
||||
isEditing,
|
||||
form,
|
||||
isAdding = false,
|
||||
}: UserDisplayFormProps) {
|
||||
// Get message wrapper from Antd
|
||||
const { message } = App.useApp()
|
||||
|
||||
// Build form values.
|
||||
const initialFormValues: UserFormValues | undefined = initialValues
|
||||
? ({
|
||||
...initialValues,
|
||||
password: "",
|
||||
} as UserFormValues)
|
||||
: undefined
|
||||
|
||||
// Department state used to save all departments
|
||||
const [departments, setDepartments] = useState<Department[]>([])
|
||||
// Position state used to save all positions
|
||||
const [positions, setPositions] = useState<Position[]>([])
|
||||
|
||||
// Departments options
|
||||
const departmentOptions = useMemo(() => {
|
||||
return transformDepartmentToTree(departments)
|
||||
}, [departments])
|
||||
|
||||
const positionOptions = useMemo(() => {
|
||||
return positions.map((position) => ({
|
||||
label: position.name,
|
||||
value: position.id,
|
||||
key: position.id,
|
||||
}))
|
||||
}, [positions])
|
||||
|
||||
// Initialise form values
|
||||
useEffect(() => {
|
||||
if (initialFormValues) {
|
||||
form.setFieldsValue(initialFormValues)
|
||||
} else {
|
||||
form.resetFields()
|
||||
}
|
||||
}, [initialValues, form])
|
||||
|
||||
// Initialise department data on component mounted
|
||||
useEffect(() => {
|
||||
const fetchDepartmentsFuture = DeptApi.fetchDepartments()
|
||||
const fetchPositionsFuture = PositionApi.fetchPositions({ pageNum: 1, pageSize: 999 })
|
||||
|
||||
Promise.all([fetchDepartmentsFuture, fetchPositionsFuture])
|
||||
.then(([departments, positionPage]) => {
|
||||
setDepartments(departments)
|
||||
setPositions(positionPage.content)
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const err = error as AxiosError<GeneralErrorResponse>
|
||||
void message.error(err.response?.data.message ?? "获取部门或岗位数据失败,请稍后再试")
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Form<UserFormValues>
|
||||
form={form}
|
||||
initialValues={initialFormValues}
|
||||
layout="vertical"
|
||||
labelAlign="right"
|
||||
disabled={!isEditing}>
|
||||
<Form.Item<UserFormValues> label="用户 ID" name="id" hidden={isAdding}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UserFormValues>
|
||||
label="全名"
|
||||
name="fullName"
|
||||
rules={[{ required: true, message: "用户全名不能为空" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UserFormValues>
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[{ required: true, message: "请输入用户名" }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{isAdding && (
|
||||
<Form.Item<UserFormValues>
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{ required: true, message: "请输入密码" }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item<UserFormValues>
|
||||
label="电子邮箱"
|
||||
name="email"
|
||||
rules={[
|
||||
{ type: "email", message: "邮箱格式不正确" },
|
||||
{ required: true, message: "邮箱不能为空" },
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UserFormValues> label="联系电话" required>
|
||||
<Space.Compact className="w-full">
|
||||
<Form.Item<UserFormValues>
|
||||
noStyle
|
||||
name="regionAbbreviation"
|
||||
rules={[{ required: true, message: "请选择国际电信区域码" }]}>
|
||||
<Select
|
||||
options={countryOptions}
|
||||
showSearch
|
||||
placeholder="国家/地区"
|
||||
className="w-[26%]"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<UserFormValues>
|
||||
noStyle
|
||||
name="phoneNumber"
|
||||
rules={[{ required: true, message: "请输入电话号码" }]}>
|
||||
<Input className="w-[74%]" />
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UserFormValues>
|
||||
label="用户状态"
|
||||
name="status"
|
||||
rules={[{ required: true, message: "用户状态不能为空" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "已启用", value: "ACTIVE" },
|
||||
{ label: "已停用", value: "INACTIVE" },
|
||||
{ label: "已禁用", value: "LOCKED" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UserFormValues> label="所在部门" name="departmentId">
|
||||
<TreeSelect treeData={departmentOptions} placeholder="选择部门" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<UserFormValues> label="岗位" name="positionId">
|
||||
<Select options={positionOptions} placeholder="选择岗位" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 配置插件
|
||||
import dayjs from "dayjs"
|
||||
|
||||
import duration from "dayjs/plugin/duration"
|
||||
dayjs.extend(duration)
|
||||
|
||||
// 配置语言
|
||||
import "dayjs/locale/zh-cn"
|
||||
dayjs.locale("zh-cn")
|
||||
|
||||
console.log("Global Dayjs plugins initialised.")
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type Configuration, PublicClientApplication } from "@azure/msal-browser"
|
||||
|
||||
const clientId = import.meta.env.VITE_MSAL_CLIENT_ID
|
||||
const tenantId = import.meta.env.VITE_MSAL_TENANT_ID
|
||||
|
||||
const msalConfig: Configuration = {
|
||||
auth: {
|
||||
clientId,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
redirectUri: "http://localhost:5173"
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "localStorage",
|
||||
}
|
||||
}
|
||||
|
||||
export const msalInstance = new PublicClientApplication(msalConfig)
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* HTTP 状态码
|
||||
*/
|
||||
export const HttpStatus = {
|
||||
CONTINUE: 100,
|
||||
SWITCHING_PROTOCOLS: 101,
|
||||
PROCESSING: 102,
|
||||
EARLY_HINTS: 103,
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
ACCEPTED: 202,
|
||||
NON_AUTHORITATIVE_INFORMATION: 203,
|
||||
NO_CONTENT: 204,
|
||||
RESET_CONTENT: 205,
|
||||
PARTIAL_CONTENT: 206,
|
||||
MULTI_STATUS: 207,
|
||||
ALREADY_REPORTED: 208,
|
||||
IM_USED: 226,
|
||||
MULTIPLE_CHOICES: 300,
|
||||
MOVED_PERMANENTLY: 301,
|
||||
FOUND: 302,
|
||||
SEE_OTHER: 303,
|
||||
NOT_MODIFIED: 304,
|
||||
USE_PROXY: 305,
|
||||
TEMPORARY_REDIRECT: 307,
|
||||
PERMANENT_REDIRECT: 308,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
PAYMENT_REQUIRED: 402,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
METHOD_NOT_ALLOWED: 405,
|
||||
NOT_ACCEPTABLE: 406,
|
||||
PROXY_AUTHENTICATION_REQUIRED: 407,
|
||||
REQUEST_TIMEOUT: 408,
|
||||
CONFLICT: 409,
|
||||
GONE: 410,
|
||||
LENGTH_REQUIRED: 411,
|
||||
PRECONDITION_FAILED: 412,
|
||||
PAYLOAD_TOO_LARGE: 413,
|
||||
URI_TOO_LONG: 414,
|
||||
UNSUPPORTED_MEDIA_TYPE: 415,
|
||||
RANGE_NOT_SATISFIABLE: 416,
|
||||
EXPECTATION_FAILED: 417,
|
||||
I_AM_A_TEAPOT: 418,
|
||||
UNPROCESSABLE_ENTITY: 422,
|
||||
LOCKED: 423,
|
||||
FAILED_DEPENDENCY: 424,
|
||||
TOO_EARLY: 425,
|
||||
UPGRADE_REQUIRED: 426,
|
||||
PRECONDITION_REQUIRED: 428,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
NOT_IMPLEMENTED: 501,
|
||||
BAD_GATEWAY: 502,
|
||||
SERVICE_UNAVAILABLE: 503,
|
||||
GATEWAY_TIMEOUT: 504,
|
||||
HTTP_VERSION_NOT_SUPPORTED: 505,
|
||||
VARIANT_ALSO_NEGOTIATES: 506,
|
||||
INSUFFICIENT_STORAGE: 507,
|
||||
LOOP_DETECTED: 508,
|
||||
NOT_EXTENDED: 510,
|
||||
NETWORK_AUTHENTICATION_REQUIRED: 511,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { HttpStatus } from "./http-status"
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useMatches } from "react-router"
|
||||
import type { BreadcrumbItemType } from "antd/es/breadcrumb/Breadcrumb"
|
||||
import type { RouteHandle } from "@/types/route"
|
||||
|
||||
export function useAntBreadcrumbs(): BreadcrumbItemType[] {
|
||||
const matches = useMatches()
|
||||
|
||||
return matches
|
||||
.filter((match) => (match.handle as RouteHandle)?.label)
|
||||
.map((match) => {
|
||||
const handle = match.handle as RouteHandle
|
||||
|
||||
const path = match.pathname
|
||||
const title = handle.label || ""
|
||||
|
||||
return {
|
||||
title,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@layer theme, base, antd, components, utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router"
|
||||
import { Avatar, Breadcrumb, Dropdown, Layout, Menu, type MenuProps, Modal, 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 type { TreeNode } from "@/types/tree"
|
||||
import type { MenuItem } from "@/types/entity"
|
||||
|
||||
const { Header, Footer, Sider, Content } = Layout
|
||||
type AntMenuItem = Required<MenuProps>["items"][number]
|
||||
|
||||
function transformMenuData(nodes: TreeNode<MenuItem>[]): AntMenuItem[] {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return nodes
|
||||
.sort((a, b) => a.item.sort - b.item.sort)
|
||||
.map((node) => {
|
||||
const { item, children } = node
|
||||
const hasChildren = children && children.length > 0
|
||||
|
||||
const menuItem: AntMenuItem = {
|
||||
key: item.code,
|
||||
label: item.name,
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
// Append children
|
||||
return { ...menuItem, children: transformMenuData(children) }
|
||||
}
|
||||
|
||||
return menuItem
|
||||
})
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const user = useAppSelector((store) => store.auth.user!)
|
||||
const dispatch = useAppDispatch()
|
||||
const breadcrumbItems = useAntBreadcrumbs()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const onLogout = ({ key }: { key: string }) => {
|
||||
if (key == "logout") {
|
||||
Modal.confirm({
|
||||
title: "确定要注销吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
dispatch(logout())
|
||||
void navigate("/login")
|
||||
},
|
||||
maskClosable: false,
|
||||
keyboard: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const [menuItems, setMenuItems] = useState<AntMenuItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
MenuApi.fetchMenuTree()
|
||||
.then((response) => {
|
||||
setMenuItems(transformMenuData(response))
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const err = error as AxiosError
|
||||
console.log(err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const dropDownMenuItems: MenuProps["items"] = [
|
||||
{
|
||||
key: "logout",
|
||||
danger: true,
|
||||
label: "注销",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Layout className="h-[100%]">
|
||||
<Header className="flex items-center justify-between bg-gradient-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>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 items-center">
|
||||
<Dropdown
|
||||
className="text-white text-xl"
|
||||
menu={{ items: dropDownMenuItems, onClick: onLogout }}>
|
||||
<Space>
|
||||
{user.fullName}
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
<Avatar src={user.avatarUrl} alt="用户头像" />
|
||||
</div>
|
||||
</Header>
|
||||
<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 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>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { Provider as ReduxProvider } from "react-redux"
|
||||
import { PersistGate } from "redux-persist/integration/react"
|
||||
import { RouterProvider } from "react-router"
|
||||
import { App as AntApp, ConfigProvider as AntConfigProvider } from "antd"
|
||||
import { StyleProvider } from "@ant-design/cssinjs"
|
||||
import AntSimplifiedChinese from "antd/locale/zh_CN"
|
||||
import "./index.css"
|
||||
import "@/config/dayjs-config"
|
||||
import store, { persistor } from "@/store"
|
||||
import router from "@/router"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ReduxProvider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<StyleProvider layer>
|
||||
<AntConfigProvider
|
||||
locale={AntSimplifiedChinese}
|
||||
button={{
|
||||
autoInsertSpace: false,
|
||||
}}>
|
||||
<AntApp className="h-full w-full">
|
||||
<RouterProvider router={router} />
|
||||
</AntApp>
|
||||
</AntConfigProvider>
|
||||
</StyleProvider>
|
||||
</PersistGate>
|
||||
</ReduxProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
@@ -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 } from "antd"
|
||||
import dayjs from "dayjs"
|
||||
import type { AxiosError } from "axios"
|
||||
// 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 { 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"
|
||||
|
||||
export default function LoginPage() {
|
||||
const registrationEnabled = useAppSelector((state) => state.auth.registrationEnabled)
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// const msalContext = useMsal()
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
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) {
|
||||
dispatch(
|
||||
loginSuccess({
|
||||
user: loginResponse.user,
|
||||
token: loginResponse.accessToken,
|
||||
})
|
||||
)
|
||||
messageApi.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(
|
||||
error.response?.data.message ?? "登录失败,请稍后再试",
|
||||
dayjs.duration({ seconds: 3 }).asSeconds()
|
||||
)
|
||||
}
|
||||
},
|
||||
[dispatch, navigate, messageApi]
|
||||
)
|
||||
|
||||
/**
|
||||
* 刷新验证码图片
|
||||
*/
|
||||
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-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>
|
||||
|
||||
<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-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">
|
||||
登录
|
||||
</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,190 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import 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 {
|
||||
DeleteOutlined,
|
||||
ExportOutlined,
|
||||
ImportOutlined,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UndoOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import type { QueryRoleForm } from "@/types/form"
|
||||
import type { Status } from "@/types/constant"
|
||||
|
||||
export default function RolePage() {
|
||||
const { message } = App.useApp()
|
||||
|
||||
const [queryForm] = Form.useForm<QueryRoleForm>()
|
||||
const [roles, setRoles] = useState<Role[]>([])
|
||||
const [pageNum, setPageNum] = useState<number>(1)
|
||||
const [pageSize, setPageSize] = useState<number>(10)
|
||||
const [totalElementCount, setTotalElementCount] = useState<number>(0)
|
||||
|
||||
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) => {
|
||||
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))
|
||||
})
|
||||
.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))
|
||||
)
|
||||
}
|
||||
|
||||
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.Compact>
|
||||
<Button color="primary" variant="solid" htmlType="submit" icon={<SearchOutlined />}>
|
||||
查询
|
||||
</Button>
|
||||
<Button color="orange" variant="solid" htmlType="reset" icon={<UndoOutlined />}>
|
||||
重置
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Space size={8}>
|
||||
<Button variant="solid" type="primary" icon={<PlusOutlined />} onClick={() => {}}>
|
||||
新增
|
||||
</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">修改</Button>
|
||||
<Button variant="solid" danger>
|
||||
删除
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ComponentType } from "react"
|
||||
import { createBrowserRouter } from "react-router-dom"
|
||||
import ErrorPage from "@/page/error"
|
||||
import LoadingFallback from "@/components/loading-fallback"
|
||||
|
||||
/**
|
||||
* 懒加载组件
|
||||
* @param importer
|
||||
*/
|
||||
function lazyLoading<T extends { default: ComponentType<unknown> }>(importer: () => Promise<T>) {
|
||||
return async () => {
|
||||
const module = await importer()
|
||||
return {
|
||||
Component: module.default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/login",
|
||||
lazy: lazyLoading(() => import("@/page/login")),
|
||||
handle: {
|
||||
label: "用户登录",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
lazy: lazyLoading(() => import("@/page/register")),
|
||||
handle: {
|
||||
label: "用户注册",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
lazy: lazyLoading(() => import("@/components/protected-route")),
|
||||
errorElement: <ErrorPage />,
|
||||
handle: {
|
||||
label: "控制台",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: lazyLoading(() => import("@/page/home")),
|
||||
handle: {
|
||||
label: "用户主页",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
lazy: lazyLoading(() => import("@/page/user")),
|
||||
handle: {
|
||||
label: "用户管理",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
{
|
||||
path: "roles",
|
||||
lazy: lazyLoading(() => import("@/page/role")),
|
||||
handle: {
|
||||
label: "角色管理",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
{
|
||||
path: "menus",
|
||||
lazy: lazyLoading(() => import("@/page/menu")),
|
||||
handle: {
|
||||
label: "菜单管理",
|
||||
},
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
lazy: lazyLoading(() => import("@/page/not-found")),
|
||||
hydrateFallbackElement: <LoadingFallback />,
|
||||
},
|
||||
])
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { IPublicClientApplication } from "@azure/msal-browser"
|
||||
import * as AuthApi from "@/api/auth"
|
||||
import type { AppDispatch } from "@/store"
|
||||
import { loginSuccess } from "@/store/auth-slice"
|
||||
|
||||
/**
|
||||
* Login with Microsoft Entra ID.
|
||||
*
|
||||
* @param instance Microsoft Entra ID application instance
|
||||
* @param dispatch app dispatcher
|
||||
* @param onSuccess callback when login succeeded
|
||||
*/
|
||||
export async function doMsalLogin(
|
||||
instance: IPublicClientApplication,
|
||||
dispatch: AppDispatch,
|
||||
onSuccess?: () => void
|
||||
) {
|
||||
try {
|
||||
const response = await instance.loginPopup({
|
||||
scopes: ["openid", "profile", "email"],
|
||||
})
|
||||
|
||||
const { accessToken, user } = await AuthApi.msalLogin(response.idToken)
|
||||
dispatch(loginSuccess({ user, token: accessToken }))
|
||||
if (onSuccess) onSuccess()
|
||||
} catch (err) {
|
||||
console.error("MSAL login failed", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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"
|
||||
|
||||
const webClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(),
|
||||
})
|
||||
|
||||
webClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const state = store.getState()
|
||||
if (state.auth.isAuthenticated) {
|
||||
config.headers["Authorization"] = `Bearer ${state.auth.token!}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error: unknown) => {
|
||||
console.log(error)
|
||||
return Promise.reject(new Error("请求错误,请稍后再试"))
|
||||
}
|
||||
)
|
||||
|
||||
webClient.interceptors.response.use((response) => {
|
||||
return response
|
||||
}, (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,46 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
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
|
||||
}
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
loginSuccess(
|
||||
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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const { loginSuccess, logout, updateRegistrationEnabled } = authSlice.actions
|
||||
export default authSlice.reducer
|
||||
@@ -0,0 +1,47 @@
|
||||
import { configureStore, combineReducers } from "@reduxjs/toolkit"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import authReducer from "./auth-slice"
|
||||
import {
|
||||
persistStore,
|
||||
persistReducer,
|
||||
FLUSH,
|
||||
REHYDRATE,
|
||||
PAUSE,
|
||||
PERSIST,
|
||||
PURGE,
|
||||
REGISTER,
|
||||
} from "redux-persist"
|
||||
import storage from "redux-persist/lib/storage/session"
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
whitelist: ["auth"],
|
||||
// blacklist: ['department'],
|
||||
}
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
})
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
export const persistor = persistStore(store)
|
||||
|
||||
export default store
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type AppStore = typeof store
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { GetProps } from "antd"
|
||||
import type Icon from "@ant-design/icons"
|
||||
|
||||
export type AntIconComponentProps = GetProps<typeof Icon>
|
||||
|
||||
export interface AntTreeSelectOption<T> {
|
||||
value: T
|
||||
label: string
|
||||
children: AntTreeSelectOption<T>[]
|
||||
disabled?: boolean
|
||||
disableCheckbox?: boolean
|
||||
selectable?: boolean
|
||||
checkable?: boolean
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* WeCom config
|
||||
*/
|
||||
export type WecomConfig = {
|
||||
/**
|
||||
* Corporation ID
|
||||
*/
|
||||
corpId: string
|
||||
|
||||
/**
|
||||
* Application ID
|
||||
*/
|
||||
agentId: string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
export type Status = "ACTIVE" | "INACTIVE"
|
||||
|
||||
export type UserStatus = Status | "LOCKED"
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Status, UserStatus } from "@/types/constant"
|
||||
import type { CountryCode as RegionAbbreviation } from "libphonenumber-js"
|
||||
import type { Dayjs } from "dayjs"
|
||||
|
||||
/**
|
||||
* User information
|
||||
*/
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
password: string
|
||||
fullName: string
|
||||
email: string
|
||||
regionAbbreviation: RegionAbbreviation
|
||||
phoneNumber: string
|
||||
avatarUrl: string
|
||||
status: UserStatus
|
||||
departmentId: number
|
||||
positionId: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu Item
|
||||
*/
|
||||
export interface MenuItem {
|
||||
id: number
|
||||
name: string
|
||||
parentId: number | null
|
||||
code: string
|
||||
sort: number
|
||||
isExternalLink: boolean
|
||||
isVisible: boolean
|
||||
status: Status
|
||||
authorityCode: string | null
|
||||
icon: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Department {
|
||||
id: number
|
||||
name: string
|
||||
parentId: number | null
|
||||
sort: number
|
||||
status: Status
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
id: number
|
||||
name: string
|
||||
code: string | null
|
||||
description: string | null
|
||||
sort: number
|
||||
status: Status
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: number | string
|
||||
name: string
|
||||
code: string
|
||||
sort: number
|
||||
defaultValue: boolean
|
||||
description: string | null
|
||||
status: Status
|
||||
createdAt: Dayjs | string
|
||||
updatedAt: Dayjs | string
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { User } from "@/types/entity"
|
||||
import type { CountryCode as RegionAbbreviation } from "libphonenumber-js"
|
||||
import type { Status } from "@/types/constant"
|
||||
import type { Dayjs } from "dayjs"
|
||||
|
||||
export interface UserFormValues extends Omit<
|
||||
User,
|
||||
"id" | "password" | "regionAbbreviation" | "departmentId" | "positionId" | "createdAt" | "updatedAt"
|
||||
> {
|
||||
id: number | string | null
|
||||
password: string | null
|
||||
regionAbbreviation: RegionAbbreviation | null
|
||||
departmentId: number | null
|
||||
positionId: number | null
|
||||
}
|
||||
|
||||
export interface QueryRoleForm {
|
||||
qName: string | null
|
||||
qCode: string | null
|
||||
qStatus: Status | null
|
||||
}
|
||||
|
||||
export interface QueryUserForm {
|
||||
qDepartmentId: number | null
|
||||
qUsername: string | null
|
||||
qRegionAbbreviation: RegionAbbreviation | null
|
||||
qPhoneNumber: string | null
|
||||
qStatus: Status | null
|
||||
qCreatedAtStart: Dayjs | null
|
||||
qCreatedAtEnd: Dayjs | null
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface Sortable {
|
||||
empty: boolean
|
||||
sorted: boolean
|
||||
unsorted: boolean
|
||||
}
|
||||
|
||||
export interface Pageable {
|
||||
pageNumber: number
|
||||
pageSize: number
|
||||
sort: Sortable
|
||||
offset: number
|
||||
paged: boolean
|
||||
unpaged: boolean
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* React Router Metadata
|
||||
*/
|
||||
export interface RouteHandle {
|
||||
label: string
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Represents a single node within a tree structure.
|
||||
*
|
||||
* Each node holds a data item of type T and an array of its children.
|
||||
*
|
||||
* @template T the type of the data item stored in the node
|
||||
*/
|
||||
export interface TreeNode<T> {
|
||||
/**
|
||||
* The actual data item contained within this node.
|
||||
*/
|
||||
item: T
|
||||
/**
|
||||
* A list of child nodes belonging to this node.
|
||||
*
|
||||
* This array will be empty if the node is a leaf node.
|
||||
*/
|
||||
children: TreeNode<T>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a tree structure, which is simply a top-level tree node.
|
||||
*
|
||||
* It represents the root of the hierarchical data structure.
|
||||
*
|
||||
* @template T the type of the data items within the tree
|
||||
*/
|
||||
export type Tree<T> = TreeNode<T>
|
||||
|
||||
/**
|
||||
* Defines a forest, which is an array of zero or more disjoint trees.
|
||||
*
|
||||
* Each element in the array is the root of an independent tree.
|
||||
*
|
||||
* @template T the type of the data items within the trees
|
||||
*/
|
||||
export type Forest<T> = TreeNode<T>[]
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { CountryCode as RegionAbbreviation } from "libphonenumber-js"
|
||||
import type { Status, UserStatus } from "@/types/constant"
|
||||
import type { User } from "@/types/entity"
|
||||
|
||||
export interface PageRequest {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface UsernamePasswordLoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
captcha?: string
|
||||
uuid?: string
|
||||
}
|
||||
|
||||
export interface QueryUserRequest extends PageRequest {
|
||||
departmentId: number | null
|
||||
username: string | null
|
||||
regionAbbreviation: RegionAbbreviation | null
|
||||
phoneNumber: string | null
|
||||
status: Status | null
|
||||
createdAtStart: string | null
|
||||
createdAtEnd: string | null
|
||||
}
|
||||
|
||||
export interface QueryPositionRequest extends PageRequest {
|
||||
}
|
||||
|
||||
export interface AddUserRequest extends Omit<User, "id" | "createdAt" | "updatedAt">{
|
||||
roleIds: number[] | null
|
||||
}
|
||||
|
||||
export interface EditUserRequest {
|
||||
id: number | string
|
||||
username: string | null
|
||||
fullName: string | null
|
||||
email: string | null
|
||||
regionAbbreviation: string | null
|
||||
phoneNumber: string | null
|
||||
avatarUrl: string | null
|
||||
status: UserStatus | null
|
||||
departmentId: number | null
|
||||
positionId: number | null
|
||||
roleIds: number[] | null
|
||||
}
|
||||
|
||||
export interface QueryRoleRequest extends PageRequest {
|
||||
name: string | null
|
||||
code: string | null
|
||||
status: Status | null
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Role, User } from "@/types/entity"
|
||||
import type { Pageable, Sortable } from "@/types/page"
|
||||
|
||||
export interface PageResponse<T> {
|
||||
content: T[]
|
||||
last: boolean
|
||||
totalPages: number
|
||||
totalElements: number
|
||||
size: number
|
||||
sort: Sortable
|
||||
first: boolean
|
||||
numberOfElements: number
|
||||
empty: boolean,
|
||||
pageable: Pageable
|
||||
}
|
||||
|
||||
export interface UserAuthResponse {
|
||||
user: User
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface UserDetailResponse extends User {
|
||||
departmentName: string
|
||||
positionName: string
|
||||
}
|
||||
|
||||
export interface CaptchaResponse {
|
||||
uuid: string
|
||||
captcha: string
|
||||
}
|
||||
|
||||
export interface GeneralErrorResponse {
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface RoleResponse extends PageResponse<Role> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type TreeDataNode } from "antd"
|
||||
import type { TreeNode } from "@/types/tree"
|
||||
import type { Department } from "@/types/entity"
|
||||
|
||||
export function transformDepartmentData(departments: TreeNode<Department>[]): TreeDataNode[] {
|
||||
if (!departments || departments.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return departments
|
||||
.sort((a, b) => a.item.sort - b.item.sort)
|
||||
.map((node) => {
|
||||
const { item, children } = node
|
||||
const hasChildren = children && children.length > 0
|
||||
|
||||
const treeItem: TreeDataNode = {
|
||||
key: item.id,
|
||||
title: item.name,
|
||||
}
|
||||
|
||||
if (hasChildren) {
|
||||
// Append children
|
||||
return { ...treeItem, children: transformDepartmentData(children) }
|
||||
}
|
||||
|
||||
return treeItem
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * as PhoneNumberUtils from "./phone-number-utils"
|
||||
export * as DepartmentUtils from "./department-utils"
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type CountryCode as RegionAbbreviation, getCountryCallingCode, parsePhoneNumberWithError, PhoneNumber } from "libphonenumber-js"
|
||||
|
||||
/**
|
||||
* Format user's phone number as user's country/region.
|
||||
*
|
||||
* @param regionAbbreviation user's region abbreviation
|
||||
* @param phoneNumber user's phone number
|
||||
* @return formatted phone number
|
||||
*/
|
||||
export function formatInternationalPhoneNumber(regionAbbreviation: string, phoneNumber: string): string {
|
||||
try {
|
||||
const _phoneNumber = parsePhoneNumberWithError(phoneNumber, regionAbbreviation as RegionAbbreviation)
|
||||
|
||||
if (!_phoneNumber.isValid()) {
|
||||
console.warn(`Phone number ${_phoneNumber.formatInternational()} is not valid`)
|
||||
}
|
||||
return _phoneNumber.formatInternational()
|
||||
} catch (error) {
|
||||
console.error("Phone number parsing failed:", error)
|
||||
const _regionAbbreviation: RegionAbbreviation = regionAbbreviation as RegionAbbreviation
|
||||
const callingCode = getCountryCallingCode(_regionAbbreviation)
|
||||
return `+${callingCode} ${phoneNumber}`
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultCountryCode(): RegionAbbreviation {
|
||||
return import.meta.env.VITE_DEFAULT_REGION_CODE
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
||||
import type { CountryCode as RegionAbbreviation } from "libphonenumber-js"
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/**
|
||||
* Server Base URL
|
||||
*/
|
||||
readonly VITE_API_BASE_URL: string
|
||||
|
||||
/**
|
||||
* Microsoft Entra ID Client ID
|
||||
*/
|
||||
readonly VITE_MSAL_CLIENT_ID: string
|
||||
|
||||
/**
|
||||
* Microsoft Entra ID Tenant ID
|
||||
*/
|
||||
readonly VITE_MSAL_TENANT_ID: string
|
||||
|
||||
readonly VITE_DEFAULT_REGION_ABBREVIATION: RegionAbbreviation
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||