visuddhinanda 1 month ago
parent
commit
a022b1d4a9

+ 27 - 0
dashboard-v6/src/api/Term.ts

@@ -1,5 +1,6 @@
 import type { IStudio, IUser, TRole } from "./Auth";
 import type { IStudio, IUser, TRole } from "./Auth";
 import type { IChannel } from "./Channel";
 import type { IChannel } from "./Channel";
+import { get } from "../request";
 
 
 export interface ITerm {
 export interface ITerm {
   id?: string;
   id?: string;
@@ -37,6 +38,7 @@ export interface ITermDataRequest {
   copy_lang?: string;
   copy_lang?: string;
   pr?: boolean;
   pr?: boolean;
 }
 }
+
 export interface ITermDataResponse {
 export interface ITermDataResponse {
   id: number;
   id: number;
   guid: string;
   guid: string;
@@ -59,11 +61,13 @@ export interface ITermDataResponse {
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;
 }
 }
+
 export interface ITermResponse {
 export interface ITermResponse {
   ok: boolean;
   ok: boolean;
   message: string;
   message: string;
   data: ITermDataResponse;
   data: ITermDataResponse;
 }
 }
+
 export interface ITermListResponse {
 export interface ITermListResponse {
   ok: boolean;
   ok: boolean;
   message: string;
   message: string;
@@ -77,10 +81,12 @@ interface IMeaningCount {
   meaning: string;
   meaning: string;
   count: number;
   count: number;
 }
 }
+
 interface IStudioChannel {
 interface IStudioChannel {
   name: string;
   name: string;
   uid: string;
   uid: string;
 }
 }
+
 export interface ITermCreate {
 export interface ITermCreate {
   word: string;
   word: string;
   meaningCount: IMeaningCount[];
   meaningCount: IMeaningCount[];
@@ -88,6 +94,7 @@ export interface ITermCreate {
   language: string;
   language: string;
   studio: IStudio;
   studio: IStudio;
 }
 }
+
 export interface ITermCreateResponse {
 export interface ITermCreateResponse {
   ok: boolean;
   ok: boolean;
   message: string;
   message: string;
@@ -98,3 +105,23 @@ export interface ITermDeleteRequest {
   uuid: boolean;
   uuid: boolean;
   id: string[];
   id: string[];
 }
 }
+
+// ---------- API ----------
+
+export interface IGetTermParams {
+  id: string;
+  mode: "read" | "edit";
+  channelsId?: string | null;
+}
+
+export function getTerm({
+  id,
+  mode,
+  channelsId,
+}: IGetTermParams): Promise<ITermResponse> {
+  const url =
+    `/api/v2/terms/${id}?mode=${mode}` +
+    (channelsId ? `&channel=${channelsId}` : "");
+
+  return get<ITermResponse>(url);
+}

+ 41 - 0
dashboard-v6/src/components/article/TypeTerm.tsx

@@ -0,0 +1,41 @@
+import type { ArticleMode } from "../../api/Article";
+
+import "./article.css";
+import ArticleLayout from "./components/ArticleLayout";
+import { useTerm } from "./hooks/useTerm";
+
+interface IWidget {
+  id?: string;
+  mode?: ArticleMode | null;
+  channelId?: string | null;
+}
+
+const TypeTermWidget = ({ channelId, id, mode = "read" }: IWidget) => {
+  const { articleData, articleHtml, errorCode, loading } = useTerm({
+    id,
+    channelId,
+    mode,
+  });
+
+  const channels = channelId?.split("_");
+
+  return (
+    <div>
+      <ArticleLayout
+        title={articleData?.title}
+        subTitle={articleData?.subtitle}
+        content={articleData?.content ?? ""}
+        html={articleHtml}
+        path={articleData?.path}
+        editor={articleData?.editor}
+        created_at={articleData?.created_at}
+        updated_at={articleData?.updated_at}
+        channels={channels}
+        loading={loading}
+        errorCode={errorCode}
+      />
+    </div>
+  );
+};
+
+export default TypeTermWidget;

+ 141 - 0
dashboard-v6/src/components/article/components/ArticleLayout.tsx

@@ -0,0 +1,141 @@
+import { Typography, Divider, Skeleton, Space } from "antd";
+import type { ITocPathNode } from "../../../api/pali-text";
+import type { IStudio, IUser } from "../../../api/Auth";
+import TocPath from "../../tipitaka/TocPath";
+import VisibleObserver from "../../general/VisibleObserver";
+import MdView from "../../general/MdView";
+import type { JSX } from "react";
+import ArticleSkeleton from "./ArticleSkeleton";
+import ErrorResult from "../../general/ErrorResult";
+import User from "../../auth/User";
+
+const { Paragraph, Title, Text } = Typography;
+export interface IFirstAnthology {
+  id: string;
+  title: string;
+  count: number;
+}
+export interface IWidgetArticleData {
+  title?: string;
+  subTitle?: string;
+  summary?: string | null;
+  content?: string;
+  html?: string[];
+  path?: ITocPathNode[];
+  resList?: JSX.Element;
+  created_at?: string;
+  updated_at?: string;
+  editor?: IUser;
+  owner?: IStudio;
+  channels?: string[];
+  loading?: boolean;
+  errorCode?: number;
+  remains?: boolean;
+  anthology?: IFirstAnthology;
+  hideTitle?: boolean;
+  onEnd?: () => void;
+  onPathChange?: (
+    node: ITocPathNode,
+    e: React.MouseEvent<HTMLSpanElement | HTMLAnchorElement, MouseEvent>
+  ) => void;
+}
+
+const ArticleLayout = ({
+  title = "",
+  subTitle,
+  summary,
+  content,
+  html = [],
+  path = [],
+  editor,
+  updated_at,
+  resList,
+  channels,
+  loading,
+  errorCode,
+  hideTitle,
+  remains,
+  onEnd,
+  onPathChange,
+}: IWidgetArticleData) => {
+  console.log("ArticleViewWidget render");
+
+  return (
+    <>
+      {loading ? (
+        <ArticleSkeleton />
+      ) : errorCode ? (
+        <ErrorResult code={errorCode} />
+      ) : (
+        <div>
+          <Space orientation="vertical">
+            {hideTitle ? (
+              <></>
+            ) : (
+              <TocPath
+                data={path}
+                channels={channels}
+                onChange={(
+                  node: ITocPathNode,
+                  e: React.MouseEvent<
+                    HTMLSpanElement | HTMLAnchorElement,
+                    MouseEvent
+                  >
+                ) => {
+                  if (typeof onPathChange !== "undefined") {
+                    onPathChange(node, e);
+                  }
+                }}
+              />
+            )}
+            {hideTitle ? (
+              <></>
+            ) : (
+              <Title level={4}>
+                <div
+                  dangerouslySetInnerHTML={{
+                    __html: title ?? "",
+                  }}
+                />
+              </Title>
+            )}
+
+            <Text type="secondary">{subTitle}</Text>
+            {resList}
+            <Paragraph ellipsis={{ rows: 2, expandable: true, symbol: "more" }}>
+              {summary}
+            </Paragraph>
+            <Space>
+              <User {...editor} /> edit at {updated_at}
+            </Space>
+            <Divider />
+          </Space>
+          {html
+            ? html.map((item, id) => {
+                return (
+                  <div key={id}>
+                    <MdView className="pcd_article" html={item} />
+                  </div>
+                );
+              })
+            : content}
+          {remains ? (
+            <>
+              <VisibleObserver
+                onVisible={(visible: boolean) => {
+                  console.log("visible", visible);
+                  if (visible && typeof onEnd !== "undefined") {
+                    onEnd();
+                  }
+                }}
+              />
+              <Skeleton title={{ width: 200 }} paragraph={{ rows: 5 }} active />
+            </>
+          ) : undefined}
+        </div>
+      )}
+    </>
+  );
+};
+
+export default ArticleLayout;

+ 13 - 0
dashboard-v6/src/components/article/components/ArticleSkeleton.tsx

@@ -0,0 +1,13 @@
+import { Divider, Skeleton } from "antd";
+
+const ArticleSkeletonWidget = () => {
+  return (
+    <div style={{ paddingTop: "1em" }}>
+      <Skeleton title={{ width: 200 }} paragraph={{ rows: 1 }} active />
+      <Divider />
+      <Skeleton title={{ width: 200 }} paragraph={{ rows: 10 }} active />
+    </div>
+  );
+};
+
+export default ArticleSkeletonWidget;

+ 65 - 0
dashboard-v6/src/components/article/hooks/useTerm.ts

@@ -0,0 +1,65 @@
+import { useEffect, useState, useTransition } from "react";
+
+import type { ArticleMode, IArticleDataResponse } from "../../../api/Article";
+import { getTerm } from "../../../api/Term";
+import { message } from "antd";
+
+interface IUseTermOptions {
+  id?: string;
+  mode?: ArticleMode | null;
+  channelId?: string | null;
+}
+
+export function useTerm({ id, mode = "read", channelId }: IUseTermOptions) {
+  const [articleData, setArticleData] = useState<IArticleDataResponse>();
+  const [articleHtml, setArticleHtml] = useState<string[]>(["<span />"]);
+  const [errorCode, setErrorCode] = useState<number>();
+  const [isPending, startTransition] = useTransition();
+
+  useEffect(() => {
+    if (typeof id === "undefined") return;
+
+    const queryMode = mode === "edit" || mode === "wbw" ? "edit" : "read";
+
+    startTransition(async () => {
+      try {
+        const json = await getTerm({
+          id: id,
+          mode: queryMode,
+          channelsId: channelId,
+        });
+
+        if (!json.ok) {
+          message.error(json.message);
+          return;
+        }
+
+        const { data } = json;
+
+        setArticleData({
+          uid: data.guid,
+          title: data.meaning,
+          subtitle: data.word,
+          summary: data.note,
+          content: data.note ?? "",
+          content_type: "markdown",
+          html: data.html,
+          path: [],
+          editor: data.editor,
+          status: 30,
+          lang: data.language,
+          created_at: data.created_at,
+          updated_at: data.updated_at,
+        });
+
+        setArticleHtml(
+          data.html ? [data.html] : data.note ? [data.note] : ["<span />"]
+        );
+      } catch (e) {
+        setErrorCode(e as number);
+      }
+    });
+  }, [id, channelId, mode]);
+
+  return { articleData, articleHtml, errorCode, loading: isPending };
+}

+ 49 - 0
dashboard-v6/src/components/general/ErrorResult copy.tsx

@@ -0,0 +1,49 @@
+import { Result } from "antd";
+import type { ResultStatusType } from "antd/lib/result"
+import { useIntl } from "react-intl";
+
+interface IWidget {
+  code: number;
+  message?: string;
+}
+
+const ErrorResultWidget = ({ code, message }: IWidget) => {
+  const intl = useIntl();
+  let strStatus: ResultStatusType;
+  let strTitle: string = "";
+  switch (code) {
+    case 401:
+      strStatus = 403;
+      strTitle = intl.formatMessage({ id: "labels.error.401" });
+      break;
+    case 403:
+      strStatus = 403;
+      strTitle = intl.formatMessage({ id: "labels.error.403" });
+      break;
+    case 404:
+      strStatus = 404;
+      strTitle = intl.formatMessage({ id: "labels.error.404" });
+      break;
+    case 500:
+      strStatus = 500;
+      strTitle = intl.formatMessage({ id: "labels.error.500" });
+      break;
+    case 429:
+      strStatus = "error";
+      strTitle = intl.formatMessage({ id: "labels.error.429" });
+      break;
+    default:
+      strStatus = "error";
+      strTitle = "无法识别的错误代码" + code;
+      break;
+  }
+  return (
+    <Result
+      status={strStatus}
+      title={strTitle}
+      subTitle={message ? message : "Sorry, something went wrong."}
+    />
+  );
+};
+
+export default ErrorResultWidget;

+ 44 - 0
dashboard-v6/src/components/general/VisibleObserver.tsx

@@ -0,0 +1,44 @@
+import { useEffect, useRef, useState, type RefObject } from "react";
+
+const useOnScreen = (ref: RefObject<HTMLElement | null>) => {
+  const [isIntersecting, setIntersecting] = useState(false);
+
+  useEffect(() => {
+    const observer = new IntersectionObserver(([entry]) =>
+      setIntersecting(entry.isIntersecting)
+    );
+
+    if (ref.current) {
+      observer.observe(ref.current);
+    }
+
+    return () => {
+      observer.disconnect();
+    };
+  }, [ref]);
+
+  return isIntersecting;
+};
+
+interface IWidget {
+  onVisible?: (isVisible: boolean) => void;
+}
+
+const VisibleObserverWidget = ({ onVisible }: IWidget) => {
+  const ref = useRef<HTMLDivElement>(null);
+  const isVisible = useOnScreen(ref);
+
+  useEffect(() => {
+    if (typeof onVisible !== "undefined") {
+      onVisible(isVisible);
+    }
+  }, [isVisible, onVisible]);
+
+  return (
+    <div ref={ref} style={{ height: 20 }}>
+      {" "}
+    </div>
+  );
+};
+
+export default VisibleObserverWidget;

+ 1 - 1
dashboard-v6/src/components/term/TermModal.tsx

@@ -52,7 +52,7 @@ const TermModalWidget = (props: IWidget) => {
             <span>术语</span>
             <span>术语</span>
             {studioName && (
             {studioName && (
               <Link
               <Link
-                to={`/workspace/editor/wiki/${restEditProps.id}`}
+                to={`/workspace/edit/wiki/${restEditProps.id}`}
                 target="_blank"
                 target="_blank"
                 style={{ fontSize: "12px", fontWeight: "normal" }}
                 style={{ fontSize: "12px", fontWeight: "normal" }}
               >
               >

+ 3 - 3
dashboard-v6/src/hooks/useMergedState.ts

@@ -17,7 +17,7 @@ import { useState, useCallback } from "react";
  * - mergedValue: 最终确定的状态值。
  * - mergedValue: 最终确定的状态值。
  * - setMergedValue: 更新状态的函数(内部会自动判断是更新本地状态还是仅触发回调)。
  * - setMergedValue: 更新状态的函数(内部会自动判断是更新本地状态还是仅触发回调)。
  * * [使用示例]
  * * [使用示例]
- * const [open, setOpen] = useMergedState(false, {
+ * const [open, setOpen] = useMergedState<boolean>(false, {
  * value: props.open,
  * value: props.open,
  * onChange: props.onOpenChange
  * onChange: props.onOpenChange
  * });
  * });
@@ -26,7 +26,7 @@ function useMergedState<T>(
   defaultStateValue: T | (() => T),
   defaultStateValue: T | (() => T),
   option?: {
   option?: {
     value?: T;
     value?: T;
-    onChange?: (value: T, prevValue: T) => void;
+    onChange?: (value: T) => void;
   }
   }
 ): [T, (value: T) => void] {
 ): [T, (value: T) => void] {
   const { value, onChange } = option || {};
   const { value, onChange } = option || {};
@@ -57,7 +57,7 @@ function useMergedState<T>(
 
 
       // 无论受控还是非受控,都触发回调通知父组件
       // 无论受控还是非受控,都触发回调通知父组件
       if (onChange) {
       if (onChange) {
-        onChange(newValue, mergedValue);
+        onChange(newValue);
       }
       }
     },
     },
     [value, mergedValue, onChange]
     [value, mergedValue, onChange]

+ 2 - 2
dashboard-v6/src/pages/workspace/term/edit.tsx

@@ -1,11 +1,11 @@
 import { useParams } from "react-router";
 import { useParams } from "react-router";
 
 
-import TermShow from "../../../components/term/TermShow";
+import TypeTerm from "../../../components/article/TypeTerm";
 
 
 const Widget = () => {
 const Widget = () => {
   const { id } = useParams();
   const { id } = useParams();
 
 
-  return <TermShow wordId={id} />;
+  return <TypeTerm id={id} />;
 };
 };
 
 
 export default Widget;
 export default Widget;