feat: 批量转移服务器给其他用户

This commit is contained in:
naiba
2025-06-16 23:44:30 +08:00
parent bf7c42e4e7
commit 5f854c3dd0
6 changed files with 141 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
import { import {
ModelBatchMoveServerForm,
ModelServer, ModelServer,
ModelServerConfigForm, ModelServerConfigForm,
ModelServerForm, ModelServerForm,
@@ -15,6 +16,10 @@ export const deleteServer = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id) return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id)
} }
export const batchMoveServer = async (data: ModelBatchMoveServerForm): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-move/server", data)
}
export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => { export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
return fetcher<ModelServerTaskResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id) return fetcher<ModelServerTaskResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id)
} }

View File

@@ -0,0 +1,100 @@
import { batchMoveServer } from "@/api/server"
import { Button, ButtonProps } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area"
import { IconButton } from "@/components/xui/icon-button"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { Textarea } from "./ui/textarea"
interface BatchMoveServerIconProps extends ButtonProps {
serverIds: number[]
}
export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ serverIds, ...props }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [toUserId, setToUserId] = useState<number | undefined>(undefined)
const onSubmit = async () => {
try {
await batchMoveServer({
ids: serverIds,
to_user: toUserId!
})
} catch (e) {
console.error(e)
toast(t("Error"), {
description: t("Results.UnExpectedError"),
})
return
}
toast(t("Done"))
setOpen(false)
}
return serverIds.length < 1 ? (
<IconButton
{...props}
icon="user-pen"
onClick={() => {
toast(t("Error"), {
description: t("Results.NoRowsAreSelected"),
})
}}
/>
) : (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<IconButton {...props} icon="user-pen" />
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>{t("BatchMoveServer")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<div className="flex flex-col gap-3 mt-4">
<Label>{t("Servers")}</Label>
<Textarea disabled>
{serverIds.join(", ")}
</Textarea>
<Label>{t("ToUser")}</Label>
<Input
type="number"
placeholder="User ID"
value={toUserId}
onChange={(e) => {
setToUserId(parseInt(e.target.value, 10))
}}
/>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
{t("Cancel")}
</Button>
</DialogClose>
<Button disabled={!toUserId || toUserId == 0} type="submit" className="my-2" onClick={onSubmit}>
{t("Move")}
</Button>
</DialogFooter>
</div>
</div>
</ScrollArea>
</DialogContent>
</Dialog>
)
}

View File

@@ -16,6 +16,7 @@ import {
Terminal, Terminal,
Trash2, Trash2,
Upload, Upload,
UserPen,
} from "lucide-react" } from "lucide-react"
import { forwardRef } from "react" import { forwardRef } from "react"
@@ -37,6 +38,7 @@ export interface IconButtonProps extends ButtonProps {
| "expand" | "expand"
| "cog" | "cog"
| "minus" | "minus"
| "user-pen"
} }
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => { export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -97,6 +99,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "minus": { case "minus": {
return <Minus /> return <Minus />
} }
case "user-pen": {
return <UserPen />
}
} }
})()} })()}
</Button> </Button>

View File

@@ -1,6 +1,7 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api"
import { deleteServer, forceUpdateServer } from "@/api/server" import { deleteServer, forceUpdateServer } from "@/api/server"
import { ActionButtonGroup } from "@/components/action-button-group" import { ActionButtonGroup } from "@/components/action-button-group"
import { BatchMoveServerIcon } from "@/components/batch-move-server-icon"
import { CopyButton } from "@/components/copy-button" import { CopyButton } from "@/components/copy-button"
import { HeaderButtonGroup } from "@/components/header-button-group" import { HeaderButtonGroup } from "@/components/header-button-group"
import { InstallCommandsMenu } from "@/components/install-commands" import { InstallCommandsMenu } from "@/components/install-commands"
@@ -213,6 +214,7 @@ export default function ServerPage() {
}) })
}} }}
/> />
<BatchMoveServerIcon serverIds={selectedRows.map((r) => r.original.id)} />
<ServerConfigCardBatch <ServerConfigCardBatch
sid={selectedRows.map((r) => r.original.id)} sid={selectedRows.map((r) => r.original.id)}
className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg" className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg"

View File

@@ -74,6 +74,11 @@ export default function UserPage() {
return row.role === 1 ? t("User") : t("Admin") return row.role === 1 ? t("User") : t("Admin")
}, },
}, },
{
header: t("LastLogin"),
accessorKey: "updated_at",
accessorFn: (row) => row.updated_at ? new Date(row.updated_at).toLocaleString() : t("Never"),
},
{ {
id: "actions", id: "actions",
header: t("Actions"), header: t("Actions"),

View File

@@ -188,6 +188,11 @@ export interface ModelAlertRuleForm {
trigger_mode: number trigger_mode: number
} }
export interface ModelBatchMoveServerForm {
ids: number[]
to_user: number
}
export interface ModelCreateFMResponse { export interface ModelCreateFMResponse {
session_id: string session_id: string
} }
@@ -631,6 +636,8 @@ export interface ModelServiceResponseItem {
export interface ModelSetting { export interface ModelSetting {
admin_template: string admin_template: string
/** Agent真实IP */
agent_real_ip_header: string
/** 覆盖范围0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器; */ /** 覆盖范围0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器; */
cover: number cover: number
custom_code: string custom_code: string
@@ -648,17 +655,17 @@ export interface ModelSetting {
/** 系统语言,默认 zh_CN */ /** 系统语言,默认 zh_CN */
language: string language: string
oauth2_providers: string[] oauth2_providers: string[]
/** 前端真实IP */
web_real_ip_header: string
/** Agent真实IP */
agent_real_ip_header: string
site_name: string site_name: string
/** 用于前端判断生成的安装命令是否启用 TLS */ /** 用于前端判断生成的安装命令是否启用 TLS */
tls: boolean tls: boolean
user_template: string user_template: string
/** 前端真实IP */
web_real_ip_header: string
} }
export interface ModelSettingForm { export interface ModelSettingForm {
/** Agent真实IP */
agent_real_ip_header?: string
cover: number cover: number
custom_code?: string custom_code?: string
custom_code_dashboard?: string custom_code_dashboard?: string
@@ -671,14 +678,12 @@ export interface ModelSettingForm {
ip_change_notification_group_id: number ip_change_notification_group_id: number
/** @minLength 2 */ /** @minLength 2 */
language: string language: string
/** 前端真实IP */
web_real_ip_header?: string
/** Agent真实IP */
agent_real_ip_header?: string
/** @minLength 1 */ /** @minLength 1 */
site_name: string site_name: string
tls?: boolean tls?: boolean
user_template?: string user_template?: string
/** 前端真实IP */
web_real_ip_header?: string
} }
export interface ModelSettingResponse { export interface ModelSettingResponse {