diff --git a/package.json b/package.json index 9c4a69b4..662b95de 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@giscus/react": "^2.4.0", - "@prisma/client": "^5.6.0", + "@prisma/client": "^5.8.1", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -68,7 +68,7 @@ "postcss-nesting": "^12.0.2", "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.7", - "prisma": "^5.6.0", + "prisma": "^5.8.1", "tailwindcss": "^3.3.5", "typescript": "^5.1.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f1d1aa1..bae4e6f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^2.4.0 version: 2.4.0(react-dom@18.2.0)(react@18.2.0) '@prisma/client': - specifier: ^5.6.0 - version: 5.7.1(prisma@5.7.1) + specifier: ^5.8.1 + version: 5.8.1(prisma@5.8.1) '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0) @@ -167,8 +167,8 @@ devDependencies: specifier: ^0.5.7 version: 0.5.10(prettier@3.1.1) prisma: - specifier: ^5.6.0 - version: 5.7.1 + specifier: ^5.8.1 + version: 5.8.1 tailwindcss: specifier: ^3.3.5 version: 3.4.0 @@ -459,8 +459,8 @@ packages: requiresBuild: true optional: true - /@prisma/client@5.7.1(prisma@5.7.1): - resolution: {integrity: sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==} + /@prisma/client@5.8.1(prisma@5.8.1): + resolution: {integrity: sha512-xQtMPfbIwLlbm0VVIVQY2yqQVOxPwRQhvIp7Z3m2900g1bu/zRHKhYZJQWELqmjl6d8YwBy0K2NvMqh47v1ubw==} engines: {node: '>=16.13'} requiresBuild: true peerDependencies: @@ -469,35 +469,35 @@ packages: prisma: optional: true dependencies: - prisma: 5.7.1 + prisma: 5.8.1 dev: false - /@prisma/debug@5.7.1: - resolution: {integrity: sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==} + /@prisma/debug@5.8.1: + resolution: {integrity: sha512-tjuw7eA0Us3T42jx9AmAgL58rzwzpFGYc3R7Y4Ip75EBYrKMBA1YihuWMcBC92ILmjlQ/u3p8VxcIE0hr+fZfg==} - /@prisma/engines-version@5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5: - resolution: {integrity: sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==} + /@prisma/engines-version@5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2: + resolution: {integrity: sha512-f5C3JM3l9yhGr3cr4FMqWloFaSCpNpMi58Om22rjD2DOz3owci2mFdFXMgnAGazFPKrCbbEhcxdsRfspEYRoFQ==} - /@prisma/engines@5.7.1: - resolution: {integrity: sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==} + /@prisma/engines@5.8.1: + resolution: {integrity: sha512-TJgYLRrZr56uhqcXO4GmP5be+zjCIHtLDK20Cnfg+o9d905hsN065QOL+3Z0zQAy6YD31Ol4u2kzSfRmbJv/uA==} requiresBuild: true dependencies: - '@prisma/debug': 5.7.1 - '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 - '@prisma/fetch-engine': 5.7.1 - '@prisma/get-platform': 5.7.1 + '@prisma/debug': 5.8.1 + '@prisma/engines-version': 5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2 + '@prisma/fetch-engine': 5.8.1 + '@prisma/get-platform': 5.8.1 - /@prisma/fetch-engine@5.7.1: - resolution: {integrity: sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==} + /@prisma/fetch-engine@5.8.1: + resolution: {integrity: sha512-+bgjjoSFa6uYEbAPlklfoVSStOEfcpheOjoBoNsNNSQdSzcwE2nM4Q0prun0+P8/0sCHo18JZ9xqa8gObvgOUw==} dependencies: - '@prisma/debug': 5.7.1 - '@prisma/engines-version': 5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5 - '@prisma/get-platform': 5.7.1 + '@prisma/debug': 5.8.1 + '@prisma/engines-version': 5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2 + '@prisma/get-platform': 5.8.1 - /@prisma/get-platform@5.7.1: - resolution: {integrity: sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==} + /@prisma/get-platform@5.8.1: + resolution: {integrity: sha512-wnA+6HTFcY+tkykMokix9GiAkaauPC5W/gg0O5JB0J8tCTNWrqpnQ7AsaGRfkYUbeOIioh6woDjQrGTTRf1Zag==} dependencies: - '@prisma/debug': 5.7.1 + '@prisma/debug': 5.8.1 /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} @@ -3759,13 +3759,13 @@ packages: hasBin: true dev: true - /prisma@5.7.1: - resolution: {integrity: sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==} + /prisma@5.8.1: + resolution: {integrity: sha512-N6CpjzECnUHZ5beeYpDzkt2rYpEdAeqXX2dweu6BoQaeYkNZrC/WJHM+5MO/uidFHTak8QhkPKBWck1o/4MD4A==} engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 5.7.1 + '@prisma/engines': 5.8.1 /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f1bc5bd8..9d72815b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,8 @@ model Poem { contentPinYin String? introduce String? translation String? + // 注解 JSON 字符串 {文字: 解释} + annotation String? author Author @relation(fields: [authorId], references: [id]) authorId Int // 内容分类:叙事诗、抒情诗、送别诗、边塞诗、山水田园诗、咏史诗、咏物诗、悼亡诗、讽喻诗 diff --git a/src/app/(.)/poem/[id]/components/PinYinText/index.css b/src/app/(.)/poem/[id]/components/PinYinText/index.css index 3049a443..1cbe93f7 100644 --- a/src/app/(.)/poem/[id]/components/PinYinText/index.css +++ b/src/app/(.)/poem/[id]/components/PinYinText/index.css @@ -11,7 +11,7 @@ } .char_annotation { - @apply cursor-pointer text-blue-900; + @apply cursor-pointer text-blue-800 dark:text-blue-400; } &.pinyin_h1 { diff --git a/src/app/(.)/poem/[id]/components/PinYinText/index.tsx b/src/app/(.)/poem/[id]/components/PinYinText/index.tsx index 64a98736..2b65ea4d 100644 --- a/src/app/(.)/poem/[id]/components/PinYinText/index.tsx +++ b/src/app/(.)/poem/[id]/components/PinYinText/index.tsx @@ -20,7 +20,13 @@ interface Props { const _pinyinNoShowChar = ["."]; function _getText(text: string, annotation: string[]) { + const newAnnotation: string[] = []; + annotation.forEach((item) => { + if (text.includes(item)) { + newAnnotation.push(item); + } + text = text.replace(item, "$"); }); @@ -31,7 +37,7 @@ function _getText(text: string, annotation: string[]) { return result.map((item) => { if (item === "$") { i++; - return annotation[i - 1] || ""; + return newAnnotation[i - 1] || ""; } return item; @@ -124,7 +130,7 @@ const PinYinText = (props: Props) => { }; return ( - { )} - + ); }; diff --git a/src/app/(.)/poem/[id]/page.tsx b/src/app/(.)/poem/[id]/page.tsx index b97d99cb..e47f756e 100644 --- a/src/app/(.)/poem/[id]/page.tsx +++ b/src/app/(.)/poem/[id]/page.tsx @@ -64,6 +64,10 @@ export default async function Page({ params, searchParams }: Props) { const blockArray = poem.content.split("\n\n"); + const annotation = JSON.parse(poem.annotation ?? "{}") as { + [key in string]: string; + }; + return ( <> @@ -108,7 +112,7 @@ export default async function Page({ params, searchParams }: Props) { pinyin={showPinYin ? poem.titlePinYin ?? "" : ""} type="h1" /> - + {poem.author.dynasty && ( {poem.author.dynasty} · )} @@ -156,6 +160,7 @@ export default async function Page({ params, searchParams }: Props) { text={line} align={poem.genre === "词" ? "left" : "center"} pinyin={showPinYin ? blockPinYin?.split("\n")[index] : ""} + annotation={annotation} /> ))} > diff --git a/src/app/create/layout.tsx b/src/app/create/layout.tsx index 538d7163..8175da73 100644 --- a/src/app/create/layout.tsx +++ b/src/app/create/layout.tsx @@ -29,7 +29,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { ))} - + {children} diff --git a/src/app/create/poem/page.tsx b/src/app/create/poem/page.tsx index 6ac54856..0880d770 100644 --- a/src/app/create/poem/page.tsx +++ b/src/app/create/poem/page.tsx @@ -42,6 +42,9 @@ export default function CreatePage() { const token = params.get("token") ?? ""; const router = useRouter(); const id = params.get("id") ?? ""; + const [annotations, setAnnotations] = useState< + { keyword: string; content: string }[] + >([]); const { data: poem } = api.poem.findById.useQuery(Number(id), { refetchOnWindowFocus: false, @@ -92,6 +95,7 @@ export default function CreatePage() { setContentPinYin(""); setIntroduce(""); setTranslation(""); + setAnnotations([]); } else { mutation.isSame.mutate({ title, @@ -129,6 +133,21 @@ export default function CreatePage() { setClassify(poem.classify ?? ""); setGenre(poem.genre ?? ""); setTranslation(poem?.translation ?? ""); + + const json = JSON.parse(poem.annotation ?? "{}") as { + [key in string]: string; + }; + + const arr: typeof annotations = []; + + for (const key in json) { + arr.push({ + keyword: key, + content: json[key] || "", + }); + } + + setAnnotations(arr); } }, [poem]); @@ -168,6 +187,7 @@ export default function CreatePage() { onClick={() => { setTitle(simplized(title)); setContent(simplized(content)); + setTranslation(simplized(translation)); }} > 繁转简 @@ -176,182 +196,257 @@ export default function CreatePage() { Fill out the details htmlFor your new poem - - - - * 标题 - - setTitle(e.target.value)} - /> - - - - * 标题 - (拼音) - - setTitlePinYin(e.target.value)} - /> + {/* 名字 */} + + + + * 标题 + + setTitle(e.target.value)} + /> + + + + * 标题 + (拼音) + + setTitlePinYin(e.target.value)} + /> + - - - * 作者 - + {/* 作者 体裁 */} + + + + * 作者 + + {authors && ( + { + setAuthorId(framework.id); + }} + defaultValue={{ + ...authorItem, + value: authorItem?.name ?? "", + name: authorItem?.name ?? "", + id: authorItem?.id ?? -1, + }} + frameworks={authors.map((item) => ({ + value: item.name, + name: item.name, + id: item.id, + }))} + /> + )} + - {authors && ( - { - setAuthorId(framework.id); - }} - defaultValue={{ - ...authorItem, - value: authorItem?.name ?? "", - name: authorItem?.name ?? "", - id: authorItem?.id ?? -1, - }} - frameworks={authors.map((item) => ({ - value: item.name, - name: item.name, - id: item.id, - }))} - /> - )} - + + + 体裁 + - - - * 内容 - - setContent(e.target.value)} - /> + setGenre(val)}> + + + + + {_genre.map((item) => ( + + {item} + + ))} + + + - - - 内容 - (拼音) - - setContentPinYin(e.target.value)} - /> + {/* 内容 */} + + + + * 内容 + + setContent(e.target.value)} + /> + + + + + 内容 + (拼音) + + setContentPinYin(e.target.value)} + /> + - - - 译文 - - setTranslation(e.target.value)} - /> + + + + 译文 + + setTranslation(e.target.value)} + /> + + + + 补充介绍 + + setIntroduce(e.target.value)} + /> + - 体裁 + 注解{" "} + { + setAnnotations([...annotations, { keyword: "", content: "" }]); + }} + > + 添加注解 + + + {annotations.map((item, index) => ( + <> + + { + const newAnnotations = [...annotations]; + newAnnotations.splice(index, 1); + setAnnotations(newAnnotations); + }} + > + - + + { + const newAnnotations = [...annotations]; + newAnnotations[index]!.keyword = e.target.value; + setAnnotations(newAnnotations); + }} + /> + - setGenre(val)}> - - - - - {_genre.map((item) => ( - - {item} - - ))} - - + { + const newAnnotations = [...annotations]; + newAnnotations[index]!.content = e.target.value; + setAnnotations(newAnnotations); + }} + /> + > + ))} + - - - 补充介绍 - - setIntroduce(e.target.value)} - /> - + + { + window.confirm("Are you sure you wish to delete this item?") + ? mutation.deletePoem.mutate({ + id: Number(id), + token, + }) + : null; + }} + > + Delete Poem + + { + if (!title || !content || authorId === -1) { + alert("Please fill out all the fields"); + return; + } - { - if (!title || !content || authorId === -1) { - alert("Please fill out all the fields"); - return; - } - - mutation.createPoem.mutate({ - id: id ? Number(id) : undefined, - token, - title, - titlePinYin: titlePinYin.replace(/(\s+)?·/g, " ."), - contentPinYin: contentPinYin.replace( - /(\s+)?(\.|,|!|、|!|。|,|;)/g, - " .", - ), - content, - authorId, - tagIds, - classify, - genre, - introduce, - translation, - }); - }} - > - Save Poem - - { - window.confirm("Are you sure you wish to delete this item?") - ? mutation.deletePoem.mutate({ - id: Number(id), - token, - }) - : null; - }} - > - Delete Poem - + const json: { [key in string]: string } = {}; + + annotations.forEach((item) => { + json[item.keyword] = item.content; + }); + + mutation.createPoem.mutate({ + id: id ? Number(id) : undefined, + token, + title, + titlePinYin: titlePinYin.replace(/(\s+)?·/g, " ."), + contentPinYin: contentPinYin.replace( + /(\s+)?(\.|,|!|、|!|。|,|;)/g, + " .", + ), + content, + authorId, + tagIds, + classify, + genre, + introduce, + translation, + annotation: JSON.stringify(json), + }); + }} + > + Save Poem + + > ); diff --git a/src/app/demo/page.tsx b/src/app/demo/page.tsx index a2f259e3..03adfdd6 100644 --- a/src/app/demo/page.tsx +++ b/src/app/demo/page.tsx @@ -21,6 +21,11 @@ export default function Page() { text={`悄庭户。试细听、莺啼燕语。分明共人愁绪。怕春去。`} pinyin={`qiāo tíng hù . shì xì tīng . yīng tí yàn yǔ . fēn míng gòng rén chóu xù . pà chūn qù . `} annotation={{ + 赖: "依靠", + 倩: "请、托", + 住: "停止,不动了。", + 妖娆: `妖媚艳丽。形容景色异常艳丽。 一作“娇饶“。`, + 处: "时候,季节", 莺啼燕语: "莺啼婉转,燕语呢喃。形容春光明媚。", }} /> diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx index fe472b14..26409e5e 100644 --- a/src/components/ui/hover-card.tsx +++ b/src/components/ui/hover-card.tsx @@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( - "z-50 max-w-64 rounded-md border bg-popover px-4 py-2 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "z-50 max-w-64 rounded-md border bg-popover px-4 py-2 leading-normal text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} diff --git a/src/server/api/routers/poem.ts b/src/server/api/routers/poem.ts index 6891e9cc..df6c97fd 100644 --- a/src/server/api/routers/poem.ts +++ b/src/server/api/routers/poem.ts @@ -177,6 +177,7 @@ export const poemRouter = createTRPCRouter({ genre: z.string().optional(), introduce: z.string().optional(), translation: z.string().optional(), + annotation: z.string().optional(), }), ) .mutation(async ({ input, ctx }) => { @@ -206,6 +207,7 @@ export const poemRouter = createTRPCRouter({ genre: input.genre, introduce: input.introduce, translation: input.translation, + annotation: input.annotation, tags: input.tagIds && { connect: input.tagIds.map((id) => ({ id })), },
Fill out the details htmlFor your new poem