diff --git a/src/app/[lang]/components/menu/content.tsx b/src/app/[lang]/components/menu/content.tsx index ffb9d11a..0af5ad75 100644 --- a/src/app/[lang]/components/menu/content.tsx +++ b/src/app/[lang]/components/menu/content.tsx @@ -11,6 +11,7 @@ import { ArrowUpRightIcon, Check, GithubIcon, + Printer, Rocket, Rows2, SendIcon, @@ -94,6 +95,13 @@ export function Content({ variant: /^(\/tag)/.test(pathname) ? "default" : "ghost", href: `/${lang}/tag`, }, + { + title: dict.menu.print, + icon: Printer, + label: , + variant: /^(\/print)/.test(pathname) ? "default" : "ghost", + href: `/tools/print`, + }, ]} /> diff --git a/src/app/tools/layout.tsx b/src/app/tools/layout.tsx new file mode 100644 index 00000000..00538c2d --- /dev/null +++ b/src/app/tools/layout.tsx @@ -0,0 +1,9 @@ +import Root from "../root"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/tools/print/components/aside.tsx b/src/app/tools/print/components/aside.tsx new file mode 100644 index 00000000..37cb2127 --- /dev/null +++ b/src/app/tools/print/components/aside.tsx @@ -0,0 +1,14 @@ +import { ToggleOption } from "./toggle-option"; +import { SelectPoem } from "./select-poem"; + +export const Aside = () => { + return ( + <> + + + + ); +}; diff --git a/src/app/tools/print/components/preview-print.tsx b/src/app/tools/print/components/preview-print.tsx new file mode 100644 index 00000000..daf95d76 --- /dev/null +++ b/src/app/tools/print/components/preview-print.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { chunk } from "lodash-es"; +import { useRef } from "react"; + +import { cn } from "~/utils"; +import { type Author, type Poem } from "@prisma/client"; +import { useSearchParams } from "next/navigation"; +import { useReactToPrint } from "react-to-print"; +import { Button } from "~/components/ui/button"; + +const Row = ({ + text, + align = "center", + className, + border, +}: { + text: string; + align?: "left" | "right" | "center"; + className?: string; + border?: boolean; +}) => { + const num = 12; + const left = Math.floor((num - text.length) / 2) + text.length; + + let data = text.padStart(left).padEnd(num).split(""); + if (align === "right") { + data = text.padStart(num).split(""); + } + + if (align === "left") { + data = text.padEnd(num).split(""); + } + + return ( +
+ {data.map((item, index) => ( +
+ {item} +
+ ))} +
+ ); +}; + +const PyRow = ({ + py, + className, + border, + align, +}: { + py: string; + className?: string; + align?: "left" | "right" | "center"; + border?: boolean; +}) => { + const num = 12; + const arr = py.split(" "); + + let left = Math.floor((num - arr.length) / 2); + + if (align === "right") { + left = num - arr.length; + } + + const data = new Array(12).fill("").map((_, index) => { + return index < left ? "" : arr[index - left]; + }); + + return ( +
+ {data.map((item, index) => ( +
+ {border && ( + <> +
+
+ + )} + {item} +
+ ))} +
+ ); +}; + +export default function PreviewPrint({ + poem, +}: { + poem: Poem & { author: Author }; +}) { + const componentRef = useRef(null); + const searchParams = useSearchParams(); + + const opts = { + translation: searchParams.get("translation") === "true", + py: searchParams.get("py") === "true", + border: searchParams.get("border") === "true", + }; + + const handlePrint = useReactToPrint({ + content: () => componentRef.current, + pageStyle: `padding:24px`, + }); + + const title = poem.title; + const author = `${poem.author.dynasty}·${poem.author.name}`; + + const content = poem.content + .replaceAll("\n", "") + .match(/[^。|!|?|,|;]+[。|!|?|,|;]+/g); + + if (!content) return null; + + const translation = chunk( + poem.translation?.replaceAll("\n", "").split(""), + 12, + ); + + const arr = [title, author, ...content]; + const py = [ + poem.titlePinYin, + poem.author.namePinYin, + ...(poem.contentPinYin ?? "").split("."), + ]; + + return ( + <> + +
+
+
+ {arr.map((item, index) => ( +
+ + +
+ ))} +
+ + {opts.translation && ( +

+ 译文 + aspoem.com | 现代化中国诗词学习网站 +

+ )} + + {opts.translation && + translation.map((item) => + Row({ + text: item.join(""), + border: opts.border, + align: "left", + className: "h-20 w-20", + }), + )} +
+
+ + ); +} diff --git a/src/app/tools/print/components/select-poem.tsx b/src/app/tools/print/components/select-poem.tsx new file mode 100644 index 00000000..bf0ad882 --- /dev/null +++ b/src/app/tools/print/components/select-poem.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { Button } from "~/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "~/components/ui/command"; +import { useDebounce } from "@react-hook/debounce"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; +import { api } from "~/trpc/react"; +import { useEffect, useState } from "react"; +import { X } from "lucide-react"; +import { useRouter } from "next/navigation"; + +export function SelectPoem(props: { + selected?: { + author: { + name: string; + id: number; + }; + id: number; + title: string; + content: string; + }; +}) { + const [value, setValue] = useState(""); + const [query, setQuery] = useDebounce(value, 500); + const router = useRouter(); + + const { data = [] } = api.poem.search.useQuery(query, { + refetchOnWindowFocus: false, + }); + + useEffect(() => { + setQuery(value); + }, [setQuery, value]); + + const [open, setOpen] = useState(false); + const [selected, setSelected] = useState<(typeof data)[number] | undefined>( + props.selected || undefined, + ); + + return ( + + + + + + + + + 没有找到诗词。 + + {data.map((item) => { + const value = `${item.title} | ${item.author.name}`; + + return ( + { + setSelected( + data.find( + (item) => `${item.title} | ${item.author.name}` === e, + ) || undefined, + ); + setOpen(false); + router.push("?id=" + item.id); + }} + > + {item.title} | {item.author.name} + + ); + })} + + + + + + ); +} diff --git a/src/app/tools/print/toggle-option.tsx b/src/app/tools/print/components/toggle-option.tsx similarity index 53% rename from src/app/tools/print/toggle-option.tsx rename to src/app/tools/print/components/toggle-option.tsx index 91c39e9f..19af6047 100644 --- a/src/app/tools/print/toggle-option.tsx +++ b/src/app/tools/print/components/toggle-option.tsx @@ -1,5 +1,7 @@ "use client"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; import { Checkbox } from "~/components/ui/checkbox"; export type Options = { @@ -8,30 +10,46 @@ export type Options = { border: boolean; }; -export const ToggleOption = (props: { - onChange?: (opt: Options) => void; - value: Options; -}) => { +export const ToggleOption = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const id = searchParams.get("id"); + + const [opts, setOpts] = useState({ + translation: searchParams.get("translation") === "true", + py: searchParams.get("py") === "true", + border: searchParams.get("border") === "true", + }); + const toggle = (key: keyof Options) => { - props.onChange?.({ ...props.value, [key]: !props.value[key] }); + setOpts({ + ...opts, + [key]: !opts[key], + }); }; + useEffect(() => { + router.push( + `?id=${id}&translation=${opts.translation}&py=${opts.py}&border=${opts.border}`, + ); + }, [opts, router, id]); + return ( -
+

