Browse Source

fix(dashboard-v6): 弹层内的链接改为新标签页打开

弹窗/抽屉/Popover 里的链接原地跳转会把弹层关掉,逐个改为新标签页:

- AiModelList: 新增 openInNewTab 参数,SettingModal 的 model tab 传入
- Avatar: 头像 Popover 里的 blog 链接加 target="_blank"
- ChapterInChannel: ChannelList 增加 target 参数,"版本选择" Modal 传 "_blank"
- MainMenu: RecentModal 的 onSelect 改为 window.open,
  顺带修掉 Ctrl/Cmd 分支 window.open("") 开空白页的问题
- TocPath: 实现此前声明却未使用的 link prop,link="blank" 时开新标签页;
  SentTab 的面包屑 Popover 改用 link="blank"
- ELinkType 去掉从未实现的 "none",并清理 PaliChapterHead 中无效的 link 传参

另外修复 fullUrl(): BASE_URL 带尾斜杠而调用方前导斜杠写法不统一,
会拼出 /pcd-v2026//workspace/... 这样的重复斜杠,现统一规范化。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhhGrvAKanvp6pKkDeWcsv
visuddhinanda 1 week ago
parent
commit
a733e441d1

+ 11 - 3
dashboard-v6/src/components/ai-model/AiModelList.tsx

@@ -19,8 +19,10 @@ import { EResType } from "../share/utils";
 
 
 interface IWidget {
 interface IWidget {
   studioName?: string;
   studioName?: string;
+  /** 在弹层(Modal/Drawer/Popover)中使用时置为 true,链接改为新标签页打开 */
+  openInNewTab?: boolean;
 }
 }
