diff --git a/src/app/[lang]/poem/[id]/components/body.tsx b/src/app/[lang]/poem/[id]/components/body.tsx index c5938b9e..66cd5f93 100644 --- a/src/app/[lang]/poem/[id]/components/body.tsx +++ b/src/app/[lang]/poem/[id]/components/body.tsx @@ -3,6 +3,7 @@ import { Verse } from "~/components/verse"; import { cn } from "~/utils"; import Link from "next/link"; import { type Locale, getLangUrl } from "~/dictionaries"; +import { TypographyArticle } from "~/components/typography-article"; export const Body = (props: { poem: Poem & { author: Author }; @@ -22,8 +23,6 @@ export const Body = (props: { const titlePinYin = py ? poem.titlePinYin ?? "" : ""; - const shi = poem.genre === "诗"; - return (
@@ -48,24 +47,31 @@ export const Body = (props: { {poem.introduce && (

{poem.introduce}

)} - {content.map((line, index) => ( - ( + + )) + ) : ( + - ))} + )}
); diff --git a/src/app/demo/verse/a/page.tsx b/src/app/demo/verse/a/page.tsx new file mode 100644 index 00000000..cc91c673 --- /dev/null +++ b/src/app/demo/verse/a/page.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { api } from "~/trpc/react"; +import { useState } from "react"; +import { Button } from "~/components/ui/button"; +import { TypographyArticle } from "~/components/typography-article"; + +export default function Page() { + const { data } = api.poem.findById.useQuery({ + id: 2255, + }); + + const [py, setPy] = useState(false); + const [an, setAn] = useState(true); + + if (!data) return null; + + const py_paragraphs = data.contentPinYin?.split("\n") ?? []; + const paragraphs = data.content.split("\n"); + const annotation = ( + data.annotation ? JSON.parse(data.annotation) : {} + ) as Record; + + return ( +
+
+ + +
+ +
+ +
+
+ ); +} diff --git a/src/components/typography-article/annotation-modal.tsx b/src/components/typography-article/annotation-modal.tsx new file mode 100644 index 00000000..adbf7755 --- /dev/null +++ b/src/components/typography-article/annotation-modal.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect } from "react"; + +export const AnnotationModal = ({ + annotation, +}: { + annotation: Record; +}) => { + useEffect(() => { + const mainBody = document.querySelector("#main-body")!; + const bodyPos = mainBody.getBoundingClientRect(); + const maxLeft = bodyPos.left + bodyPos.width - 256; + + const fn = (e: Event) => { + const target = e.target as HTMLElement; + const BTarget = + target.tagName === "B" ? target : (target.offsetParent as HTMLElement); + const Btext = BTarget.innerText.match(/[\u4e00-\u9fa5]/g)?.join("") || ""; + + if (BTarget.tagName === "B" && Btext) { + const pos = target.getBoundingClientRect(); + + const div = document.createElement("div"); + div.id = "annotation-box"; + div.setAttribute( + "style", + `position: fixed;width: 100vw;height: 100vh;top: 0;left: 0;z-index: 100;`, + ); + + div.addEventListener("click", (e) => { + const target = e.target as HTMLElement; + if (target.id === "annotation-box") { + div.remove(); + } + }); + + const x = pos.x > maxLeft ? maxLeft : pos.x; + + div.setAttribute("data-text", Btext); + div.innerHTML = `
${annotation[Btext]}
`; + + document.body.appendChild(div); + } + }; + + mainBody.addEventListener("click", fn); + + return () => { + mainBody.removeEventListener("click", fn); + }; + }, [annotation]); + + return null; +}; diff --git a/src/components/typography-article/index.css b/src/components/typography-article/index.css new file mode 100644 index 00000000..17523dd7 --- /dev/null +++ b/src/components/typography-article/index.css @@ -0,0 +1,27 @@ +.py-line { + @apply text-f200; + line-height: 2.5em; + text-indent: 3em; + + &:not(:last-child) { + @apply mb-8; + } + + .py-chinese-item { + @apply px-1.5; + } + + b { + @apply relative cursor-pointer border-b border-primary/50 font-normal text-primary transition-colors dark:text-destructive; + } + + rt { + @apply font-serif text-[60%] !font-normal text-muted-foreground; + } +} + +.py-line.no-py { + @apply text-f200; + line-height: 1.75em; + text-indent: 2em; +} diff --git a/src/components/typography-article/index.tsx b/src/components/typography-article/index.tsx new file mode 100644 index 00000000..b03097c1 --- /dev/null +++ b/src/components/typography-article/index.tsx @@ -0,0 +1,102 @@ +import { cn } from "~/utils"; +import { AnnotationModal } from "./annotation-modal"; +import "./index.css"; + +interface Props { + paragraphs: string[]; + py_paragraphs: string[]; + annotation: Record; +} + +export const TypographyArticle = ({ + paragraphs, + py_paragraphs, + annotation, +}: Props) => { + return ( +
+ + + +
+ ); +}; + +/** + * 段落 + */ +const Paragraph = ({ + paragraphs, + py_paragraphs, + annotation, +}: { + paragraphs: string[]; + py_paragraphs: string[]; + annotation: Record; +}) => { + return ( + <> + {paragraphs.map((paragraph, i) => ( +

+ ))} + + ); +}; + +const Annotation = ({ + paragraph, + py_paragraph, + annotation = {}, +}: { + paragraph: string; + py_paragraph?: string; + annotation: Record; +}) => { + const origin = paragraph; + const py = py_paragraph?.split(" "); + // 正则汉字 + const re = /[\u4e00-\u9fa5]/g; + const cn_symbol = /,|。|;|?|!|“|”|‘|’|(|)|《|》|【|】|、/g; + + // 中文分号替换为句号 + // 分号无法触发谷歌的首字符号优化 + paragraph = paragraph.replace( + cn_symbol, + (val) => + `${val.replace(";", "。")}`, + ); + + for (const key in annotation) { + paragraph = paragraph.replace( + new RegExp(key, "g"), + (val) => `${val}`, + ); + } + + if (!py) return paragraph; + + paragraph = paragraph.replace(re, (val) => { + const index = origin.indexOf(val); + const rt = py[index]; + + return `${val}(${rt})`; + }); + + return paragraph; +};