Skip to content

Commit

Permalink
feat: 数字签名
Browse files Browse the repository at this point in the history
  • Loading branch information
wkylin committed Dec 30, 2024
1 parent b5b06f5 commit 39c5817
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 2 deletions.
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
"@react-three/fiber": "^8.17.10",
"@reduxjs/toolkit": "^2.5.0",
"@smakss/react-scroll-direction": "^4.2.0",
"@uiw/react-signature": "^1.3.1",
"@use-gesture/react": "^10.3.1",
"animate.css": "^4.1.1",
"antd": "^5.22.5",
Expand Down
4 changes: 2 additions & 2 deletions src/components/hooks/useCopyToClipboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import useCopyToClipboard from './use-copy-clipboard'

import useCopy from './use-copy'

export default useCopy
export default useCopyToClipboard

export { useCopyToClipboard }
export { useCopy }
Empty file.
182 changes: 182 additions & 0 deletions src/components/stateless/ReactSignature/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use client'

Check failure on line 1 in src/components/stateless/ReactSignature/index.tsx

View workflow job for this annotation

GitHub Actions / Qodana for JS

ESLint

ESLint: Install the 'eslint' package
import clsx from 'clsx'
import Signature, { type SignatureRef } from '@uiw/react-signature'
import { CheckIcon, CopyIcon, DownloadIcon, Eraser, RefreshCcwIcon } from 'lucide-react'
import React, { type ComponentProps, useRef, useState } from 'react'
import { useCopy } from '@hooks/useCopyToClipboard'

export function ReactSignature({ className, ...props }: ComponentProps<typeof Signature>) {
const [readonly, setReadonly] = useState(false)
const $svg = useRef<SignatureRef>(null)

const handleClear = () => $svg.current?.clear()

const handleValidate = () => {
if (readonly) {
$svg.current?.clear()
setReadonly(false)
} else {
setReadonly(true)
}
}

return (
<div className="flex flex-col gap-2">
<p className="text-lg tracking-tight text-neutral-500">数字签名:</p>
<Signature
className={clsx(
'h-28 w-80 rounded-lg border border-neutral-500/20 bg-neutral-500/10',
readonly ? 'cursor-not-allowed fill-neutral-500' : 'fill-neutral-800 dark:fill-neutral-200',
className
)}
options={{
smoothing: 0,
streamline: 0.8,
thinning: 0.7,
}}
readonly={readonly}
{...props}
ref={$svg}
/>
<div className="flex justify-end gap-1 text-neutral-700 dark:text-neutral-200">
<ValidateButton onClick={handleValidate} readonly={readonly} />
{readonly && (
<>
<DownloadButton svgElement={$svg.current?.svg} />
<CopySvgButton svgElement={$svg.current?.svg} />
</>
)}
{!readonly && <ClearButton onClick={handleClear} />}
</div>
</div>
)
}

function prepareSvgElement(svgElement: SVGSVGElement) {
const svgElm = svgElement.cloneNode(true) as SVGSVGElement
const clientWidth = svgElement.clientWidth
const clientHeight = svgElement.clientHeight
svgElm.removeAttribute('style')
svgElm.setAttribute('width', `${clientWidth}px`)
svgElm.setAttribute('height', `${clientHeight}px`)
svgElm.setAttribute('viewBox', `0 0 ${clientWidth} ${clientHeight}`)
return { svgElm, clientWidth, clientHeight }
}

function ValidateButton({
readonly,
onClick,
}: Readonly<{
readonly: boolean
onClick: () => void
}>) {
return (
<button
className="inline-grid border rounded-md size-8 place-content-center border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
onClick={onClick}
type="button"
>
{readonly ? (
<>
<RefreshCcwIcon className="size-5" />
<span className="sr-only">Reset</span>
</>
) : (
<>
<CheckIcon className="size-5" />
<span className="sr-only">Validate</span>
</>
)}
</button>
)
}

function DownloadButton({
svgElement,
}: Readonly<{
svgElement: SVGSVGElement | undefined | null
}>) {
const handleDownloadImage = () => {
if (!svgElement) {
return
}

const { svgElm, clientWidth, clientHeight } = prepareSvgElement(svgElement)

const data = new XMLSerializer().serializeToString(svgElm)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
img.onload = () => {
canvas.width = clientWidth ?? 0
canvas.height = clientHeight ?? 0
ctx?.drawImage(img, 0, 0)
const a = document.createElement('a')
a.download = 'signature.png'
a.href = canvas.toDataURL('image/png')
a.click()
}
img.src = `data:image/svg+xml;base64,${window.btoa(decodeURIComponent(encodeURIComponent(data)))}`
}

return (
<button
className="inline-grid border rounded-md size-8 place-content-center border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
onClick={handleDownloadImage}
type="button"
>
<DownloadIcon className="size-5" />
<span className="sr-only">Download</span>
</button>
)
}

function CopySvgButton({
svgElement,
}: Readonly<{
svgElement: SVGSVGElement | undefined | null
}>) {
const [_, copyText, isCopied] = useCopy()

const handleCopySvg = () => {
if (!svgElement) {
return
}

const { svgElm } = prepareSvgElement(svgElement)
copyText(svgElm.outerHTML)

Check notice on line 147 in src/components/stateless/ReactSignature/index.tsx

View workflow job for this annotation

GitHub Actions / Qodana for JS

Result of method call returning a promise is ignored

Promise returned from copyText is ignored
}

return (
<button
className="inline-flex items-center gap-1 px-1 text-sm tracking-tight border rounded-md border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
onClick={handleCopySvg}
type="button"
>
{isCopied ? (
<>
<span>Copied</span>
<CheckIcon className="size-5" />
</>
) : (
<>
<span>Copy to SVG</span>
<CopyIcon className="size-5" />
</>
)}
</button>
)
}

function ClearButton({ onClick }: Readonly<{ onClick: () => void }>) {
return (
<button
className="inline-grid border rounded-md size-8 place-content-center border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
onClick={onClick}
type="button"
>
<Eraser className="size-5" />
<span className="sr-only">Clear</span>
</button>
)
}
4 changes: 4 additions & 0 deletions src/pages/home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import ShiCode from '@stateless/ShiCode'
import DynamicBackground from '@stateless/DynamicBackground'
import ContentPlaceholder from '@stateless/ContentPlaceholder'
import SkeletonFix from '@stateless/SkeletonFix'
import { ReactSignature } from '@stateless/ReactSignature'
import { oneApiChat, prettyObject, randomNum } from '@utils/aidFn'

Check warning on line 40 in src/pages/home/index.jsx

View workflow job for this annotation

GitHub Actions / Qodana for JS

Unused import

Unused import specifier randomNum
import { fireConfetti } from '@utils/confetti'

Expand Down Expand Up @@ -398,6 +399,9 @@ const Home = () => {
<section style={{ position: 'relative' }}>
<AnimateWave />
</section>
<section style={{ position: 'relative' }}>
<ReactSignature />
</section>
<section style={{ margin: 20 }}>
<SkeletonFix />
</section>
Expand Down

0 comments on commit 39c5817

Please sign in to comment.