-const AiModelList = ({ studioName }: IWidget) => {
+const AiModelList = ({ studioName, openInNewTab }: IWidget) => {
   const intl = useIntl(); //i18n
   const intl = useIntl(); //i18n
 
 
   const [openCreate, setOpenCreate] = useState(false);
   const [openCreate, setOpenCreate] = useState(false);
@@ -41,7 +43,10 @@ const AiModelList = ({ studioName }: IWidget) => {
               return (
               return (
                 <Space>
                 <Space>
                   <PublicityIcon value={entity.privacy} />
                   <PublicityIcon value={entity.privacy} />
-                  <Link to={`/workspace/settings/ai-model/${entity.uid}/edit`}>
+                  <Link
+                    to={`/workspace/settings/ai-model/${entity.uid}/edit`}
+                    target={openInNewTab ? "_blank" : undefined}
+                  >
                     {entity.name}
                     {entity.name}
                   </Link>
                   </Link>
                 </Space>
                 </Space>
@@ -70,7 +75,10 @@ const AiModelList = ({ studioName }: IWidget) => {
             render(_dom, entity) {
             render(_dom, entity) {
               return (
               return (
                 <Space>
                 <Space>
-                  <Link to={`/workspace/settings/ai-model/${entity.uid}/log`}>
+                  <Link
+                    to={`/workspace/settings/ai-model/${entity.uid}/log`}
+                    target={openInNewTab ? "_blank" : undefined}
+                  >
                     logs
                     logs
                   </Link>
                   </Link>
                   <ShareModal
                   <ShareModal

+ 1 - 1
dashboard-v6/src/components/auth/Avatar.tsx

@@ -45,7 +45,7 @@ const UserCard = ({ user }: IUserCard) => {
             id: "columns.library.blog.label",
             id: "columns.library.blog.label",
           })}
           })}
         >
         >
-          <Link to={`/blog/${user?.realName}/overview`}>
+          <Link to={`/blog/${user?.realName}/overview`} target="_blank">
             <HomeOutlined key="home" />
             <HomeOutlined key="home" />
           </Link>
           </Link>
         </Tooltip>,
         </Tooltip>,

+ 3 - 6
dashboard-v6/src/components/navigation/MainMenu.tsx

@@ -355,12 +355,9 @@ const Widget = ({ onSearch }: Props) => {
       <RecentModal
       <RecentModal
         open={recentOpen}
         open={recentOpen}
         onOpenChange={() => setRecentOpen(false)}
         onOpenChange={() => setRecentOpen(false)}
-        onSelect={(e, row) => {
-          if (e.ctrlKey || e.metaKey) {
-            window.open("");
-          } else {
-            navigate(recentPath(row.type, row.articleId));
-          }
+        onSelect={(_e, row) => {
+          // 弹窗中的链接一律新标签页打开,避免弹窗被原地跳转关掉
+          window.open(fullUrl(recentPath(row.type, row.articleId)), "_blank");
           setRecentOpen(false);
           setRecentOpen(false);
         }}
         }}
       />
       />

+ 1 - 1
dashboard-v6/src/components/sentence/SentTab.tsx

@@ -128,7 +128,7 @@ const SentTabWidget = ({
       tabBarExtraContent={
       tabBarExtraContent={
         <Space>
         <Space>
           <TocPath
           <TocPath
-            link="none"
+            link="blank"
             data={mPath}
             data={mPath}
             channels={channelsId}
             channels={channelsId}
             trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
             trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}

+ 3 - 1
dashboard-v6/src/components/setting/SettingModal.tsx

@@ -65,7 +65,9 @@ const SettingModal = ({ trigger, open, onClose }: IWidget) => {
             {
             {
               label: "model",
               label: "model",
               key: "model",
               key: "model",
-              children: <AiModelList studioName={currUser?.realName} />,
+              children: (
+                <AiModelList studioName={currUser?.realName} openInNewTab />
+              ),
             },
             },
           ]}
           ]}
         />
         />

+ 8 - 3
dashboard-v6/src/components/tipitaka/ChapterInChannel.tsx

@@ -36,11 +36,16 @@ const ChapterInChannelWidget = ({
   book,
   book,
   para,
   para,
   channelId,
   channelId,
+  openTarget,
 }: IWidgetChapterInChannel) => {
 }: IWidgetChapterInChannel) => {
   const intl = useIntl(); //i18n
   const intl = useIntl(); //i18n
   const [searchParams] = useSearchParams();
   const [searchParams] = useSearchParams();
   const [open, setOpen] = useState(false);
   const [open, setOpen] = useState(false);
-  const ChannelList = (channels: IChapterChannelData[]): JSX.Element => {
+  const ChannelList = (
+    channels: IChapterChannelData[],
+    /** 在弹层中渲染时传 "_blank",避免原地跳转导致弹层关闭 */
+    target: React.HTMLAttributeAnchorTarget | undefined = openTarget
+  ): JSX.Element => {
     return channels.length ? (
     return channels.length ? (
       <List
       <List
         style={{ maxWidth: 500 }}
         style={{ maxWidth: 500 }}
@@ -70,7 +75,7 @@ const ChapterInChannelWidget = ({
             <List.Item key={id}>
             <List.Item key={id}>
               <Row>
               <Row>
                 <Col span={12}>
                 <Col span={12}>
-                  <Link to={url}>
+                  <Link to={url} target={target}>
                     <ChannelListItem
                     <ChannelListItem
                       channel={item.channel}
                       channel={item.channel}
                       studio={item.studio}
                       studio={item.studio}
@@ -139,7 +144,7 @@ const ChapterInChannelWidget = ({
           onCancel={handleCancel}
           onCancel={handleCancel}
           onOk={handleCancel}
           onOk={handleCancel}
         >
         >
-          <div>{ChannelList(data)}</div>
+          <div>{ChannelList(data, "_blank")}</div>
         </Modal>
         </Modal>
       </div>
       </div>
     );
     );

+ 0 - 1
dashboard-v6/src/components/tipitaka/PaliChapterHead.tsx

@@ -71,7 +71,6 @@ const PaliChapterHeadWidget = ({ para, onChange }: IWidget) => {
             }
             }
           }
           }
         }}
         }}
-        link={"none"}
       />
       />
       <ChapterHead data={chapterData} />
       <ChapterHead data={chapterData} />
     </>
     </>

+ 5 - 2
dashboard-v6/src/components/tipitaka/TocPath.tsx

@@ -6,7 +6,7 @@ import { articlePath, fullUrl } from "../../utils";
 import type { ITocPathNode } from "../../api/pali-text";
 import type { ITocPathNode } from "../../api/pali-text";
 import PaliText from "../general/PaliText";
 import PaliText from "../general/PaliText";
 
 
-export declare type ELinkType = "none" | "blank" | "self";
+export declare type ELinkType = "blank" | "self";
 
 
 interface IWidgetTocPath {
 interface IWidgetTocPath {
   data?: ITocPathNode[];
   data?: ITocPathNode[];
@@ -24,6 +24,7 @@ interface IWidgetTocPath {
 const TocPathWidget = ({
 const TocPathWidget = ({
   data = [],
   data = [],
   trigger,
   trigger,
+  link,
   channels,
   channels,
   style,
   style,
   onChange,
   onChange,
@@ -51,7 +52,9 @@ const TocPathWidget = ({
           const urlMode = mode ? mode : "read";
           const urlMode = mode ? mode : "read";
           let url = `${articlePath(type, `${item.book}-${item.paragraph}`)}?mode=${urlMode}${param}`;
           let url = `${articlePath(type, `${item.book}-${item.paragraph}`)}?mode=${urlMode}${param}`;
           url += channel ? `&channel=${channel}` : "";
           url += channel ? `&channel=${channel}` : "";
-          if (e.ctrlKey || e.metaKey) {
+          // link="blank":在弹层(Modal/Drawer/Popover)中使用时,
+          // 一律新标签页打开,避免原地跳转把弹层关掉
+          if (link === "blank" || e.ctrlKey || e.metaKey) {
             window.open(fullUrl(url), "_blank");
             window.open(fullUrl(url), "_blank");
           } else {
           } else {
             navigate(url);
             navigate(url);

+ 8 - 6
dashboard-v6/src/utils.ts

@@ -27,12 +27,14 @@ export function dashboardBasePath(): string {
 }
 }
 
 
 export function fullUrl(url: string): string {
 export function fullUrl(url: string): string {
-  if (import.meta.env.BASE_URL.includes("http")) {
-    //for CDN
-    return import.meta.env.BASE_URL + url;
-  } else {
-    return window.location.origin + import.meta.env.BASE_URL + url;
-  }
+  // BASE_URL 通常以 "/" 结尾,而调用方传入的 url 有的带前导 "/" 有的不带,
+  // 这里统一去掉重复的斜杠,避免拼出 ".../pcd-v2026//workspace/..." 这种地址
+  const base = import.meta.env.BASE_URL.replace(/\/+$/, "");
+  const path = url.replace(/^\/+/, "");
+  const prefix = base.includes("http")
+    ? base //for CDN
+    : window.location.origin + base;
+  return path ? `${prefix}/${path}` : `${prefix}/`;
 }
 }
 
 
 export function PaliToEn(pali: string): string {
 export function PaliToEn(pali: string): string {