完成分享功能
This commit is contained in:
@@ -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"}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user