Files
i-tools/app/qrcode/page.tsx
yfan 3d175d75af
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
first commit
2026-01-30 16:57:44 +08:00

340 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { QrCode, Download, Settings, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { QRCodeCanvas } from "qrcode.react";
import { toast } from "sonner";
interface QRConfig {
size: number;
icon: string;
iconSize: number;
fgColor: string;
bgColor: string;
includeMargin: boolean;
level: "L" | "M" | "Q" | "H";
}
export default function QRCodeGenerator() {
const [text, setText] = useState("");
const [config, setConfig] = useState<QRConfig>({
size: 200,
icon: "",
iconSize: 40,
fgColor: "#000000",
bgColor: "#ffffff",
includeMargin: true,
level: "M",
});
const handleConfigChange = (field: keyof QRConfig, value: any) => {
setConfig((prev) => ({ ...prev, [field]: value }));
};
const downloadQRCode = () => {
const canvas = document.getElementById("qr-code-canvas") as HTMLCanvasElement;
if (canvas) {
try {
const link = document.createElement("a");
link.download = "qrcode.png";
link.href = canvas.toDataURL("image/png");
link.click();
toast.success("二维码下载成功");
} catch {
toast.error("下载失败");
}
} else {
toast.error("未找到二维码");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-emerald-500 to-green-600 shadow-lg">
<QrCode className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Left: Configuration */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="text"></Label>
<Textarea
id="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="请输入要生成二维码的文本内容,如:网址、文本、微信号等..."
maxLength={2953}
className="min-h-25"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Settings className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="size"> (px)</Label>
<Input
id="size"
type="number"
value={config.size}
onChange={(e) =>
handleConfigChange(
"size",
parseInt(e.target.value) || 200
)
}
min={80}
max={500}
/>
</div>
<div className="space-y-2">
<Label htmlFor="iconSize"> (px)</Label>
<Input
id="iconSize"
type="number"
value={config.iconSize}
onChange={(e) =>
handleConfigChange(
"iconSize",
parseInt(e.target.value) || 40
)
}
min={20}
max={100}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="icon"> ()</Label>
<Input
id="icon"
value={config.icon}
onChange={(e) => handleConfigChange("icon", e.target.value)}
placeholder="https://example.com/logo.png"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="fgColor"></Label>
<div className="flex gap-2">
<Input
id="fgColor"
type="color"
value={config.fgColor}
onChange={(e) =>
handleConfigChange("fgColor", e.target.value)
}
className="h-9 w-full p-1 cursor-pointer"
/>
<Input
value={config.fgColor}
onChange={(e) => handleConfigChange("fgColor", e.target.value)}
className="font-mono"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bgColor"></Label>
<div className="flex gap-2">
<Input
id="bgColor"
type="color"
value={config.bgColor}
onChange={(e) =>
handleConfigChange("bgColor", e.target.value)
}
className="h-9 w-full p-1 cursor-pointer"
/>
<Input
value={config.bgColor}
onChange={(e) => handleConfigChange("bgColor", e.target.value)}
className="font-mono"
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={config.level}
onValueChange={(value: "L" | "M" | "Q" | "H") =>
handleConfigChange("level", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="L">L - (7%)</SelectItem>
<SelectItem value="M">M - (15%)</SelectItem>
<SelectItem value="Q">Q - (25%)</SelectItem>
<SelectItem value="H">H - (30%)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end pb-2">
<div className="flex items-center space-x-2">
<Switch
id="includeMargin"
checked={config.includeMargin}
onCheckedChange={(checked) =>
handleConfigChange("includeMargin", checked)
}
/>
<Label htmlFor="includeMargin"></Label>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Right: Preview */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Eye className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center min-h-80 bg-muted/30 rounded-lg p-8 border border-dashed">
{text ? (
<div className="flex flex-col items-center gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm">
<QRCodeCanvas
id="qr-code-canvas"
value={text}
size={config.size}
fgColor={config.fgColor}
bgColor={config.bgColor}
level={config.level}
includeMargin={config.includeMargin}
imageSettings={
config.icon
? {
src: config.icon,
height: config.iconSize,
width: config.iconSize,
excavate: true,
}
: undefined
}
/>
</div>
<div className="text-sm text-muted-foreground">
: {config.size}×{config.size}px
</div>
</div>
) : (
<div className="text-muted-foreground text-center">
<QrCode className="h-12 w-12 mx-auto mb-2 opacity-20" />
<p></p>
</div>
)}
</div>
</CardContent>
</Card>
{text && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<Button
onClick={downloadQRCode}
className="w-full h-12 text-lg gap-2"
>
<Download className="h-5 w-5" />
PNG
</Button>
</CardContent>
</Card>
)}
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm">使</h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li>使</li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}