Просмотр исходного кода

💄 (dashboard-v6): 三藏目录右侧抽屉改为资源在上、目录在下

- 去掉「目录 / 资源」tabs,改为上下两段;标题下加主按钮「打开原文」(新标签页打开不带 channel 的阅读页)
- 目录用紧凑模式:小号列表、标题与统计两行、进度图缩小、不显示路径;整行可点并有 hover 高亮
- 统计行:字符数用 K/M 缩写、段落数千分位,标签灰色与数字区分;v2 view=chapter 不返回段落数时不显示该项(修复 toLocaleString 崩溃)
- 资源列表每页 3 条

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 8 часов назад
Родитель
Сommit
cb6e671697

+ 27 - 24
dashboard-v6/src/components/tipitaka/BookViewer.tsx

@@ -4,7 +4,9 @@ import PaliChapterChannelList from "./PaliChapterChannelList";
 import PaliChapterListByPara from "./PaliChapterListByPara";
 import PaliChapterHead from "./PaliChapterHead";
 import type { IChapterClickEvent } from "./PaliChapterList";
-import { Tabs } from "antd";
+import { Button, Typography } from "antd";
+import { useIntl } from "react-intl";
+import { Link } from "react-router";
 
 export interface IChapter {
   book: number;
@@ -17,6 +19,7 @@ interface IWidget {
   onChange?: (currChapter: IChapter) => void;
 }
 const BookViewerWidget = ({ chapter, onChange }: IWidget) => {
+  const intl = useIntl();
   const [currChapter, setCurrChapter] = useState(chapter);
   useEffect(() => {
     if (typeof onChange !== "undefined") {
@@ -35,29 +38,29 @@ const BookViewerWidget = ({ chapter, onChange }: IWidget) => {
         }}
         para={currChapter}
       />
-      <Tabs
-        size="small"
-        items={[
-          {
-            label: `目录`,
-            key: "toc",
-            children: (
-              <PaliChapterListByPara
-                chapter={currChapter}
-                onChapterClick={(e: IChapterClickEvent) => {
-                  setCurrChapter({ book: e.para.Book, para: e.para.Paragraph });
-                  console.log("PaliChapterListByPara", "onchange", e);
-                }}
-              />
-            ),
-          },
-          {
-            label: `资源`,
-            key: "res",
-            children: <PaliChapterChannelList para={currChapter} />,
-          },
-        ]}
-      />
+      {/* 不带 channel 打开阅读页,只看巴利原文;与下面资源链接一样新标签页打开 */}
+      <Link
+        to={`/workspace/tipitaka/chapter/${currChapter.book}-${currChapter.para}`}
+        target="_blank"
+      >
+        <Button type="primary">
+          {intl.formatMessage({ id: "buttons.open.original" })}
+        </Button>
+      </Link>
+      <div style={{ marginTop: 8 }}>
+        <Typography.Title level={5}>资源</Typography.Title>
+        <PaliChapterChannelList para={currChapter} />
+      </div>
+      <div style={{ marginTop: 16 }}>
+        <Typography.Title level={5}>目录</Typography.Title>
+        <PaliChapterListByPara
+          compact
+          chapter={currChapter}
+          onChapterClick={(e: IChapterClickEvent) => {
+            setCurrChapter({ book: e.para.Book, para: e.para.Paragraph });
+          }}
+        />
+      </div>
     </>
   );
 };

+ 1 - 1
dashboard-v6/src/components/tipitaka/ChapterInChannel.tsx

@@ -57,7 +57,7 @@ const ChapterInChannelWidget = ({
             : {
                 showQuickJumper: false,
                 showSizeChanger: false,
-                pageSize: 5,
+                pageSize: 3,
                 total: channels.length,
                 position: "bottom",
                 showTotal: (total) => {

+ 90 - 8
dashboard-v6/src/components/tipitaka/PaliChapterCard.tsx

@@ -12,17 +12,103 @@ export interface IPaliChapterData {
   Path: string;
   Book: number;
   Paragraph: number;
-  chapterStrLen: number;
-  paragraphCount: number;
+  chapterStrLen?: number;
+  /** v2 `view=chapter` 不返回 chapter_len,此时为 undefined */
+  paragraphCount?: number;
   progressLine?: number[];
 }
 
+/** 1234 → 1.2K,1234567 → 1.2M;小于 1000 原样。固定用 en,避免中文环境出「万」 */
+const countFormatter = new Intl.NumberFormat("en", {
+  notation: "compact",
+  maximumFractionDigits: 1,
+});
+const formatCount = (n: number): string => countFormatter.format(n);
+
+/** 统计行:标签灰色、数字正文色加粗,两组之间留空,不用竖线 */
+const ChapterStats = ({
+  data,
+  fontSize,
+}: {
+  data: IPaliChapterData;
+  fontSize?: number;
+}) => {
+  // 接口不一定两项都给(v2 view=chapter 没有段落数),缺的那项不显示
+  const items = [
+    data.chapterStrLen != null && {
+      label: "字符",
+      value: formatCount(data.chapterStrLen),
+    },
+    data.paragraphCount != null && {
+      label: "段落",
+      value: data.paragraphCount.toLocaleString("en"),
+    },
+  ].filter((it): it is { label: string; value: string } => Boolean(it));
+  if (items.length === 0) return null;
+  return (
+    <Space size={12} style={{ fontSize }}>
+      {items.map((it) => (
+        <span key={it.label}>
+          <Text type="secondary" style={{ fontSize }}>
+            {it.label}
+          </Text>{" "}
+          <Text strong style={{ fontSize }}>
+            {it.value}
+          </Text>
+        </span>
+      ))}
+    </Space>
+  );
+};
+
 interface IWidget {
   data: IPaliChapterData;
+  /** 紧凑模式:标题一行、统计一行,不显示路径,进度图缩小 */
+  compact?: boolean;
   onTitleClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
 }
 
-const PaliChapterCardWidget = ({ data, onTitleClick }: IWidget) => {
+const PaliChapterCardWidget = ({
+  data,
+  compact = false,
+  onTitleClick,
+}: IWidget) => {
+  if (compact) {
+    return (
+      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
+        <div style={{ flex: "auto", minWidth: 0 }}>
+          <Link
+            strong
+            onClick={(e) => {
+              if (typeof onTitleClick !== "undefined") {
+                onTitleClick(e);
+              }
+            }}
+          >
+            {data.Title}
+          </Link>
+          {data.PaliTitle !== data.Title ? (
+            <Text type="secondary"> {data.PaliTitle}</Text>
+          ) : null}
+          <div>
+            <ChapterStats data={data} fontSize={12} />
+          </div>
+        </div>
+        {data.progressLine ? (
+          <div style={{ flex: "none" }}>
+            <Tiny.Line
+              height={32}
+              width={120}
+              autoFit={false}
+              data={data.progressLine}
+              smooth={true}
+            />
+          </div>
+        ) : null}
+      </div>
+    );
+  }
+
   const path = JSON.parse(data.Path);
 
   return (
@@ -71,11 +157,7 @@ const PaliChapterCardWidget = ({ data, onTitleClick }: IWidget) => {
           </Row>
           <Row>
             <Col>
-              <Text type="secondary">
-                <Space>
-                  字符数{data.chapterStrLen} | 段落数{data.paragraphCount}
-                </Space>
-              </Text>
+              <ChapterStats data={data} />
             </Col>
           </Row>
           <Row>

+ 11 - 0
dashboard-v6/src/components/tipitaka/PaliChapterList.module.css

@@ -0,0 +1,11 @@
+/* 紧凑模式(右侧抽屉目录)的列表项:整行可点,hover 高亮 */
+.compactItem {
+  padding: 4px 8px !important;
+  border-radius: var(--ant-border-radius, 6px);
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.compactItem:hover {
+  background-color: var(--ant-color-fill-tertiary, rgba(0, 0, 0, 0.04));
+}

+ 30 - 14
dashboard-v6/src/components/tipitaka/PaliChapterList.tsx

@@ -1,6 +1,7 @@
 import { List } from "antd";
 
 import PaliChapterCard, { type IPaliChapterData } from "./PaliChapterCard";
+import styles from "./PaliChapterList.module.css";
 
 export interface IChapterClickEvent {
   para: IPaliChapterData;
@@ -10,33 +11,48 @@ export interface IChapterClickEvent {
 interface IWidgetPaliChapterList {
   data: IPaliChapterData[];
   maxLevel?: number;
+  /** 紧凑模式:小号列表 + 单行卡片(右侧抽屉里用) */
+  compact?: boolean;
   onChapterClick?: (v: IChapterClickEvent) => void;
 }
 const PaliChapterListWidget = ({
   data,
   maxLevel = 8,
+  compact = false,
   onChapterClick,
 }: IWidgetPaliChapterList) => {
+  const emitClick = (
+    item: IPaliChapterData,
+    e: React.MouseEvent<HTMLElement, MouseEvent>
+  ) => {
+    if (typeof onChapterClick !== "undefined") {
+      onChapterClick({ para: item, event: e });
+    }
+  };
+
   return (
     <List
       itemLayout="vertical"
-      size="large"
+      size={compact ? "small" : "large"}
       dataSource={data}
       renderItem={(item) =>
         item.level <= maxLevel ? (
-          <List.Item>
-            <PaliChapterCard
-              onTitleClick={(e) => {
-                if (typeof onChapterClick !== "undefined") {
-                  onChapterClick({
-                    para: item,
-                    event: e,
-                  });
-                }
-              }}
-              data={item}
-            />
-          </List.Item>
+          compact ? (
+            // 紧凑模式整行可点,点击交给 List.Item,卡片里的标题不再单独绑定
+            <List.Item
+              className={styles.compactItem}
+              onClick={(e) => emitClick(item, e)}
+            >
+              <PaliChapterCard compact data={item} />
+            </List.Item>
+          ) : (
+            <List.Item>
+              <PaliChapterCard
+                onTitleClick={(e) => emitClick(item, e)}
+                data={item}
+              />
+            </List.Item>
+          )
         ) : (
           <></>
         )

+ 7 - 1
dashboard-v6/src/components/tipitaka/PaliChapterListByPara.tsx

@@ -8,9 +8,14 @@ import PaliChapterList, { type IChapterClickEvent } from "./PaliChapterList";
 
 interface IWidget {
   chapter: IChapter;
+  compact?: boolean;
   onChapterClick?: (e: IChapterClickEvent) => void;
 }
-const PaliChapterListByParaWidget = ({ chapter, onChapterClick }: IWidget) => {
+const PaliChapterListByParaWidget = ({
+  chapter,
+  compact = false,
+  onChapterClick,
+}: IWidget) => {
   const [tableData, setTableData] = useState<IPaliChapterData[]>([]);
 
   useEffect(() => {
@@ -38,6 +43,7 @@ const PaliChapterListByParaWidget = ({ chapter, onChapterClick }: IWidget) => {
   return (
     <>
       <PaliChapterList
+        compact={compact}
         onChapterClick={(e: IChapterClickEvent) => {
           if (onChapterClick) {
             onChapterClick(e);

+ 1 - 0
dashboard-v6/src/locales/en-US/buttons.ts

@@ -62,6 +62,7 @@ const items = {
   "buttons.copy.id": "copy id",
   "buttons.reply": "reply",
   "buttons.open": "open",
+  "buttons.open.original": "Open Pali text",
   "buttons.compact": "compact",
   "buttons.magic-dict": "magic-dict",
   "buttons.theme.ant": "theme ant",

+ 1 - 0
dashboard-v6/src/locales/zh-Hans/buttons.ts

@@ -62,6 +62,7 @@ const items = {
   "buttons.copy.id": "复制编号",
   "buttons.reply": "回复",
   "buttons.open": "打开",
+  "buttons.open.original": "打开原文",
   "buttons.compact": "紧凑",
   "buttons.magic-dict": "神奇字典",
   "buttons.theme.ant": "默认",

+ 1 - 0
dashboard-v6/src/locales/zh-Hant/buttons.ts

@@ -62,6 +62,7 @@ const items = {
   "buttons.copy.id": "複製編號",
   "buttons.reply": "回覆",
   "buttons.open": "開啟",
+  "buttons.open.original": "開啟原文",
   "buttons.compact": "緊湊",
   "buttons.magic-dict": "神奇字典",
   "buttons.theme.ant": "預設",