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

Merge pull request #2482 from visuddhinanda/development

Development
visuddhinanda 1 неделя назад
Родитель
Сommit
1fb4b7759b

+ 13 - 10
dashboard-test/src/tests/auth.setup.ts

@@ -1,21 +1,24 @@
-import { test as setup, expect } from '@playwright/test';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
+import { test as setup, expect } from "@playwright/test";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
-export const AUTH_FILE = path.join(__dirname, '../../playwright/.auth/user.json');
+export const AUTH_FILE = path.join(
+  __dirname,
+  "../../playwright/.auth/user.json",
+);
 
-const USER = process.env.E2E_USER ?? 'test161';
-const PASS = process.env.E2E_PASS ?? '12345';
+const USER = process.env.E2E_USER ?? "test161";
+const PASS = process.env.E2E_PASS ?? "11111";
 
-setup('登录并保存会话', async ({ page }) => {
-  await page.goto('anonymous/sign-in');
+setup("登录并保存会话", async ({ page }) => {
+  await page.goto("anonymous/sign-in");
 
   // ProForm 会渲染隐藏 input,必须用 :visible 过滤
-  const inputs = page.locator('form input:visible');
+  const inputs = page.locator("form input:visible");
   await inputs.nth(0).fill(USER);
   await inputs.nth(1).fill(PASS);
-  await page.getByRole('button', { name: /提 *交/ }).click();
+  await page.getByRole("button", { name: /提 *交/ }).click();
 
   // 登录成功会跳离 sign-in
   await expect(page).not.toHaveURL(/sign-in/, { timeout: 20_000 });

+ 2 - 0
dashboard-v6/src/Router.tsx

@@ -18,6 +18,7 @@ import tagRoutes from "./routes/tagRoutes";
 import driverRoutes from "./routes/driverRoutes";
 import dictRoutes from "./routes/dictRoutes";
 import courseRoutes from "./routes/courseRoutes";
+import nissayaRoutes from "./routes/nissayaRoutes";
 
 const RootLayout = lazy(() => import("./layouts/Root"));
 const AnonymousLayout = lazy(() => import("./layouts/anonymous"));
@@ -105,6 +106,7 @@ const router = createBrowserRouter(
             ...driverRoutes,
             ...dictRoutes,
             ...courseRoutes,
+            ...nissayaRoutes,
           ],
         },
 

+ 45 - 24
dashboard-v6/src/components/nissaya/NissayaCard.tsx

@@ -1,5 +1,13 @@
 import { useEffect, useState, type JSX } from "react";
-import { App, Button, Modal, Popover, Skeleton, Typography } from "antd";
+import {
+  App,
+  Button,
+  Modal,
+  Popover,
+  Skeleton,
+  Typography,
+  theme,
+} from "antd";
 import { EditOutlined, ReloadOutlined } from "@ant-design/icons";
 import { Link } from "react-router";
 import { useIntl } from "react-intl";
@@ -39,15 +47,41 @@ interface INissayaCardModal {
   trigger?: JSX.Element | string;
 }
 
-export const NissayaCardPop = ({ text, trigger }: INissayaCardModal) => (
-  <Popover
-    style={{ width: 700 }}
-    content={<NissayaCardWidget text={text} cache hideEditButton />}
-    placement="bottom"
-  >
-    <Typography.Link>{trigger}</Typography.Link>
-  </Popover>
-);
+export const NissayaCardPop = ({ text, trigger }: INissayaCardModal) => {
+  const intl = useIntl();
+  const { token } = theme.useToken();
+
+  return (
+    <Popover
+      style={{ width: 700 }}
+      title={
+        <div style={{ textAlign: "right" }}>
+          <Link to={`/workspace/nissaya/ending/${text}`} target="_blank">
+            {intl.formatMessage(
+              { id: "buttons.open.in.new.tab" },
+              { item: "" }
+            )}
+          </Link>
+        </div>
+      }
+      content={<NissayaCardWidget text={text} cache hideEditButton />}
+      placement="bottom"
+      // 垂直方向始终贴在锚点下方:不翻转、不平移回视口,宁可超出可视范围也不遮挡锚点;
+      // 横向仍自动适应窗口。autoAdjustOverflow 做不到——antd 在 shiftY 关闭时会
+      // 强制打开 adjustY——所以自己给出 bottom 的对齐规则,offset 同 antd 默认算法
+      builtinPlacements={{
+        bottom: {
+          points: ["tc", "bc"],
+          offset: [0, token.sizePopupArrow / 2 + token.marginXXS],
+          overflow: { adjustX: true, shiftX: true },
+          htmlRegion: "visibleFirst",
+        },
+      }}
+    >
+      <Typography.Link>{trigger}</Typography.Link>
+    </Popover>
+  );
+};
 
 export const NissayaCardModal = ({ text, trigger }: INissayaCardModal) => {
   const [open, setOpen] = useState(false);
@@ -84,7 +118,6 @@ const NissayaCardWidget = ({
   cache = false,
   hideEditButton = false,
 }: IWidgetProps) => {
-  const intl = useIntl();
   // antd v6: use App.useApp() instead of static message/notification
   const { message } = App.useApp();
 
@@ -171,19 +204,7 @@ const NissayaCardWidget = ({
           )}
         </Title>
 
-        <div>
-          <Link to={`/nissaya/ending/${term?.word}`} target="_blank">
-            {intl.formatMessage(
-              { id: "buttons.open.in.new.tab" },
-              { item: "" }
-            )}
-          </Link>
-          <Button
-            type="link"
-            icon={<ReloadOutlined />}
-            onClick={handleReload}
-          />
-        </div>
+        <Button type="link" icon={<ReloadOutlined />} onClick={handleReload} />
       </div>
 
       <Paragraph>{term?.meaning}</Paragraph>

+ 23 - 15
dashboard-v6/src/components/wbw/WbwPali.tsx

@@ -141,28 +141,30 @@ const WbwPaliWidget = ({
   }, [tempSettings]);
 
   /**
-   * 修复2:popPlacement 用 useState 存储,但只在事件处理器中更新。
-   * 在事件处理器(用户点击)里读取 DOM ref 并 setState 完全合法,
-   * 不会产生"effect 内 setState"的级联渲染警告。
+   * 修复2:只在打开弹窗时(事件处理器里)读 DOM,记下单词是否贴近右边缘;
+   * 上/下方向由 popOnTop 派生。这样弹窗打开期间切换「顶端/底端弹窗」能立即生效。
    */
-  const [popPlacement, setPopPlacement] = useState<TooltipPlacement>("bottom");
+  const [nearRightEdge, setNearRightEdge] = useState(false);
+
+  const popPlacement = useMemo<TooltipPlacement>(
+    () =>
+      popOnTop
+        ? nearRightEdge
+          ? "topRight"
+          : "top"
+        : nearRightEdge
+          ? "bottomRight"
+          : "bottom",
+    [popOnTop, nearRightEdge]
+  );
 
   const computeAndSetPlacement = useCallback(() => {
     const rightPanel = document.getElementById("article_right_panel");
     const rightPanelWidth = rightPanel?.offsetWidth ?? 0;
     const containerWidth = window.innerWidth - rightPanelWidth;
     const divRight = divShell.current?.getBoundingClientRect().right ?? 0;
-    const toDivRight = containerWidth - divRight;
-    setPopPlacement(
-      popOnTop
-        ? toDivRight > 200
-          ? "top"
-          : "topRight"
-        : toDivRight > 200
-          ? "bottom"
-          : "bottomRight"
-    );
-  }, [popOnTop]);
+    setNearRightEdge(containerWidth - divRight <= 200);
+  }, []);
 
   // ── Popover open/close ────────────────────────────────────────────────────
   const popOpenChange = useCallback(
@@ -392,6 +394,12 @@ const WbwPaliWidget = ({
           <Popover
             content={wbwDialog}
             placement={popPlacement}
+            // 上下方向由「顶端/底端弹窗」按钮决定,不让 antd 因空间不足自动翻转,
+            // 否则贴近视口顶部的词切到顶端后会被翻回底部,按钮看起来像失效。
+            // 保留 shiftY:空间不够时整体平移回视口,切换按钮始终点得到。
+            // antd 类型写的是 0 | 1,但 rc-trigger 按 `val >= 0` 判断,0 也算开启,
+            // 只有 false 才真正关掉,故断言。
+            autoAdjustOverflow={{ adjustX: 1, adjustY: false as unknown as 0 }}
             trigger="click"
             open={finalPopOpen}
           >

+ 69 - 11
dashboard-v6/src/layouts/anonymous/index.tsx

@@ -1,19 +1,77 @@
-import { Outlet } from "react-router";
+import { ConfigProvider, theme } from "antd";
+import { Link, Outlet } from "react-router";
 
-import Footer from "../Footer";
+import UiLangSelect from "../../components/general/UiLangSelect";
+import BeiAn from "../../components/general/BeiAn";
+import img_logo from "../../assets/general/images/wikipali_logo.svg";
+
+/**
+ * 未登录页(登录 / 注册 / 找回密码)的外壳,布局参照 dashboard-v4 的 layouts/anonymous。
+ *
+ * logo 是白字 + 黄色图形,只能放在深色底上,所以底色固定为 v4 的 #3e3e3e,
+ * 不跟随主题切换;顶栏与页脚套暗色算法,文字才看得清。
+ * 中间的表单卡片仍跟随用户的主题。
+ */
+const BACKGROUND = "#3e3e3e";
 
 const Widget = () => {
-  // TODO
   return (
-    <div>
-      <div>anonymous header</div>
-      <div>
-        <Outlet />
-      </div>
-      <div>
-        anonymous layout footer
-        <Footer />
+    <div
+      style={{
+        minHeight: "100vh",
+        display: "flex",
+        flexDirection: "column",
+        backgroundColor: BACKGROUND,
+      }}
+    >
+      <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
+        <div
+          style={{
+            display: "flex",
+            justifyContent: "flex-end",
+            padding: "4px 16px",
+          }}
+        >
+          <UiLangSelect />
+        </div>
+      </ConfigProvider>
+
+      <div
+        style={{
+          flex: 1,
+          display: "flex",
+          flexWrap: "wrap",
+          justifyContent: "center",
+          alignItems: "flex-start",
+          gap: 32,
+          padding: "3em 16px 120px",
+        }}
+      >
+        <div style={{ flex: "0 1 400px", padding: "1em 0" }}>
+          <Link to="/">
+            <img
+              alt="wikipali"
+              src={img_logo}
+              style={{ width: "20em", maxWidth: "100%" }}
+            />
+          </Link>
+        </div>
+        <div style={{ flex: "0 1 auto", maxWidth: "100%" }}>
+          <Outlet />
+        </div>
       </div>
+
+      <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
+        <div
+          style={{
+            padding: "16px",
+            textAlign: "center",
+            color: "rgba(255, 255, 255, 0.65)",
+          }}
+        >
+          <BeiAn />
+        </div>
+      </ConfigProvider>
     </div>
   );
 };

+ 1 - 0
dashboard-v6/src/pages/users/forgot-password.tsx

@@ -14,6 +14,7 @@ const Widget = () => {
         title={intl.formatMessage({
           id: "buttons.forgot.password",
         })}
+        style={{ width: 400, maxWidth: "100%" }}
       >
         <ForgotPassword />
         <Divider />

+ 1 - 0
dashboard-v6/src/pages/users/reset-password.tsx

@@ -15,6 +15,7 @@ const Widget = () => {
         title={intl.formatMessage({
           id: "buttons.reset.password",
         })}
+        style={{ width: 400, maxWidth: "100%" }}
       >
         <ResetPassword token={token} />
         <Divider />

+ 7 - 7
dashboard-v6/src/pages/users/sign-in.tsx

@@ -1,24 +1,24 @@
 import SignInForm from "../../components/users/SignIn";
 import SharedLinks from "../../components/users/NonSignInSharedLinks";
-import { Card, Space } from "antd";
+import { Card, Divider } from "antd";
 import { useIntl } from "react-intl";
 
 const Widget = () => {
   const intl = useIntl();
   return (
-    <div>
+    <>
       <title>{intl.formatMessage({ id: "nut.users.sign-in.title" })}</title>
       <Card
         title={intl.formatMessage({
           id: "nut.users.sign-in.title",
         })}
+        style={{ width: 400, maxWidth: "100%" }}
       >
-        <Space orientation="vertical">
-          <SignInForm />
-          <SharedLinks />
-        </Space>
+        <SignInForm />
+        <Divider />
+        <SharedLinks />
       </Card>
-    </div>
+    </>
   );
 };
 

+ 3 - 1
dashboard-v6/src/pages/users/sign-up-invite.tsx

@@ -1,4 +1,4 @@
-import { Card } from "antd";
+import { Card, Divider } from "antd";
 import { useIntl } from "react-intl";
 import { useParams } from "react-router";
 import SharedLinks from "../../components/users/NonSignInSharedLinks";
@@ -15,8 +15,10 @@ const Widget = () => {
         title={intl.formatMessage({
           id: "buttons.sign-up",
         })}
+        style={{ width: 400, maxWidth: "100%" }}
       >
         <InviteSignUp token={token} />
+        <Divider />
         <SharedLinks />
       </Card>
     </>

+ 3 - 1
dashboard-v6/src/pages/users/sign-up.tsx

@@ -1,4 +1,4 @@
-import { Card } from "antd";
+import { Card, Divider } from "antd";
 import { useIntl } from "react-intl";
 import SharedLinks from "../../components/users/NonSignInSharedLinks";
 import SelfSignUp from "../../components/users/SelfSignUp";
@@ -13,8 +13,10 @@ const Widget = () => {
         title={intl.formatMessage({
           id: "buttons.sign-up",
         })}
+        style={{ width: 760, maxWidth: "100%" }}
       >
         <SelfSignUp />
+        <Divider />
         <SharedLinks />
       </Card>
     </>

+ 23 - 0
dashboard-v6/src/pages/workspace/nissaya/ending.tsx

@@ -0,0 +1,23 @@
+import { Col, Row } from "antd";
+import { useParams } from "react-router";
+
+import NissayaCardWidget from "../../../components/nissaya/NissayaCard";
+
+const Widget = () => {
+  const { ending } = useParams();
+
+  return (
+    <>
+      <title>{ending}</title>
+      <Row>
+        <Col flex="auto" />
+        <Col flex="960px">
+          <NissayaCardWidget text={ending} />
+        </Col>
+        <Col flex="auto" />
+      </Row>
+    </>
+  );
+};
+
+export default Widget;

+ 27 - 0
dashboard-v6/src/routes/nissayaRoutes.ts

@@ -0,0 +1,27 @@
+// src/routes/nissayaRoutes.ts
+import { lazy } from "react";
+import type { RouteObject } from "react-router";
+
+const WorkspaceNissayaEnding = lazy(
+  () => import("../pages/workspace/nissaya/ending")
+);
+
+const nissayaRoutes: RouteObject[] = [
+  {
+    path: "nissaya",
+    children: [
+      {
+        // NissayaCard 右上角「在新标签页中打开」
+        path: "ending/:ending",
+        Component: WorkspaceNissayaEnding,
+        handle: {
+          id: "workspace.nissaya.ending",
+          crumb: (match: { params: { ending?: string } }) =>
+            match.params.ending,
+        },
+      },
+    ],
+  },
+];
+
+export default nissayaRoutes;