mirror of
https://github.com/wireadmin/wireadmin
synced 2025-04-19 05:34:53 +00:00
updates ui and fixes a few minor issues
This commit is contained in:
parent
0b18182103
commit
34050ed173
@ -4,13 +4,16 @@ import PageRouter from "@ui/pages/PageRouter";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { APIResponse } from "@lib/typings";
|
import { APIResponse, WgServer } from "@lib/typings";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { MiddleEllipsis } from "@ui/MiddleEllipsis";
|
import { MiddleEllipsis } from "@ui/MiddleEllipsis";
|
||||||
import StatusBadge from "@ui/StatusBadge";
|
import StatusBadge from "@ui/StatusBadge";
|
||||||
import { SmartModalRef } from "@ui/Modal/SmartModal";
|
import { SmartModalRef } from "@ui/Modal/SmartModal";
|
||||||
import CreateClientModal from "@ui/Modal/CreateClientModal";
|
import CreateClientModal from "@ui/Modal/CreateClientModal";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import QRCodeModal from "@ui/Modal/QRCodeModal";
|
||||||
|
import { getPeerConf } from "@lib/wireguard-utils";
|
||||||
|
|
||||||
|
|
||||||
export async function getServerSideProps(context: any) {
|
export async function getServerSideProps(context: any) {
|
||||||
@ -31,7 +34,7 @@ export default function ServerPage(props: PageProps) {
|
|||||||
|
|
||||||
const createClientRef = React.useRef<SmartModalRef | null>(null)
|
const createClientRef = React.useRef<SmartModalRef | null>(null)
|
||||||
|
|
||||||
const { data, error, isLoading } = useSWR(
|
const { data, error, isLoading, mutate: refresh } = useSWR(
|
||||||
`/api/wireguard/${props.serverId}`,
|
`/api/wireguard/${props.serverId}`,
|
||||||
async (url: string) => {
|
async (url: string) => {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
@ -52,7 +55,9 @@ export default function ServerPage(props: PageProps) {
|
|||||||
|
|
||||||
const { isMutating: isChangingStatus, trigger: changeStatus } = useSWRMutation(
|
const { isMutating: isChangingStatus, trigger: changeStatus } = useSWRMutation(
|
||||||
`/api/wireguard/${props.serverId}`,
|
`/api/wireguard/${props.serverId}`,
|
||||||
async (url: string, { arg }: { arg: string }) => {
|
async (url: string, { arg }: {
|
||||||
|
arg: string
|
||||||
|
}) => {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: arg === 'remove' ? 'DELETE' : 'PUT',
|
method: arg === 'remove' ? 'DELETE' : 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@ -71,10 +76,8 @@ export default function ServerPage(props: PageProps) {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: async () => await refresh(),
|
||||||
},
|
onError: async () => await refresh(),
|
||||||
onError: () => {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -82,7 +85,11 @@ export default function ServerPage(props: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage>
|
<BasePage>
|
||||||
<CreateClientModal ref={createClientRef} />
|
<CreateClientModal
|
||||||
|
ref={createClientRef}
|
||||||
|
serverId={props.serverId}
|
||||||
|
refreshTrigger={() => refresh()}
|
||||||
|
/>
|
||||||
<PageRouter
|
<PageRouter
|
||||||
route={[
|
route={[
|
||||||
{ title: data ? data.name.toString() : 'LOADING...' }
|
{ title: data ? data.name.toString() : 'LOADING...' }
|
||||||
@ -158,8 +165,37 @@ export default function ServerPage(props: PageProps) {
|
|||||||
|
|
||||||
<Card
|
<Card
|
||||||
className={'[&>.ant-card-body]:p-0'}
|
className={'[&>.ant-card-body]:p-0'}
|
||||||
title={<span> Clients </span>}
|
title={(
|
||||||
|
<div className={'flex items-center justify-between'}>
|
||||||
|
<span> Clients </span>
|
||||||
|
|
||||||
|
{data && data.peers.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type={'primary'}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => createClientRef.current?.open()}
|
||||||
>
|
>
|
||||||
|
Add a client
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{data && data.peers.length > 0 ? (
|
||||||
|
<List>
|
||||||
|
{data.peers.map((s) => (
|
||||||
|
<Client
|
||||||
|
key={s.id}
|
||||||
|
{...s}
|
||||||
|
serverId={props.serverId}
|
||||||
|
listenPort={data?.listen}
|
||||||
|
refreshTrigger={() => refresh()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
<div className={'flex flex-col items-center justify-center gap-y-4 py-8'}>
|
<div className={'flex flex-col items-center justify-center gap-y-4 py-8'}>
|
||||||
<p className={'text-gray-400 text-md'}>
|
<p className={'text-gray-400 text-md'}>
|
||||||
There are no clients yet!
|
There are no clients yet!
|
||||||
@ -168,6 +204,7 @@ export default function ServerPage(props: PageProps) {
|
|||||||
Add a client
|
Add a client
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -175,6 +212,114 @@ export default function ServerPage(props: PageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Peer = WgServer['peers'][0]
|
||||||
|
|
||||||
|
interface ClientProps extends Peer {
|
||||||
|
serverId: string
|
||||||
|
listenPort: number
|
||||||
|
refreshTrigger: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function Client(props: ClientProps) {
|
||||||
|
|
||||||
|
const qrcodeRef = React.useRef<SmartModalRef | null>(null)
|
||||||
|
|
||||||
|
const [ conf, setConf ] = React.useState<string | null>(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setConf(getPeerConf({
|
||||||
|
...props,
|
||||||
|
port: props.listenPort
|
||||||
|
}))
|
||||||
|
console.log('conf', conf)
|
||||||
|
}, [ props ])
|
||||||
|
|
||||||
|
const { isMutating: removingClient, trigger: removeClient } = useSWRMutation(
|
||||||
|
`/api/wireguard/${props.serverId}/${props.id}`,
|
||||||
|
async (url: string,) => {
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
const data = await resp.json() as APIResponse<any>
|
||||||
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => props.refreshTrigger(),
|
||||||
|
onError: () => props.refreshTrigger()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item className={'flex items-center justify-between p-4'}>
|
||||||
|
<QRCodeModal ref={qrcodeRef} content={conf?.trim() || 'null'} />
|
||||||
|
<div className={'w-full grid grid-cols-12 items-center gap-x-2'}>
|
||||||
|
<div className={'col-span-1 rounded-full bg-gray-200 aspect-square'} />
|
||||||
|
<span className={'font-medium col-span-4'}> {props.name} </span>
|
||||||
|
</div>
|
||||||
|
<div className={'flex items-center justify-center gap-x-3'}>
|
||||||
|
{/* QRCode */}
|
||||||
|
<ClientBaseButton disabled={removingClient} onClick={() => {
|
||||||
|
qrcodeRef.current?.open()
|
||||||
|
}}>
|
||||||
|
<i className={'fal text-neutral-700 group-hover:text-primary fa-qrcode'} />
|
||||||
|
</ClientBaseButton>
|
||||||
|
|
||||||
|
{/* Download */}
|
||||||
|
<ClientBaseButton disabled={removingClient} onClick={() => {
|
||||||
|
if (!conf) {
|
||||||
|
console.error('conf is null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('conf', conf)
|
||||||
|
// create a blob
|
||||||
|
const blob = new Blob([ conf ], { type: 'text/plain' })
|
||||||
|
// create a link
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = window.URL.createObjectURL(blob)
|
||||||
|
link.download = `${props.name}.conf`
|
||||||
|
// click the link
|
||||||
|
link.click()
|
||||||
|
// remove the link
|
||||||
|
link.remove()
|
||||||
|
}}>
|
||||||
|
<i className={'fal text-neutral-700 group-hover:text-primary fa-download'} />
|
||||||
|
</ClientBaseButton>
|
||||||
|
|
||||||
|
{/* Remove */}
|
||||||
|
<ClientBaseButton loading={removingClient} onClick={() => removeClient()}>
|
||||||
|
<i className={'fal text-neutral-700 group-hover:text-primary text-lg fa-trash-can'} />
|
||||||
|
</ClientBaseButton>
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClientBaseButton(props: {
|
||||||
|
onClick: () => void
|
||||||
|
loading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'group flex items-center justify-center w-10 aspect-square rounded-md',
|
||||||
|
'bg-gray-200/80 hover:bg-gray-100',
|
||||||
|
'border border-transparent hover:border-primary',
|
||||||
|
'transition-colors duration-200 ease-in-out',
|
||||||
|
'cursor-pointer',
|
||||||
|
props.disabled && 'opacity-50 cursor-not-allowed',
|
||||||
|
props.loading && 'animate-pulse'
|
||||||
|
)}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Row(props: {
|
function Row(props: {
|
||||||
label: string
|
label: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -13,7 +13,9 @@ import CreateServerModal from "@ui/Modal/CreateServerModal";
|
|||||||
import StatusBadge from "@ui/StatusBadge";
|
import StatusBadge from "@ui/StatusBadge";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { data, error, isLoading } = useSWR('/api/wireguard/listServers', async (url: string) => {
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
|
'/api/wireguard/listServers',
|
||||||
|
async (url: string) => {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@ -23,7 +25,8 @@ export default function Home() {
|
|||||||
const data = await resp.json() as APIResponse<any>
|
const data = await resp.json() as APIResponse<any>
|
||||||
if (!data.ok) throw new Error('Server responded with error status')
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
return data.result
|
return data.result
|
||||||
})
|
}
|
||||||
|
)
|
||||||
const createServerRef = React.useRef<SmartModalRef | null>(null)
|
const createServerRef = React.useRef<SmartModalRef | null>(null)
|
||||||
return (
|
return (
|
||||||
<BasePage>
|
<BasePage>
|
||||||
@ -35,7 +38,7 @@ export default function Home() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</PageRouter>
|
</PageRouter>
|
||||||
<CreateServerModal ref={createServerRef} />
|
<CreateServerModal ref={createServerRef} refreshTrigger={() => mutate()} />
|
||||||
<div className={'space-y-4'}>
|
<div className={'space-y-4'}>
|
||||||
{error ? (
|
{error ? (
|
||||||
<Card className={'flex items-center justify-center p-4'}>
|
<Card className={'flex items-center justify-center p-4'}>
|
||||||
@ -51,7 +54,7 @@ export default function Home() {
|
|||||||
title={<span> Servers </span>}
|
title={<span> Servers </span>}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
{data.map((s) => <Server {...s} />)}
|
{data.map((s) => <Server key={s.id} {...s} />)}
|
||||||
</List>
|
</List>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
@ -19,8 +19,8 @@ export function MiddleEllipsis(props: MiddleEllipsisProps) {
|
|||||||
}, [ maxLength ])
|
}, [ maxLength ])
|
||||||
|
|
||||||
const [ left, right ] = React.useMemo(() => {
|
const [ left, right ] = React.useMemo(() => {
|
||||||
if (content.length <= maxLength) return [ content, '' ]
|
if (content?.length <= maxLength) return [ content, '' ]
|
||||||
return [ content.slice(0, leftL), content.slice(content.length - rightL) ]
|
return [ content.slice(0, leftL), content.slice(content?.length - rightL) ]
|
||||||
}, [ content, leftL, rightL ])
|
}, [ content, leftL, rightL ])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,13 +4,19 @@ import { Button, Form, Input, notification } from "antd";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIResponse } from "@lib/typings";
|
import { APIResponse } from "@lib/typings";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from "@lib/schemas/WireGuard";
|
import { NameSchema } from "@lib/schemas/WireGuard";
|
||||||
import { zodErrorMessage } from "@lib/zod";
|
import { zodErrorMessage } from "@lib/zod";
|
||||||
|
|
||||||
|
|
||||||
|
type CreateClientModalProps = {
|
||||||
|
serverId: string
|
||||||
|
refreshTrigger: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const CreateClientModal = React.forwardRef<
|
const CreateClientModal = React.forwardRef<
|
||||||
SmartModalRef,
|
SmartModalRef,
|
||||||
{}
|
CreateClientModalProps
|
||||||
>((_, ref) => {
|
>((props, ref) => {
|
||||||
|
|
||||||
|
|
||||||
const [ notificationApi, contextHolder ] = notification.useNotification()
|
const [ notificationApi, contextHolder ] = notification.useNotification()
|
||||||
@ -25,7 +31,7 @@ const CreateClientModal = React.forwardRef<
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { isMutating, trigger } = useSWRMutation(
|
const { isMutating, trigger } = useSWRMutation(
|
||||||
'/api/wireguard/createClient',
|
`/api/wireguard/${props.serverId}/createClient`,
|
||||||
async (url: string, { arg }: { arg: FormValues }) => {
|
async (url: string, { arg }: { arg: FormValues }) => {
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -35,11 +41,12 @@ const CreateClientModal = React.forwardRef<
|
|||||||
body: JSON.stringify(arg)
|
body: JSON.stringify(arg)
|
||||||
})
|
})
|
||||||
const data = await resp.json() as APIResponse<any>
|
const data = await resp.json() as APIResponse<any>
|
||||||
if (!data.ok) throw new Error('Client responded with error status')
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
return data.result
|
return true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
props.refreshTrigger()
|
||||||
notificationApi.success({
|
notificationApi.success({
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
description: (
|
description: (
|
||||||
@ -52,6 +59,7 @@ const CreateClientModal = React.forwardRef<
|
|||||||
form?.resetFields()
|
form?.resetFields()
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
props.refreshTrigger()
|
||||||
notificationApi.error({
|
notificationApi.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
description: 'Failed to create Client'
|
description: 'Failed to create Client'
|
||||||
@ -98,7 +106,7 @@ const CreateClientModal = React.forwardRef<
|
|||||||
<Input placeholder={'Unicorn 🦄'} />
|
<Input placeholder={'Unicorn 🦄'} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Button type={'primary'} htmlType={'submit'} className={'w-full'}>
|
<Button type={'primary'} htmlType={'submit'} className={'w-full'} loading={isMutating}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@ -110,12 +118,7 @@ const CreateClientModal = React.forwardRef<
|
|||||||
export default CreateClientModal
|
export default CreateClientModal
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
name: NameSchema,
|
name: NameSchema
|
||||||
address: AddressSchema,
|
|
||||||
port: PortSchema,
|
|
||||||
type: TypeSchema,
|
|
||||||
dns: DnsSchema,
|
|
||||||
mtu: MtuSchema
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type FormValues = z.infer<typeof FormSchema>
|
type FormValues = z.infer<typeof FormSchema>
|
||||||
|
@ -9,7 +9,9 @@ import { isPrivateIP } from "@lib/utils";
|
|||||||
import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from "@lib/schemas/WireGuard";
|
import { AddressSchema, DnsSchema, MtuSchema, NameSchema, PortSchema, TypeSchema } from "@lib/schemas/WireGuard";
|
||||||
import { zodErrorMessage } from "@lib/zod";
|
import { zodErrorMessage } from "@lib/zod";
|
||||||
|
|
||||||
export type CreateServerModalProps = {}
|
type CreateServerModalProps = {
|
||||||
|
refreshTrigger: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const CreateServerModal = React.forwardRef<
|
const CreateServerModal = React.forwardRef<
|
||||||
SmartModalRef,
|
SmartModalRef,
|
||||||
@ -42,6 +44,7 @@ const CreateServerModal = React.forwardRef<
|
|||||||
})
|
})
|
||||||
const data = await resp.json() as APIResponse<any>
|
const data = await resp.json() as APIResponse<any>
|
||||||
if (!data.ok) throw new Error('Server responded with error status')
|
if (!data.ok) throw new Error('Server responded with error status')
|
||||||
|
props.refreshTrigger()
|
||||||
return data.result
|
return data.result
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,7 +149,7 @@ const CreateServerModal = React.forwardRef<
|
|||||||
defaultValue={type}
|
defaultValue={type}
|
||||||
onChange={(v) => setType(v as any)}
|
onChange={(v) => setType(v as any)}
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Direct', value: 'default', icon: <i className={'fal fa-arrows-left-right-to-line'} /> },
|
{ label: 'Direct', value: 'direct', icon: <i className={'fal fa-arrows-left-right-to-line'} /> },
|
||||||
{ label: 'Tor', value: 'tor', icon: <TorOnion />, disabled: true }
|
{ label: 'Tor', value: 'tor', icon: <TorOnion />, disabled: true }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -156,7 +159,7 @@ const CreateServerModal = React.forwardRef<
|
|||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (!value) return Promise.resolve()
|
if (!value) return Promise.resolve()
|
||||||
const res = NameSchema.safeParse(value)
|
const res = DnsSchema.safeParse(value)
|
||||||
if (res.success) return Promise.resolve()
|
if (res.success) return Promise.resolve()
|
||||||
return Promise.reject(zodErrorMessage(res.error)[0])
|
return Promise.reject(zodErrorMessage(res.error)[0])
|
||||||
}
|
}
|
||||||
@ -169,7 +172,7 @@ const CreateServerModal = React.forwardRef<
|
|||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (!value) return Promise.resolve()
|
if (!value) return Promise.resolve()
|
||||||
const res = NameSchema.safeParse(value)
|
const res = MtuSchema.safeParse(value)
|
||||||
if (res.success) return Promise.resolve()
|
if (res.success) return Promise.resolve()
|
||||||
return Promise.reject(zodErrorMessage(res.error)[0])
|
return Promise.reject(zodErrorMessage(res.error)[0])
|
||||||
}
|
}
|
||||||
@ -178,7 +181,12 @@ const CreateServerModal = React.forwardRef<
|
|||||||
<Input placeholder={'1420'} />
|
<Input placeholder={'1420'} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Button type={'primary'} htmlType={'submit'} className={'w-full'}>
|
<Button
|
||||||
|
type={'primary'}
|
||||||
|
htmlType={'submit'}
|
||||||
|
className={'w-full'}
|
||||||
|
loading={isMutating}
|
||||||
|
>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
39
src/ui/Modal/QRCodeModal.tsx
Normal file
39
src/ui/Modal/QRCodeModal.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SmartModal, { SmartModalRef } from "@ui/Modal/SmartModal";
|
||||||
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
|
import { SHA1 } from "crypto-js";
|
||||||
|
|
||||||
|
type QRCodeModalProps = {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRCodeModal = React.forwardRef<
|
||||||
|
SmartModalRef,
|
||||||
|
QRCodeModalProps
|
||||||
|
>((props, ref) => {
|
||||||
|
|
||||||
|
const innerRef = React.useRef<SmartModalRef | null>(null)
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => innerRef.current as SmartModalRef)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SmartModal
|
||||||
|
key={SHA1(props.content || '').toString()}
|
||||||
|
ref={innerRef}
|
||||||
|
title={null}
|
||||||
|
footer={null}
|
||||||
|
className={'flex items-center justify-center'}
|
||||||
|
>
|
||||||
|
<div className={'flex items-center justify-center p-5'}>
|
||||||
|
<QRCodeCanvas
|
||||||
|
size={256}
|
||||||
|
level={'M'}
|
||||||
|
value={props.content || ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SmartModal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default QRCodeModal
|
||||||
|
|
Loading…
Reference in New Issue
Block a user