diff --git a/apps/server/src/models/resource/resource.router.ts b/apps/server/src/models/resource/resource.router.ts index 6b91c40..5463603 100755 --- a/apps/server/src/models/resource/resource.router.ts +++ b/apps/server/src/models/resource/resource.router.ts @@ -23,13 +23,24 @@ export class ResourceRouter { ) {} router = this.trpc.router({ create: this.trpc.protectProcedure - .input(ResourceCreateArgsSchema) - + .input( + z.object({ + data: z.object({ + fileId: z.string(), + isPublic: z.boolean().optional(), + title: z.string().optional(), + description: z.string().optional(), + type: z.string().optional(), + }), + }), + ) + .mutation(async ({ ctx, input }) => { const { staff } = ctx; + console.log('资源创建数据:', input.data); return await this.resourceService.create(input, { staff }); }), - + createMany: this.trpc.protectProcedure .input(z.array(ResourceCreateManyInputSchema)) .mutation(async ({ ctx, input }) => { @@ -48,9 +59,11 @@ export class ResourceRouter { return await this.resourceService.findFirst(input); }), softDeleteByIds: this.trpc.protectProcedure - .input(z.object({ - ids: z.array(z.string()) - })) + .input( + z.object({ + ids: z.array(z.string()), + }), + ) .mutation(async ({ input, ctx }) => { const result = await this.resourceService.softDeleteByIds(input.ids); return result; @@ -78,20 +91,24 @@ export class ResourceRouter { const { staff } = ctx; return await this.resourceService.findManyWithCursor(input); }), - count: this.trpc.procedure + count: this.trpc.procedure .input( z.object({ - where: z.object({ - AND: z.object({ - title: z.object({ - not: z.null() - }), - description: z.object({ - not: z.null() - }) - }).optional(), - deletedAt: z.date().nullable().optional(), - }).optional(), + where: z + .object({ + AND: z + .object({ + title: z.object({ + not: z.null(), + }), + description: z.object({ + not: z.null(), + }), + }) + .optional(), + deletedAt: z.date().nullable().optional(), + }) + .optional(), }), ) .query(async ({ input }) => { diff --git a/apps/web/src/app/main/help/ExampleContent.tsx b/apps/web/src/app/main/help/ExampleContent.tsx deleted file mode 100755 index 50dc350..0000000 --- a/apps/web/src/app/main/help/ExampleContent.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export function ExampleContent() { - return ( -
-

案例分析

- {/* 这里放视频列表内容 */} -
- ); - } \ No newline at end of file diff --git a/apps/web/src/app/main/help/MusicContent.tsx b/apps/web/src/app/main/help/MusicContent.tsx index 3a2e814..f3a97e3 100755 --- a/apps/web/src/app/main/help/MusicContent.tsx +++ b/apps/web/src/app/main/help/MusicContent.tsx @@ -18,7 +18,7 @@ import dayjs from "dayjs"; import { env } from "@web/src/env"; import { getFileIcon } from "@web/src/components/models/post/detail/utils"; import { formatFileSize, getCompressedImageUrl } from "@nice/utils"; -import { ResourceDto, RoleName } from "packages/common/dist"; +import { ResourceDto, RoleName, ResourceType } from "@nice/common"; import { useAuth } from "@web/src/providers/auth-provider"; const { TabPane } = Tabs; diff --git a/apps/web/src/app/main/help/PsychologyNav.tsx b/apps/web/src/app/main/help/PsychologyNav.tsx index 2bcc421..d2b30a0 100755 --- a/apps/web/src/app/main/help/PsychologyNav.tsx +++ b/apps/web/src/app/main/help/PsychologyNav.tsx @@ -1,88 +1,85 @@ -import { Tabs } from 'antd'; +import { Tabs } from "antd"; import { - VideoCameraOutlined, - FileTextOutlined, - CustomerServiceOutlined, - ReadOutlined, - BookOutlined -} from '@ant-design/icons'; -import { VideoContent } from './VideoContent'; -import { MusicContent } from './MusicContent'; -import { ScienceContent } from './ScienceContent'; -import { PublicityContent } from './PublicityContent'; -import { ExampleContent } from './ExampleContent'; -import './pt.css'; + VideoCameraOutlined, + FileTextOutlined, + CustomerServiceOutlined, + ReadOutlined, + BookOutlined, +} from "@ant-design/icons"; +import { VideoContent } from "./VideoContent"; +import { MusicContent } from "./MusicContent"; +import { ScienceContent } from "./science/ScienceContent"; +import { PublicityContent } from "./news/PublicityContent"; +import { ExampleContent } from "./example/ExampleContent"; +import "./pt.css"; export function PsychologyNav() { - const items = [ - { - key: 'publicity', - label: ( - - - 宣传报道 - - ), - children: - }, - { - key: 'science', - label: ( - - < ReadOutlined className="text-lg" /> - 常识科普 - - ), - children: - }, - { - key: 'example', - label: ( - - - 案例分析 - - ), - children: - }, - { - key: 'video', - label: ( - - < FileTextOutlined className="text-lg" /> - 心理课件 - - ), - children: - }, - { - key: 'music', - label: ( - - - 音视频 - - ), - children: - }, - - - ]; - return ( -
- -
- ); - + const items = [ + { + key: "publicity", + label: ( + + + 宣传报道 + + ), + children: , + }, + { + key: "science", + label: ( + + + 常识科普 + + ), + children: , + }, + { + key: "example", + label: ( + + + 案例分析 + + ), + children: , + }, + { + key: "video", + label: ( + + + 文件共享 + + ), + children: , + }, + // { + // key: 'music', + // label: ( + // + // + // 音视频 + // + // ), + // children: + // }, + ]; + return ( +
+ +
+ ); } -export default PsychologyNav; \ No newline at end of file +export default PsychologyNav; diff --git a/apps/web/src/app/main/help/PublicityContent.tsx b/apps/web/src/app/main/help/PublicityContent.tsx deleted file mode 100755 index ebd6436..0000000 --- a/apps/web/src/app/main/help/PublicityContent.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export function PublicityContent() { - return ( -
-

宣传报道

- {/* 这里放课件列表内容 */} -
- ); - } \ No newline at end of file diff --git a/apps/web/src/app/main/help/ScienceContent.tsx b/apps/web/src/app/main/help/ScienceContent.tsx deleted file mode 100755 index 42c80da..0000000 --- a/apps/web/src/app/main/help/ScienceContent.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export function ScienceContent() { - return ( -
-

科普

- {/* 这里放视频列表内容 */} -
- ); - } \ No newline at end of file diff --git a/apps/web/src/app/main/help/VideoContent.tsx b/apps/web/src/app/main/help/VideoContent.tsx index 74fdaa0..3800758 100755 --- a/apps/web/src/app/main/help/VideoContent.tsx +++ b/apps/web/src/app/main/help/VideoContent.tsx @@ -18,7 +18,7 @@ import dayjs from "dayjs"; import { env } from "@web/src/env"; import { getFileIcon } from "@web/src/components/models/post/detail/utils"; import { formatFileSize, getCompressedImageUrl } from "@nice/utils"; -import { ResourceDto, RoleName } from "packages/common/dist"; +import { ResourceDto, RoleName, ResourceType } from "@nice/common"; import { useAuth } from "@web/src/providers/auth-provider"; const { TabPane } = Tabs; @@ -26,6 +26,7 @@ export function VideoContent() { const { isAuthenticated, user, hasSomePermissions } = useAuth(); const [fileIds, setFileIds] = useState([]); const [uploaderKey, setUploaderKey] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); // 分页状态 const [imagePage, setImagePage] = useState(1); @@ -41,7 +42,8 @@ export function VideoContent() { const { data: resources, refetch, - }: { data: ResourceDto[]; refetch: () => void } = + isLoading, + }: { data: ResourceDto[]; refetch: () => void; isLoading: boolean } = api.resource.findMany.useQuery({ where: { deletedAt: null, @@ -55,13 +57,14 @@ export function VideoContent() { // 处理资源数据 const { imageResources, fileResources, imagePagination, filePagination } = useMemo(() => { - if (!resources) + if (!resources) { return { imageResources: [], fileResources: [], imagePagination: { total: 0, data: [] }, filePagination: { total: 0, data: [] }, }; + } const isImage = (url: string) => /\.(png|jpg|jpeg|gif|webp)$/i.test(url); @@ -70,17 +73,34 @@ export function VideoContent() { if (!resource?.url) return null; const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`; const isImg = isImage(resource.url); + + // 确保 title 存在,优先使用 resource.title,然后是 resource.meta.filename + const displayTitle = + resource.title || resource.meta?.filename || "未命名文件"; + // 用于搜索的名称,确保从 meta.filename 获取(如果存在) + const searchableFilename = resource.meta?.filename || ""; + return { ...resource, url: isImg ? getCompressedImageUrl(original) : original, originalUrl: original, isImage: isImg, + title: displayTitle, // 用于显示 + searchableFilename: searchableFilename, // 用于搜索 }; }) .filter(Boolean); + // 根据搜索词筛选文件资源 (基于 searchableFilename) + const filteredFileResources = processedResources.filter( + (res) => + !res.isImage && + res.searchableFilename + .toLowerCase() + .includes(searchTerm.toLowerCase()) + ); + const allImageResources = processedResources.filter((res) => res.isImage); - const allFileResources = processedResources.filter((res) => !res.isImage); // 分页处理 const imageStart = (imagePage - 1) * pageSize; @@ -91,17 +111,20 @@ export function VideoContent() { imageStart, imageStart + pageSize ), - fileResources: allFileResources.slice(fileStart, fileStart + pageSize), + fileResources: filteredFileResources.slice( + fileStart, + fileStart + pageSize + ), imagePagination: { total: allImageResources.length, data: allImageResources, }, filePagination: { - total: allFileResources.length, - data: allFileResources, + total: filteredFileResources.length, + data: filteredFileResources, }, }; - }, [resources, imagePage, filePage]); + }, [resources, imagePage, filePage, searchTerm]); // searchTerm 作为依赖项 const createMutation = api.resource.create.useMutation({}); const handleSubmit = async () => { @@ -161,7 +184,7 @@ export function VideoContent() { } if (!isDomainAdmin) { - message.error("只有域管理员才能删除文件"); + message.error("只有管理员才能删除文件"); return; } @@ -196,11 +219,17 @@ export function VideoContent() {
-

资源上传

-

支持视频、图片、文档、PPT等多种格式文件

+ {/*

资源上传

*/} +

支持视频、excel、文档、ppt等多种格式文件

- + setSearchTerm(value)} + onChange={(e) => setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> {!isAuthenticated && ( {/* 图片资源展示 */} - {imagePagination?.total > 0 && ( + {/* {imagePagination?.total > 0 && (

图片列表

共 {imagePagination.total} 张图片 -
+
*/} - + {/* {imageResources.map((resource) => ( @@ -303,10 +332,10 @@ export function VideoContent() { ))} - + */} - {/* 图片分页 */} - {imagePagination.total > pageSize && ( + {/* 图片分页 */} + {/* {imagePagination.total > pageSize && (
)}
- )} + )} */} {/* 其他文件资源展示 */} - {filePagination?.total > 0 && ( + {isLoading ? ( +
加载中...
+ ) : filePagination?.total > 0 ? (
-
+

文件列表

共 {filePagination.total} 个文件
-
- {fileResources.map((resource) => ( -
-
- {getFileIcon(resource.url)} -
-
-
- {resource.title || "未命名文件"} + {fileResources.length > 0 && ( +
+ {fileResources.map((resource) => ( +
+
+ {getFileIcon(resource.url)}
- {resource.description && ( -
- 描述: {resource.description} +
+
+ {resource.title} +
+ {resource.description && ( +
+ 描述: {resource.description} +
+ )} +
+ + {dayjs(resource.createdAt).format("YYYY-MM-DD")} + + + {resource.meta?.size && + formatFileSize(resource.meta.size)} +
- )} -
- - {dayjs(resource.createdAt).format("YYYY-MM-DD")} - - - {resource.meta?.size && - formatFileSize(resource.meta.size)} -
-
-
- - {isDomainAdmin && ( +
+ {isDomainAdmin && ( +
-
- ))} -
+ ))} +
+ )} + + {fileResources.length === 0 && searchTerm && ( +
+ 未找到匹配 "{searchTerm}" 的文件。 +
+ )} {/* 文件分页 */} {filePagination.total > pageSize && ( @@ -396,6 +435,10 @@ export function VideoContent() {
)}
+ ) : ( +
+ {searchTerm ? `未找到匹配"${searchTerm}"的文件` : "暂无文件资源"} +
)}
diff --git a/apps/web/src/app/main/help/example/ExampleBasicForm.tsx b/apps/web/src/app/main/help/example/ExampleBasicForm.tsx new file mode 100644 index 0000000..eafea81 --- /dev/null +++ b/apps/web/src/app/main/help/example/ExampleBasicForm.tsx @@ -0,0 +1,139 @@ +import { Form, Input, Button, Tabs } from "antd"; +import { SendOutlined } from "@ant-design/icons"; +import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; +import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; +import TermSelect from "@web/src/components/models/term/term-select"; +import TabPane from "antd/es/tabs/TabPane"; +import toast from "react-hot-toast"; +import { useExampleEditor } from "./ExampleEditorContext"; +import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; +import { useNavigate } from "react-router-dom"; + +export function ExampleBasicForm() { + const { onSubmit, form } = useExampleEditor(); + const navigate = useNavigate(); + const handleFinish = async (values: any) => { + await onSubmit(values); + }; + + const handleSubmit = async () => { + try { + await form.validateFields(); + form.submit(); + navigate("/example"); + } catch (error) { + const errorMessages = (error as any).errorFields + .map((field) => field.errors[0]) + .filter(Boolean); + + toast.error( +
+ 表单校验失败: + {errorMessages.map((msg, i) => ( + · {msg} + ))} +
, + { + duration: 5000, + position: "top-center", + } + ); + } + }; + + return ( +
+
+ + + + + +
+
封面图片:
+ { + const meta = form.getFieldValue("meta") || {}; + form.setFieldValue("meta", { + ...meta, + coverImageUrl: coverUrl, + }); + }} + placeholder="点击上传" + style={{ width: "200px", height: "112px", borderRadius: "6px" }} + successText="封面上传成功" + value={form.getFieldValue("meta")?.coverImageUrl} + /> +
+
+ + + + +
+ form.setFieldValue("content", content)} + /> +
+
+
+ +
+ + { + form.setFieldValue("resources", resources); + }} + /> + +
+
+
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/help/example/ExampleCard.tsx b/apps/web/src/app/main/help/example/ExampleCard.tsx new file mode 100644 index 0000000..22a8a1f --- /dev/null +++ b/apps/web/src/app/main/help/example/ExampleCard.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + EyeOutlined, + LikeOutlined, + LikeFilled, + FileTextOutlined, + MailOutlined, + ReadOutlined, + FileImageOutlined, +} from "@ant-design/icons"; +import { Button, Typography, Space, Tooltip } from "antd"; +import { PostDto, PostStateLabels, ResourceDto } from "@nice/common"; +import dayjs from "dayjs"; +import PostLikeButton from "@web/src/components/models/post/detail/PostHeader/PostLikeButton"; +import { LetterBadge } from "@web/src/components/models/post/LetterBadge"; +import PostHateButton from "@web/src/components/models/post/detail/PostHeader/PostHateButton"; +import { env } from "@web/src/env"; +import { getCompressedImageUrl } from "@nice/utils"; +const { Title, Paragraph, Text } = Typography; +interface ExampleCardProps { + example: PostDto; +} +interface PostMeta { + coverImageUrl?: string; +} +export function ExampleCard({ example }: ExampleCardProps) { + const [debugInfo, setDebugInfo] = useState(""); + // 获取封面图片URL + const coverImageUrl = useMemo(() => { + // 首先检查meta中是否有封面URL + if (example.meta?.coverImageUrl) { + return example.meta.coverImageUrl; + } + // 如果meta中没有封面URL,回退到从resources中查找图片 + if (!example.resources || example.resources.length === 0) return null; + + // 查找第一个图片资源 + const imageResource = example.resources.find( + (resource) => + resource.url && /\.(png|jpg|jpeg|gif|webp)$/i.test(resource.url) + ); + + if (!imageResource || !imageResource.url) return null; + + // 构建原始URL + const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${imageResource.url}`; + + // 返回压缩后的URL + return getCompressedImageUrl(original); + }, [example.resources, example.meta?.coverImageUrl]); + useEffect(() => { + console.log("coverImageUrl", coverImageUrl); + }, [coverImageUrl]); + return ( +
{ + window.open(`/${example.id}/detail`); + }} + className="cursor-pointer p-3 bg-slate-100/80 rounded-xl hover:ring-blue-400 hover:ring-1 transition-all + duration-300 ease-in-out hover:-translate-y-0.5 + active:scale-[0.98] border border-white + group relative overflow-hidden h-full" + > +
+ {/* 左侧图片区域 */} +
+ {coverImageUrl ? ( + {example.title { + console.error("图片加载失败:", coverImageUrl); + // 显示占位图标 + e.currentTarget.style.display = "none"; + e.currentTarget.parentElement!.innerHTML = + '
'; + }} + /> + ) : ( +
+ +
+ )} +
+ + {/* 右侧内容区域 */} +
+
+ +
{example.title}
+
+ + {/* Badges & Interactions */} +
+
+ {example.terms && + example.terms.map((term) => ( + + ))} + +
+ +
+ + + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/main/help/example/ExampleContent.tsx b/apps/web/src/app/main/help/example/ExampleContent.tsx new file mode 100755 index 0000000..f034dcd --- /dev/null +++ b/apps/web/src/app/main/help/example/ExampleContent.tsx @@ -0,0 +1,130 @@ +import { Button, Input, Pagination } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import { api } from "@nice/client"; +import { PostType } from "@nice/common"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { ExampleCard } from "./ExampleCard"; +import { useState, useEffect, useMemo } from "react"; + +export function ExampleContent() { + const navigate = useNavigate(); + const { user, hasSomePermissions, isAuthenticated } = useAuth(); + const [exampleList, setExampleList] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const pageSize = 6; // 每页显示5条案例 + const isDomainAdmin = useMemo(() => { + return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); + }, [hasSomePermissions]); + const { data, isLoading } = api.post.findManyWithPagination.useQuery( + { + page: currentPage, + pageSize: pageSize, + where: { + type: PostType.EXAMPLE, + isPublic: true, + deletedAt: null, + title: searchTerm + ? { contains: searchTerm, mode: "insensitive" } + : undefined, + }, + orderBy: { + updatedAt: "desc", + }, + }, + { + enabled: true, + } + ); + + useEffect(() => { + if (data?.items) { + setExampleList(data.items); + setLoading(false); + } else if (!isLoading) { + setExampleList([]); + setLoading(false); + } + }, [data, isLoading]); + + useEffect(() => { + if (searchTerm) { + setCurrentPage(1); + } + }, [searchTerm]); + + const handleAddExample = () => { + navigate("/example"); + }; + + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const handleSearch = (value: string) => { + setSearchTerm(value); + }; + + return ( +
+
+ setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> + {isDomainAdmin && ( + + )} +
+ + {loading || isLoading ? ( +
加载中...
+ ) : exampleList.length > 0 ? ( + <> +
+ {exampleList.map((example) => ( + + ))} +
+ + {/* 分页组件 */} +
+ +
+ + ) : ( +
+ {searchTerm ? `未找到标题包含"${searchTerm}"的案例` : "暂无案例分析"} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/help/example/ExampleEditorContext.tsx b/apps/web/src/app/main/help/example/ExampleEditorContext.tsx new file mode 100644 index 0000000..a85bfe0 --- /dev/null +++ b/apps/web/src/app/main/help/example/ExampleEditorContext.tsx @@ -0,0 +1,105 @@ +import { createContext, useContext, ReactNode } from "react"; +import { Form, FormInstance } from "antd"; +import { api, usePost } from "@nice/client"; +import toast from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; +import { PostState, PostType } from "@nice/common"; +import dayjs from "dayjs"; + +export interface ExampleFormData { + title: string; + content: string; + resources?: string[]; + term?: string; + meta: { + tags: string[]; + }; +} + +interface ExampleEditorContextType { + onSubmit: (values: ExampleFormData) => Promise; + termId?: string; + form: FormInstance; +} + +interface ExampleFormProviderProps { + children: ReactNode; + termId?: string; +} + +const ExampleEditorContext = createContext(null); + +export function ExampleFormProvider({ children, termId }: ExampleFormProviderProps) { + const { create } = usePost(); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const onSubmit = async (data: ExampleFormData) => { + try { + const term = data?.term; + delete data.term; + + console.log("即将提交的资源IDs:", data.resources); + + const result = await create.mutateAsync({ + data: { + ...data, + type: PostType.EXAMPLE, + terms: term + ? { + connect: { + id: term, + }, + } + : undefined, + state: PostState.RESOLVED, // 案例直接设为已发布状态 + isPublic: true, // 案例永远是公开的 + resources: data.resources?.length + ? { + connect: (data.resources?.filter(Boolean) || []).map( + (fileId) => ({ + fileId, + }) + ), + } + : undefined, + }, + }); + + toast.success(`发布成功!`, { + duration: 3000, + }); + + navigate("/help", { + state: { + successMessage: "发布成功", + }, + }); + + form.resetFields(); + } catch (error) { + console.error("Error submitting form:", error); + toast.error("操作失败,请重试!"); + } + }; + + return ( + + {children} + + ); +} + +export const useExampleEditor = () => { + const context = useContext(ExampleEditorContext); + if (!context) { + throw new Error("useExampleEditor must be used within ExampleFormProvider"); + } + return context; +}; \ No newline at end of file diff --git a/apps/web/src/app/main/help/example/ExampleHeader.tsx b/apps/web/src/app/main/help/example/ExampleHeader.tsx new file mode 100644 index 0000000..75b820d --- /dev/null +++ b/apps/web/src/app/main/help/example/ExampleHeader.tsx @@ -0,0 +1,61 @@ +export default function ExampleHeader() { + return ( +
+
+
+

+ 案例发布 +

+
+ +
+
+ + + + 发布案例 +
+
+ + + + 分享经验 +
+
+ + + + 案例分析 +
+
+
+
+ ); + } \ No newline at end of file diff --git a/apps/web/src/app/main/help/example/page.tsx b/apps/web/src/app/main/help/example/page.tsx new file mode 100644 index 0000000..9c2ea76 --- /dev/null +++ b/apps/web/src/app/main/help/example/page.tsx @@ -0,0 +1,18 @@ +import { ExampleFormProvider } from "./ExampleEditorContext"; +import { ExampleBasicForm } from "./ExampleBasicForm"; +import ExampleHeader from "./ExampleHeader"; +import { useSearchParams } from "react-router-dom"; + +export default function ExampleEditorPage() { + const [searchParams] = useSearchParams(); + const termId = searchParams.get("termId"); + + return ( +
+ + + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/help/news/NewsBasicForm.tsx b/apps/web/src/app/main/help/news/NewsBasicForm.tsx new file mode 100644 index 0000000..bd832be --- /dev/null +++ b/apps/web/src/app/main/help/news/NewsBasicForm.tsx @@ -0,0 +1,146 @@ +import { Form, Input, Button, Tabs } from "antd"; +import { SendOutlined } from "@ant-design/icons"; +import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; +import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; +import TermSelect from "@web/src/components/models/term/term-select"; +import TabPane from "antd/es/tabs/TabPane"; +import toast from "react-hot-toast"; +import { useNewsEditor } from "./NewsEditorContext"; +import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; + +export function NewsBasicForm() { + const { onSubmit, form } = useNewsEditor(); + const handleFinish = async (values: any) => { + await onSubmit(values); + }; + + const handleSubmit = async () => { + try { + await form.validateFields(); + form.submit(); + } catch (error) { + const errorMessages = (error as any).errorFields + .map((field) => field.errors[0]) + .filter(Boolean); + + toast.error( +
+ 表单校验失败: + {errorMessages.map((msg, i) => ( + · {msg} + ))} +
, + { + duration: 5000, + position: "top-center", + } + ); + } + }; + + return ( +
+
+ {/*
+ + + +
*/} + + + + + + +
+
封面图片:
+ { + const meta = form.getFieldValue("meta") || {}; + form.setFieldValue("meta", { + ...meta, + coverImageUrl: coverUrl, + }); + }} + placeholder="点击上传" + style={{ width: "200px", height: "112px", borderRadius: "6px" }} + successText="封面上传成功" + value={form.getFieldValue("meta")?.coverImageUrl} + /> +
+
+ + + + +
+ form.setFieldValue("content", content)} + /> +
+
+
+ +
+ + { + form.setFieldValue("resources", resources); + }} + /> + +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/apps/web/src/app/main/help/news/NewsCard.tsx b/apps/web/src/app/main/help/news/NewsCard.tsx new file mode 100644 index 0000000..1a3205f --- /dev/null +++ b/apps/web/src/app/main/help/news/NewsCard.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + EyeOutlined, + LikeOutlined, + LikeFilled, + FileTextOutlined, + MailOutlined, + NotificationOutlined, + FileImageOutlined, +} from "@ant-design/icons"; +import { Button, Typography, Space, Tooltip } from "antd"; +import { PostDto, PostStateLabels, ResourceDto } from "@nice/common"; +import dayjs from "dayjs"; +import PostLikeButton from "@web/src/components/models/post/detail/PostHeader/PostLikeButton"; +import { LetterBadge } from "@web/src/components/models/post/LetterBadge"; +import PostHateButton from "@web/src/components/models/post/detail/PostHeader/PostHateButton"; +import { env } from "@web/src/env"; +import { getCompressedImageUrl } from "@nice/utils"; +const { Title, Paragraph, Text } = Typography; +interface NewsCardProps { + news: PostDto; +} +interface PostMeta { + coverImageUrl?: string; +} +export function NewsCard({ news }: NewsCardProps) { + const [debugInfo, setDebugInfo] = useState(""); + // 获取封面图片URL + const coverImageUrl = useMemo(() => { + // 首先检查meta中是否有封面URL + if (news.meta?.coverImageUrl) { + return news.meta.coverImageUrl; + } + // 如果meta中没有封面URL,回退到从resources中查找图片 + if (!news.resources || news.resources.length === 0) return null; + + // 查找第一个图片资源 + const imageResource = news.resources.find( + (resource) => + resource.url && /\.(png|jpg|jpeg|gif|webp)$/i.test(resource.url) + ); + + if (!imageResource || !imageResource.url) return null; + + // 构建原始URL + const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${imageResource.url}`; + + // 返回压缩后的URL + return getCompressedImageUrl(original); + }, [news.resources, news.meta?.coverImageUrl]); + useEffect(() => { + console.log("coverImageUrl", coverImageUrl); + }, [coverImageUrl]); + return ( +
{ + window.open(`/${news.id}/detail`); + }} + className="cursor-pointer p-3 bg-slate-100/80 rounded-xl hover:ring-blue-400 hover:ring-1 transition-all + duration-300 ease-in-out hover:-translate-y-0.5 + active:scale-[0.98] border border-white + group relative overflow-hidden h-full" + > +
+ {/* 左侧图片区域 */} +
+ {coverImageUrl ? ( + {news.title { + console.error("图片加载失败:", coverImageUrl); + // 显示占位图标 + e.currentTarget.style.display = "none"; + e.currentTarget.parentElement!.innerHTML = + '
'; + }} + /> + ) : ( +
+ +
+ )} +
+ + {/* 右侧内容区域 */} +
+
+ +
{news.title}
+
+ + {/* Badges & Interactions */} +
+
+ {news.terms && + news.terms.map((term) => ( + + ))} + +
+ +
+ + + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/main/help/news/NewsEditorContext.tsx b/apps/web/src/app/main/help/news/NewsEditorContext.tsx new file mode 100644 index 0000000..09e4335 --- /dev/null +++ b/apps/web/src/app/main/help/news/NewsEditorContext.tsx @@ -0,0 +1,105 @@ +import { createContext, useContext, ReactNode } from "react"; +import { Form, FormInstance } from "antd"; +import { api, usePost } from "@nice/client"; +import toast from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; +import { PostState, PostType } from "@nice/common"; +import dayjs from "dayjs"; + +export interface NewsFormData { + title: string; + content: string; + resources?: string[]; + term?: string; + meta: { + tags: string[]; + }; +} + +interface NewsEditorContextType { + onSubmit: (values: NewsFormData) => Promise; + termId?: string; + form: FormInstance; +} + +interface NewsFormProviderProps { + children: ReactNode; + termId?: string; +} + +const NewsEditorContext = createContext(null); + +export function NewsFormProvider({ children, termId }: NewsFormProviderProps) { + const { create } = usePost(); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const onSubmit = async (data: NewsFormData) => { + try { + const term = data?.term; + delete data.term; + + console.log("即将提交的资源IDs:", data.resources); + + const result = await create.mutateAsync({ + data: { + ...data, + type: PostType.NEW, + terms: term + ? { + connect: { + id: term, + }, + } + : undefined, + state: PostState.RESOLVED, // 新闻直接设为已发布状态 + isPublic: true, // 新闻永远是公开的 + resources: data.resources?.length + ? { + connect: (data.resources?.filter(Boolean) || []).map( + (fileId) => ({ + fileId, + }) + ), + } + : undefined, + }, + }); + + toast.success(`发布成功!`, { + duration: 3000, + }); + + navigate("/help", { + state: { + successMessage: "发布成功", + }, + }); + + form.resetFields(); + } catch (error) { + console.error("Error submitting form:", error); + toast.error("操作失败,请重试!"); + } + }; + + return ( + + {children} + + ); +} + +export const useNewsEditor = () => { + const context = useContext(NewsEditorContext); + if (!context) { + throw new Error("useNewsEditor must be used within NewsFormProvider"); + } + return context; +}; diff --git a/apps/web/src/app/main/help/news/NewsHeader.tsx b/apps/web/src/app/main/help/news/NewsHeader.tsx new file mode 100644 index 0000000..987c862 --- /dev/null +++ b/apps/web/src/app/main/help/news/NewsHeader.tsx @@ -0,0 +1,61 @@ +export default function NewsHeader() { + return ( +
+
+
+

+ 新闻发布 +

+
+ +
+
+ + + + 发布资讯 +
+
+ + + + 分享活动 +
+
+ + + + 信息共享 +
+
+
+
+ ); + } \ No newline at end of file diff --git a/apps/web/src/app/main/help/news/PublicityContent.tsx b/apps/web/src/app/main/help/news/PublicityContent.tsx new file mode 100755 index 0000000..abd30d6 --- /dev/null +++ b/apps/web/src/app/main/help/news/PublicityContent.tsx @@ -0,0 +1,130 @@ +import { Button, Input, Pagination } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import { api } from "@nice/client"; +import { PostType } from "@nice/common"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { NewsCard } from "./NewsCard"; +import { useState, useEffect, useMemo } from "react"; + +export function PublicityContent() { + const navigate = useNavigate(); + const { user, hasSomePermissions, isAuthenticated } = useAuth(); + const [newsList, setNewsList] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const pageSize = 6; // 每页显示5条新闻 + const isDomainAdmin = useMemo(() => { + return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); + }, [hasSomePermissions]); + const { data, isLoading } = api.post.findManyWithPagination.useQuery( + { + page: currentPage, + pageSize: pageSize, + where: { + type: PostType.NEW, + isPublic: true, + deletedAt: null, + title: searchTerm + ? { contains: searchTerm, mode: "insensitive" } + : undefined, + }, + orderBy: { + updatedAt: "desc", + }, + }, + { + enabled: true, + } + ); + + useEffect(() => { + if (data?.items) { + setNewsList(data.items); + setLoading(false); + } else if (!isLoading) { + setNewsList([]); + setLoading(false); + } + }, [data, isLoading]); + + useEffect(() => { + if (searchTerm) { + setCurrentPage(1); + } + }, [searchTerm]); + + const handleAddNews = () => { + navigate("/news"); + }; + + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const handleSearch = (value: string) => { + setSearchTerm(value); + }; + + return ( +
+
+ setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> + {isDomainAdmin && ( + + )} +
+ + {loading || isLoading ? ( +
加载中...
+ ) : newsList.length > 0 ? ( + <> +
+ {newsList.map((news) => ( + + ))} +
+ + {/* 分页组件 */} +
+ +
+ + ) : ( +
+ {searchTerm ? `未找到标题包含"${searchTerm}"的新闻` : "暂无新闻报道"} +
+ )} +
+ ); +} diff --git a/apps/web/src/app/main/help/news/page.tsx b/apps/web/src/app/main/help/news/page.tsx new file mode 100644 index 0000000..2da78d9 --- /dev/null +++ b/apps/web/src/app/main/help/news/page.tsx @@ -0,0 +1,18 @@ +import { NewsFormProvider } from "./NewsEditorContext"; +import { NewsBasicForm } from "./NewsBasicForm"; +import NewsHeader from "./NewsHeader"; +import { useSearchParams } from "react-router-dom"; + +export default function NewsEditorPage() { + const [searchParams] = useSearchParams(); + const termId = searchParams.get("termId"); + + return ( +
+ + + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/help/science/ScienceBasicForm.tsx b/apps/web/src/app/main/help/science/ScienceBasicForm.tsx new file mode 100644 index 0000000..2650e3f --- /dev/null +++ b/apps/web/src/app/main/help/science/ScienceBasicForm.tsx @@ -0,0 +1,139 @@ +import { Form, Input, Button, Tabs } from "antd"; +import { SendOutlined } from "@ant-design/icons"; +import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; +import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; +import TermSelect from "@web/src/components/models/term/term-select"; +import TabPane from "antd/es/tabs/TabPane"; +import toast from "react-hot-toast"; +import { useScienceEditor } from "./ScienceEditorContext"; +import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; +import { useNavigate } from "react-router-dom"; + +export function ScienceBasicForm() { + const { onSubmit, form } = useScienceEditor(); + const navigate = useNavigate(); + const handleFinish = async (values: any) => { + await onSubmit(values); + }; + + const handleSubmit = async () => { + try { + await form.validateFields(); + form.submit(); + navigate("/help/science"); + } catch (error) { + const errorMessages = (error as any).errorFields + .map((field) => field.errors[0]) + .filter(Boolean); + + toast.error( +
+ 表单校验失败: + {errorMessages.map((msg, i) => ( + · {msg} + ))} +
, + { + duration: 5000, + position: "top-center", + } + ); + } + }; + + return ( +
+
+ + + + + +
+
封面图片:
+ { + const meta = form.getFieldValue("meta") || {}; + form.setFieldValue("meta", { + ...meta, + coverImageUrl: coverUrl, + }); + }} + placeholder="点击上传" + style={{ width: "200px", height: "112px", borderRadius: "6px" }} + successText="封面上传成功" + value={form.getFieldValue("meta")?.coverImageUrl} + /> +
+
+ + + + +
+ form.setFieldValue("content", content)} + /> +
+
+
+ +
+ + { + form.setFieldValue("resources", resources); + }} + /> + +
+
+
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/help/science/ScienceCard.tsx b/apps/web/src/app/main/help/science/ScienceCard.tsx new file mode 100644 index 0000000..7356358 --- /dev/null +++ b/apps/web/src/app/main/help/science/ScienceCard.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + EyeOutlined, + LikeOutlined, + LikeFilled, + FileTextOutlined, + BulbOutlined, + FileImageOutlined, +} from "@ant-design/icons"; +import { Button, Typography, Space, Tooltip } from "antd"; +import { PostDto, PostStateLabels, ResourceDto, PostMeta } from "@nice/common"; +import dayjs from "dayjs"; +import PostLikeButton from "@web/src/components/models/post/detail/PostHeader/PostLikeButton"; +import { LetterBadge } from "@web/src/components/models/post/LetterBadge"; +import PostHateButton from "@web/src/components/models/post/detail/PostHeader/PostHateButton"; +import { env } from "@web/src/env"; +import { getCompressedImageUrl } from "@nice/utils"; +const { Title, Paragraph, Text } = Typography; + +interface ScienceCardProps { + science: PostDto; +} + +export function ScienceCard({ science }: ScienceCardProps) { + const [debugInfo, setDebugInfo] = useState(""); + + // 获取封面图片URL + const coverImageUrl = useMemo(() => { + // 首先检查meta中是否有封面URL + if (science.meta?.coverImageUrl) { + return science.meta.coverImageUrl; + } + // 如果meta中没有封面URL,回退到从resources中查找图片 + if (!science.resources || science.resources.length === 0) return null; + + // 查找第一个图片资源 + const imageResource = science.resources.find( + (resource) => + resource.url && /\.(png|jpg|jpeg|gif|webp)$/i.test(resource.url) + ); + + if (!imageResource || !imageResource.url) return null; + + // 构建原始URL + const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${imageResource.url}`; + + // 返回压缩后的URL + return getCompressedImageUrl(original); + }, [science.resources, science.meta?.coverImageUrl]); + + useEffect(() => { + console.log("coverImageUrl", coverImageUrl); + }, [coverImageUrl]); + + return ( +
{ + window.open(`/${science.id}/detail`); + }} + className="cursor-pointer p-3 bg-slate-100/80 rounded-xl hover:ring-blue-400 hover:ring-1 transition-all + duration-300 ease-in-out hover:-translate-y-0.5 + active:scale-[0.98] border border-white + group relative overflow-hidden h-full" + > +
+ {/* 左侧图片区域 */} +
+ {coverImageUrl ? ( + {science.title { + console.error("图片加载失败:", coverImageUrl); + // 显示占位图标 + e.currentTarget.style.display = "none"; + e.currentTarget.parentElement!.innerHTML = + '
'; + }} + /> + ) : ( +
+ +
+ )} +
+ + {/* 右侧内容区域 */} +
+
+ +
{science.title}
+
+ + {/* 内容预览 */} + {/* {science.content && ( +
+ {science.content.replace(/<[^>]*>/g, "")} +
+ )} */} + + {/* Badges & Interactions */} +
+
+ {science.terms && + science.terms.map((term) => ( + + ))} + + {science.meta?.tags && + science.meta.tags.length > 0 && + science.meta.tags.map((tag, index) => ( + + ))} +
+
+ + + {/* {science.author && ( + +
+ {science.author.showname || science.author.username} +
+
+ )} */} +
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/main/help/science/ScienceContent.tsx b/apps/web/src/app/main/help/science/ScienceContent.tsx new file mode 100755 index 0000000..7ba3c3d --- /dev/null +++ b/apps/web/src/app/main/help/science/ScienceContent.tsx @@ -0,0 +1,131 @@ +import { Button, Input, Pagination } from "antd"; +import { PlusOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import { api } from "@nice/client"; +import { PostType } from "@nice/common"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { ScienceCard } from "./ScienceCard"; +import { useState, useEffect, useMemo } from "react"; + +export function ScienceContent() { + const navigate = useNavigate(); + const { user, hasSomePermissions, isAuthenticated } = useAuth(); + const [scienceList, setScienceList] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const pageSize = 6; // 每页显示5条科普 + const isDomainAdmin = useMemo(() => { + return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); + }, [hasSomePermissions]); + + const { data, isLoading } = api.post.findManyWithPagination.useQuery( + { + page: currentPage, + pageSize: pageSize, + where: { + type: PostType.SCIENCE, + isPublic: true, + deletedAt: null, + title: searchTerm + ? { contains: searchTerm, mode: "insensitive" } + : undefined, + }, + orderBy: { + updatedAt: "desc", + }, + }, + { + enabled: true, + } + ); + + useEffect(() => { + if (data?.items) { + setScienceList(data.items); + setLoading(false); + } else if (!isLoading) { + setScienceList([]); + setLoading(false); + } + }, [data, isLoading]); + + useEffect(() => { + if (searchTerm) { + setCurrentPage(1); + } + }, [searchTerm]); + + const handleAddScience = () => { + navigate("/science"); + }; + + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const handleSearch = (value: string) => { + setSearchTerm(value); + }; + + return ( +
+
+ setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> + {isDomainAdmin && ( + + )} +
+ + {loading || isLoading ? ( +
加载中...
+ ) : scienceList.length > 0 ? ( + <> +
+ {scienceList.map((science) => ( + + ))} +
+ + {/* 分页组件 */} +
+ +
+ + ) : ( +
+ {searchTerm ? `未找到标题包含"${searchTerm}"的科普内容` : "暂无科普内容"} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/help/science/ScienceEditorContext.tsx b/apps/web/src/app/main/help/science/ScienceEditorContext.tsx new file mode 100644 index 0000000..ab3b8b2 --- /dev/null +++ b/apps/web/src/app/main/help/science/ScienceEditorContext.tsx @@ -0,0 +1,106 @@ +import { createContext, useContext, ReactNode } from "react"; +import { Form, FormInstance } from "antd"; +import { api, usePost } from "@nice/client"; +import toast from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; +import { PostState, PostType } from "@nice/common"; +import dayjs from "dayjs"; + +export interface ScienceFormData { + title: string; + content: string; + resources?: string[]; + term?: string; + meta: { + tags: string[]; + videoUrl?: string; + }; +} + +interface ScienceEditorContextType { + onSubmit: (values: ScienceFormData) => Promise; + termId?: string; + form: FormInstance; +} + +interface ScienceFormProviderProps { + children: ReactNode; + termId?: string; +} + +const ScienceEditorContext = createContext(null); + +export function ScienceFormProvider({ children, termId }: ScienceFormProviderProps) { + const { create } = usePost(); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const onSubmit = async (data: ScienceFormData) => { + try { + const term = data?.term; + delete data.term; + + console.log("即将提交的资源IDs:", data.resources); + + const result = await create.mutateAsync({ + data: { + ...data, + type: PostType.SCIENCE, + terms: term + ? { + connect: { + id: term, + }, + } + : undefined, + state: PostState.RESOLVED, + isPublic: true, + resources: data.resources?.length + ? { + connect: (data.resources?.filter(Boolean) || []).map( + (fileId) => ({ + fileId, + }) + ), + } + : undefined, + }, + }); + + toast.success(`发布成功!`, { + duration: 3000, + }); + + navigate("/help", { + state: { + successMessage: "发布成功", + }, + }); + + form.resetFields(); + } catch (error) { + console.error("Error submitting form:", error); + toast.error("操作失败,请重试!"); + } + }; + + return ( + + {children} + + ); +} + +export const useScienceEditor = () => { + const context = useContext(ScienceEditorContext); + if (!context) { + throw new Error("useScienceEditor must be used within ScienceFormProvider"); + } + return context; +}; \ No newline at end of file diff --git a/apps/web/src/app/main/help/science/ScienceHeader.tsx b/apps/web/src/app/main/help/science/ScienceHeader.tsx new file mode 100644 index 0000000..e4a39bd --- /dev/null +++ b/apps/web/src/app/main/help/science/ScienceHeader.tsx @@ -0,0 +1,61 @@ +export default function ScienceHeader() { + return ( +
+
+
+

+ 科普发布 +

+
+ +
+
+ + + + 发布科普 +
+
+ + + + 学习常识 +
+
+ + + + 知识分享 +
+
+
+
+ ); + } \ No newline at end of file diff --git a/apps/web/src/app/main/help/science/page.tsx b/apps/web/src/app/main/help/science/page.tsx new file mode 100644 index 0000000..3ae3b4c --- /dev/null +++ b/apps/web/src/app/main/help/science/page.tsx @@ -0,0 +1,18 @@ +import { ScienceFormProvider } from "./ScienceEditorContext"; +import { ScienceBasicForm } from "./ScienceBasicForm"; +import ScienceHeader from "./ScienceHeader"; +import { useSearchParams } from "react-router-dom"; + +export default function ScienceEditorPage() { + const [searchParams] = useSearchParams(); + const termId = searchParams.get("termId"); + + return ( +
+ + + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx index faa368a..c40073e 100755 --- a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx @@ -143,7 +143,7 @@ export function StatsSection() { diff --git a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx index 63c70b7..7bfcbd7 100755 --- a/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx @@ -20,7 +20,7 @@ export function LetterBasicForm() { const { data: enabledStaffIds, isLoading: roleMapIsLoading } = api.rolemap.getStaffIdsByRoleNames.useQuery({ - roleNames: [RoleName.Leader, RoleName.Organization, RoleName.RootAdmin], + roleNames: [RoleName.Leader, RoleName.Organization, RoleName.DomainAdmin], }); const handleSubmit = async () => { try { diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index e3b6737..53988c8 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -14,6 +14,10 @@ import InboxPage from "../app/main/letter/inbox/page"; import OutboxPage from "../app/main/letter/outbox/page"; import IndexPage from "../app/main/letter/index/page"; import SubmissionSuccess from "../app/SubmissionSuccess"; +import NewsEditorPage from "../app/main/help/news/page"; +import ScienceEditorPage from "../app/main/help/science/page"; +import ExampleEditorPage from "../app/main/help/example/page"; +import { NewsCard } from "../app/main/help/news/NewsCard"; export const routes: CustomRouteObject[] = [ { path: "/", @@ -64,6 +68,19 @@ export const routes: CustomRouteObject[] = [ { path: "help", element: , + + }, + { + path: "news", // 新添加的路由 + element: , + }, + { + path: "science", // 新添加的路由 + element: , + }, + { + path: "example", // 新添加的路由 + element: , }, { path: "submission-success", diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 4d66418..fc54227 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -5,6 +5,9 @@ export enum PostType { POST = "post", POST_COMMENT = "post_comment", COURSE_REVIEW = "course_review", + NEW = "new", + SCIENCE = "science", + EXAMPLE = "example", } export enum TaxonomySlug { CATEGORY = "category", diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index ca23407..5717ce6 100755 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -240,6 +240,7 @@ export interface PostMeta { ip?: string; tags?: string[]; ownCode?: string; + coverImageUrl?: string; } export type RowModelResult = { rowData: any[];