自定义打印内容

toggle("translation")} + id="border" + checked={opts.border} + onCheckedChange={() => toggle("border")} /> - +
toggle("py")} /> @@ -39,11 +57,11 @@ export const ToggleOption = (props: {
toggle("border")} + id="translation" + checked={opts.translation} + onCheckedChange={() => toggle("translation")} /> - +
); diff --git a/src/app/tools/print/layout.tsx b/src/app/tools/print/layout.tsx index 45dd6d9d..9ca46cd4 100644 --- a/src/app/tools/print/layout.tsx +++ b/src/app/tools/print/layout.tsx @@ -1,26 +1,14 @@ -import Root from "../../root"; +import { Aside } from "./components/aside"; -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { return ( - - 诗词打印、导出为PDF工具 - - - - } - > - {children} - +
+
); } diff --git a/src/app/tools/print/page.tsx b/src/app/tools/print/page.tsx index 4281223a..b87241a0 100644 --- a/src/app/tools/print/page.tsx +++ b/src/app/tools/print/page.tsx @@ -1,232 +1,40 @@ -"use client"; +import { api } from "~/trpc/server"; +import PreviewPrint from "./components/preview-print"; +import { cache } from "react"; -import { chunk } from "lodash-es"; -import { useRef, useState } from "react"; -import { useReactToPrint } from "react-to-print"; - -import { Button } from "~/components/ui/button"; -import { type Locale } from "~/dictionaries"; -import { api } from "~/trpc/react"; -import { cn } from "~/utils"; -import { type Options, ToggleOption } from "./toggle-option"; -import Link from "next/link"; - -const Row = ({ - text, - align = "center", - className, - border, -}: { - text: string; - align?: "left" | "right" | "center"; - className?: string; - border?: boolean; -}) => { - const num = 12; - const left = Math.floor((num - text.length) / 2) + text.length; - - let data = text.padStart(left).padEnd(num).split(""); - if (align === "right") { - data = text.padStart(num).split(""); - } - - if (align === "left") { - data = text.padEnd(num).split(""); - } - - return ( -
- {data.map((item, index) => ( -
- {item} -
- ))} -
- ); -}; - -const ChoosePoem = () => { - return ( - - ); -}; - -const PyRow = ({ - py, - className, - border, - align, -}: { - py: string; - className?: string; - align?: "left" | "right" | "center"; - border?: boolean; -}) => { - const num = 12; - const arr = py.split(" "); - - let left = Math.floor((num - arr.length) / 2); - - if (align === "right") { - left = num - arr.length; - } - - const data = new Array(12).fill("").map((_, index) => { - return index < left ? "" : arr[index - left]; - }); - - return ( -
- {data.map((item, index) => ( -
- {border && ( - <> -
-
- - )} - {item} -
- ))} -
- ); -}; - -export default function PrintPage({ - searchParams, -}: { - searchParams: { id: string; lang: Locale }; -}) { - const componentRef = useRef(null); - const handlePrint = useReactToPrint({ - content: () => componentRef.current, - pageStyle: `padding:24px`, - }); +interface Props { + searchParams: { id: string }; +} - const [opts, setOpts] = useState({ - translation: true, - py: false, - border: true, - }); +const getItem = cache(async ({ searchParams }: Props) => { + if (searchParams.id === "null" || !searchParams.id) return null; - const { data: poem } = api.poem.findById.useQuery({ + return api.poem.findById.query({ id: Number(searchParams.id), - lang: searchParams.lang, }); +}); - if (!poem) return ; - - const title = poem.title; - const author = `${poem.author.dynasty}·${poem.author.name}`; +export async function generateMetadata(props: Props) { + const poem = await getItem(props); - const content = poem.content - .replaceAll("\n", "") - .match(/[^。|!|?|,|;]+[。|!|?|,|;]+/g); - - if (!content) return ; - - const translation = chunk( - poem.translation?.replaceAll("\n", "").split(""), - 12, - ); - - const arr = [title, author, ...content]; - const py = [ - poem.titlePinYin, - poem.author.namePinYin, - ...(poem.contentPinYin ?? "").split("."), - ]; - - return ( - <> -
- - + return { + title: `诗词《${poem.title}》打印`, + description: `诗词《${poem.title}》作者${poem.author.name},支持在线打印诗词田字格、内容、译文、拼音。允许你自由组合打印方式。`, + }; +} -
-
-
- {arr.map((item, index) => ( -
- - -
- ))} -
+export default async function Page({ searchParams }: Props) { + const poem = await getItem({ searchParams }); - {opts.translation && ( -

- 译文 - aspoem.com | 现代化中国诗词学习网站 -

- )} + if (!poem) return null; - {opts.translation && - translation.map((item) => - Row({ - text: item.join(""), - border: opts.border, - align: "left", - className: "h-20 w-20", - }), - )} -
-
-
- - ); + return ; } diff --git a/src/dictionaries/zh-Hans.json b/src/dictionaries/zh-Hans.json index 085a5175..72156739 100644 --- a/src/dictionaries/zh-Hans.json +++ b/src/dictionaries/zh-Hans.json @@ -33,7 +33,8 @@ "contact": "联系方式", "theme": "主题", "language": "语言", - "feedback": "留言" + "feedback": "留言", + "print": "打印" }, "home": { "poem": "推荐", diff --git a/src/dictionaries/zh-Hant.json b/src/dictionaries/zh-Hant.json index 9e96f0eb..8db44b26 100644 --- a/src/dictionaries/zh-Hant.json +++ b/src/dictionaries/zh-Hant.json @@ -33,7 +33,8 @@ "contact": "聯繫方式", "theme": "主題", "language": "語言", - "feedback": "留言" + "feedback": "留言", + "print": "打印" }, "home": { "poem": "推薦", diff --git a/src/server/api/routers/poem.ts b/src/server/api/routers/poem.ts index c5063799..1a5416eb 100644 --- a/src/server/api/routers/poem.ts +++ b/src/server/api/routers/poem.ts @@ -90,7 +90,7 @@ export const poemRouter = createTRPCRouter({ ), search: publicProcedure - .input(z.string().optional()) + .input(z.string().default("")) .query(({ input, ctx }) => { return ctx.db.poem.findMany({ where: {