完成分享功能

This commit is contained in:
ymk
2025-08-02 17:00:15 +08:00
parent aa1e2b105d
commit 377a6b7cb1
14 changed files with 283 additions and 119 deletions

View File

@@ -7,6 +7,8 @@ import PreviewToolbar from "./common/PreviewToolbar";
import { useState } from "react";
import { useTranslations } from "next-intl";
import TextSetting, { TextProp } from "./common/TextSetting";
import { useSearchParams } from "next/navigation";
import { decodeText } from "@/lib/utils";
/**
* 全特性工具栏
@@ -16,12 +18,16 @@ export default function Page({ textProp, backgroundProp }: { textProp: TextProp
const t = useTranslations("TextEditor");
const [background, setBackground] = useState<BackgroundProp>({
backgroundProp = backgroundProp || {
type: "color",
color: "#c4b1b1",
image: null,
});
const [text, setText] = useState<TextProp>(TextProp.default(t("defaultText")));
} satisfies BackgroundProp;
textProp = textProp || TextProp.default(t("defaultText"));
const [background, setBackground] = useState<BackgroundProp>(backgroundProp!);
const [text, setText] = useState<TextProp>(textProp!);
return (
<Flex gap={"2"}>

View File

@@ -14,39 +14,43 @@ import { useRouter } from "@/i18n/navigation";
* 简易工具
* @returns
*/
export default function Page({ textProp, backgroundProp }: { textProp: TextProp | undefined, backgroundProp: BackgroundProp | undefined }) {
export default function Page() {
const t = useTranslations("TextEditor");
const tIndex = useTranslations("HomePage");
const router = useRouter();
const [background, setBackground] = useState<BackgroundProp>(backgroundProp || {
let backgroundProp = {
type: "color",
color: "#c4b1b1",
image: null,
});
} satisfies BackgroundProp;
const [text, setText] = useState<TextProp>(textProp || TextProp.default(t("defaultText")));
let textProp = TextProp.default(t("defaultText"));
useEffect(() => {
let bg = sessionStorage.getItem("background");
const [background, setBackground] = useState<BackgroundProp>(backgroundProp);
if (bg) {
console.log("初始化设置 bg", bg);
const [text, setText] = useState<TextProp>(textProp);
setBackground(JSON.parse(bg));
}
// useEffect(() => {
// let bg = sessionStorage.getItem("background");
let txt = sessionStorage.getItem("text");
// if (bg) {
// console.log("初始化设置 bg", bg);
if (txt) {
console.log("初始化设置 txt", txt);
// setBackground(JSON.parse(bg));
// }
setText(JSON.parse(txt));
}
// let txt = sessionStorage.getItem("text");
}, []);
// if (txt) {
// console.log("初始化设置 txt", txt);
// setText(JSON.parse(txt));
// }
// }, []);
useEffect(() => {
sessionStorage.setItem("background", JSON.stringify(background));
@@ -65,12 +69,11 @@ export default function Page({ textProp, backgroundProp }: { textProp: TextProp
setBackground={setBackground}
/>
<SimpleTextSetting text={text} setText={setText} />
<Box className="text-center"> <Link className="cursor-pointer" underline="always" onClick={() => { router.push("/editor") }}>{tIndex("toolMore")} ?</Link> </Box>
</Flex>
<Flex className="w-2/3" direction={"column"} justify={"between"}>
<PreviewToolbar background={background} text={text} />
<Box className="text-center"> <Link onClick={() => { router.push("/editor") }}>{tIndex("toolMore")}?</Link> </Box>
</Flex>
</Flex>
);

View File

@@ -1,11 +1,12 @@
"use client";
import { useState, useRef, useEffect, } from "react";
import { useTranslations } from "next-intl";
import { Eye, Download } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import { Eye, Download, Share } from "lucide-react";
import { BackgroundProp } from "./BackgroundSelector";
import { Text, Flex } from "@radix-ui/themes";
import { Text, Flex, Button, Select, AlertDialog, Code, Blockquote, Box } from "@radix-ui/themes";
import { getPicture, resize, init as threeInit, updateBackground, updateTextProps } from "./ThreeTools";
import { TextProp } from "./TextSetting";
import { encodeText, getShareLink } from "@/lib/utils";
const Sizes = [
"1920x1080",
@@ -35,6 +36,9 @@ export default function PreviewToolbar({
const container = useRef<HTMLCanvasElement>(null);
const fullscreenElement = useRef<HTMLImageElement>(null);
const [picture, setPicture] = useState<string | null>(null);
const [shareError, setShareError] = useState<string | null>(null);
const [shareLink, setShareLink] = useState<string | null>(null);
const locale = useLocale();
const updateSize = () => {
const split = Sizes[aspectRadio].split("x").map(Number);
@@ -76,7 +80,7 @@ export default function PreviewToolbar({
updateTextProps(text);
console.log("text change", text);
}, 1000);
}, 200);
return () => clearTimeout(timeoutId);
}, [text]);
@@ -124,6 +128,35 @@ export default function PreviewToolbar({
};
const handleShare = () => {
setShareError(null);
setShareLink(null);
if (background.type == "image" && background.image) {
setShareError(t("shareErrorNotSupportDesc"));
return;
}
const bg = { ...background, image: null };
let txt = JSON.stringify({ bg, text });
txt = encodeText(txt);
const link = getShareLink(txt, locale);
setShareLink(link);
}
const copyLink = () => {
if (shareLink) {
navigator.clipboard.writeText(shareLink).catch(err => {
console.error("copy error:", err);
alert("copy error");
});
}
}
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "F11") {
@@ -160,33 +193,76 @@ export default function PreviewToolbar({
)}
<Flex gap={"9"} className="justify-around">
<button
{/* 全屏预览按钮 */}
<Button
onClick={() => handleFullScreen()}
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<Eye className="w-4 h-4" />
{t("previewFullscreen")} (F11)
</button>
</Button>
{/* 分享按钮 */}
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button
onClick={() => handleShare()}
>
<Share className="w-4 h-4" />
{t("share")}
</Button>
</AlertDialog.Trigger>
<AlertDialog.Content maxWidth="450px">
<AlertDialog.Title>{t("share")}</AlertDialog.Title>
{!shareError ? (<AlertDialog.Description size="2">
{t("shareSuccessDesc")} !
<br />
<Code className="mt-2">
{shareLink}
</Code>
</AlertDialog.Description>) :
(<AlertDialog.Description size="2" className="text-red-500">
{shareError}
</AlertDialog.Description>)}
<Flex gap="3" mt="4" justify="end">
{!shareError && <AlertDialog.Action>
<Button variant="soft" color="blue" onClick={() => copyLink()}>
{t("shareDialogCopyLink")}
</Button>
</AlertDialog.Action>
}
<AlertDialog.Cancel>
<Button variant="soft" color="gray">
{t("shareDialogClose")}
</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-muted-foreground">
{t("downloadSize")}
</label>
<select
value={`${aspectRadio}`}
onChange={(e) => setAspectRadio(parseInt(e.target.value))}
className="h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{AspectRatio.map((_, i) => <option key={i} value={i}>{Sizes[i]}</option>)}
</select>
<Select.Root defaultValue={`${aspectRadio}`} onValueChange={(e) => setAspectRadio(parseInt(e))}>
<Select.Trigger />
<Select.Content>
{AspectRatio.map((_, i) => <Select.Item key={i} value={i + ""}>{Sizes[i]}</Select.Item>)}
</Select.Content>
</Select.Root>
</div>
<button
<Button
onClick={handleDownload}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<Download className="w-4 h-4" />
{t("downloadBackground")}
</button>
</Button>
</div>
</Flex>

View File

@@ -1,4 +1,4 @@
import { Flex, Heading } from "@radix-ui/themes";
import { Flex, Heading, Select } from "@radix-ui/themes";
import { useTranslations } from "next-intl";
import { FontNames, FontWeights, TextProp } from "./TextSetting";
@@ -12,7 +12,7 @@ export default function TextSetting({
const t = useTranslations("TextEditor");
return (
<Flex className="p-4 border rounded-lg " gap={"3"} direction={"column"}>
<Flex className="p-4 border rounded-lg " gap={"3"} wrap={"wrap"}>
<Heading size={"3"} className="font-medium text-lg" >{t("title")}</Heading>
<textarea
value={text.text}
@@ -20,7 +20,7 @@ export default function TextSetting({
className="w-full p-3 border rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
rows={4}
/>
<div className="space-y-1">
<div className="space-y-1 w-full">
<label className="block text-sm text-muted-foreground">
{t("textColor")}
</label>
@@ -31,37 +31,29 @@ export default function TextSetting({
className="w-full h-10 rounded-md cursor-pointer"
/>
</div>
<div className="space-y-1">
<div className="space-y-1 w-1/3">
<label className="block text-sm text-muted-foreground">
{t("fontFamily")}
</label>
<select
value={text.font}
onChange={(e) => setText({ ...text, font: e.target.value })}
className="w-full p-2 border rounded-md"
>
{FontNames.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
<Select.Root defaultValue={`${text.font}`} onValueChange={(e) => setText({ ...text, font: e })}>
<Select.Trigger />
<Select.Content>
{FontNames.map((name) => <Select.Item key={name} value={name}>{name}</Select.Item>)}
</Select.Content>
</Select.Root>
</div>
<div className="space-y-2">
<div className="space-y-2 w-1/3">
<label className="block text-sm text-muted-foreground">
{t("fontWeight")}
</label>
<select
value={text.weight}
onChange={(e) => setText({ ...text, weight: e.target.value })}
className="w-full p-2 border rounded-md"
>
{FontWeights.map((weight) => (
<option key={weight} value={weight}>
{weight}
</option>
))}
</select>
<Select.Root defaultValue={`${text.weight}`} onValueChange={(e) => setText({ ...text, weight: e })}>
<Select.Trigger />
<Select.Content>
{FontWeights.map((name) => <Select.Item key={name} value={name}>{name}</Select.Item>)}
</Select.Content>
</Select.Root>
</div>
</Flex>
);

View File

@@ -1,4 +1,4 @@
import { Flex, Heading } from "@radix-ui/themes";
import { Flex, Heading, Select } from "@radix-ui/themes";
import { useTranslations } from "next-intl";
export const FontWeights = ["regular", "bold"];
@@ -92,33 +92,25 @@ export default function TextSetting({
<label className="block text-sm text-muted-foreground">
{t("fontFamily")}
</label>
<select
value={text.font}
onChange={(e) => setText({ ...text, font: e.target.value })}
className="w-full p-2 border rounded-md"
>
{FontNames.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
<Select.Root defaultValue={`${text.font}`} onValueChange={(e) => setText({ ...text, font: e })}>
<Select.Trigger />
<Select.Content>
{FontNames.map((name) => <Select.Item key={name} value={name}>{name}</Select.Item>)}
</Select.Content>
</Select.Root>
</div>
<div className="space-y-2">
<label className="block text-sm text-muted-foreground">
{t("fontWeight")}
</label>
<select
value={text.weight}
onChange={(e) => setText({ ...text, weight: e.target.value })}
className="w-full p-2 border rounded-md"
>
{FontWeights.map((weight) => (
<option key={weight} value={weight}>
{weight}
</option>
))}
</select>
<Select.Root defaultValue={`${text.weight}`} onValueChange={(e) => setText({ ...text, weight: e })}>
<Select.Trigger />
<Select.Content>
{FontWeights.map((name) => <Select.Item key={name} value={name}>{name}</Select.Item>)}
</Select.Content>
</Select.Root>
</div>
</Flex>
);