Parcourir la source

feat: 静态页面

chenlei il y a 1 an
Parent
commit
3a7e63b2b4

+ 2 - 0
package.json

@@ -23,6 +23,8 @@
     "babel-plugin-named-asset-import": "^0.3.8",
     "babel-preset-react-app": "^10.0.1",
     "bfj": "^7.0.2",
+    "braft-editor": "^2.3.9",
+    "braft-utils": "^3.0.12",
     "browserslist": "^4.18.1",
     "camelcase": "^6.2.1",
     "case-sensitive-paths-webpack-plugin": "^2.4.0",

+ 172 - 0
pnpm-lock.yaml

@@ -65,6 +65,12 @@ dependencies:
   bfj:
     specifier: ^7.0.2
     version: 7.0.2
+  braft-editor:
+    specifier: ^2.3.9
+    version: 2.3.9(react-dom@18.2.0)(react@18.2.0)
+  braft-utils:
+    specifier: ^3.0.12
+    version: 3.0.12(braft-convert@2.3.0)(draft-js@0.10.5)(draftjs-utils@0.9.4)(immutable@3.7.6)
   browserslist:
     specifier: ^4.18.1
     version: 4.18.1
@@ -3786,6 +3792,61 @@ packages:
     dependencies:
       fill-range: 7.0.1
 
+  /braft-convert@2.3.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-5km+dLHk8iYDv2iEYDrDQ2ld/ZoUx66QLql0qdm5PqZEcNXc8dBHGLORfzeu3iMw1jLeAiHxtdY5+ypuIhczVg==}
+    peerDependencies:
+      react: ^16.0.0
+    dependencies:
+      draft-convert: 2.1.13(draft-js@0.10.5)(react-dom@18.2.0)(react@18.2.0)
+      draft-js: 0.10.5(react-dom@18.2.0)(react@18.2.0)
+      react: 18.2.0
+    transitivePeerDependencies:
+      - react-dom
+    dev: false
+
+  /braft-editor@2.3.9(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-mqdPk/zI2dhFK8tW/A4Qj/AkkARLh5L/niNw+iif5wFqb6zh15rMlrShgz1nWO/QXyAKr8XtDgxiBbR0zWwtRg==}
+    peerDependencies:
+      react: ^15.0.2|| ^16.0.0-rc || ^16.0.0
+      react-dom: ^15.0.2|| ^16.0.0-rc || ^16.0.0
+    dependencies:
+      '@babel/runtime': 7.23.2
+      braft-convert: 2.3.0(react-dom@18.2.0)(react@18.2.0)
+      braft-finder: 0.0.19(react-dom@18.2.0)(react@18.2.0)
+      braft-utils: 3.0.12(braft-convert@2.3.0)(draft-js@0.10.5)(draftjs-utils@0.9.4)(immutable@3.7.6)
+      draft-convert: 2.1.13(draft-js@0.10.5)(react-dom@18.2.0)(react@18.2.0)
+      draft-js: 0.10.5(react-dom@18.2.0)(react@18.2.0)
+      draft-js-multidecorators: 1.0.0
+      draftjs-utils: 0.9.4(draft-js@0.10.5)(immutable@3.7.6)
+      immutable: 3.7.6
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /braft-finder@0.0.19(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-0kzI6/KbomJJhYX1hpjn4edCKhblyUyWdUrsgBmOrwy0vrj+pPkm69+Uf8Uj6KGAULM6LF0ooC++p7fqUGgFHw==}
+    peerDependencies:
+      react: ^16.4.1
+      react-dom: ^16.4.1
+    dependencies:
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /braft-utils@3.0.12(braft-convert@2.3.0)(draft-js@0.10.5)(draftjs-utils@0.9.4)(immutable@3.7.6):
+    resolution: {integrity: sha512-O2cKysURNC4HSEMKgNmQ2RluwcrxvYrztlEmyPN5SzktiNX3vaLFQoo0Ez3PlIhvjaGrIBSIT2Oyh2N6mn6TFg==}
+    peerDependencies:
+      braft-convert: ^2.1.4
+      draft-js: ^0.10.5
+      draftjs-utils: ^0.9.4
+      immutable: ~3.7.4
+    dependencies:
+      braft-convert: 2.3.0(react-dom@18.2.0)(react@18.2.0)
+      draft-js: 0.10.5(react-dom@18.2.0)(react@18.2.0)
+      draftjs-utils: 0.9.4(draft-js@0.10.5)(immutable@3.7.6)
+      immutable: 3.7.6
+    dev: false
+
   /browser-process-hrtime@1.0.0:
     resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==}
     dev: false
@@ -4152,6 +4213,11 @@ packages:
     requiresBuild: true
     dev: false
 
+  /core-js@1.2.7:
+    resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==}
+    deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
+    dev: false
+
   /core-js@3.33.2:
     resolution: {integrity: sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==}
     requiresBuild: true
@@ -4789,6 +4855,50 @@ packages:
     engines: {node: '>=10'}
     dev: false
 
+  /draft-convert@2.1.13(draft-js@0.10.5)(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-/h/n4JCfyO8aWby7wKBkccHdsuVbbDyHWXi/B3Zf2pN++lN1lDOIVt5ulXCcbH2Y5YJEFzMJw/YGfN+R0axxxg==}
+    peerDependencies:
+      draft-js: '>=0.7.0'
+      react: ^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@babel/runtime': 7.23.2
+      draft-js: 0.10.5(react-dom@18.2.0)(react@18.2.0)
+      immutable: 3.7.6
+      invariant: 2.2.4
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /draft-js-multidecorators@1.0.0:
+    resolution: {integrity: sha512-7qdy+YQol5iq38AoEerhgSJWhCzxvZLn1x5ODfUlGfWlg0SrZ9AXJbaxHVIjdSIZNrbVIm+WANujNxMqCmDSZQ==}
+    dependencies:
+      immutable: 4.3.4
+    dev: false
+
+  /draft-js@0.10.5(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-LE6jSCV9nkPhfVX2ggcRLA4FKs6zWq9ceuO/88BpXdNCS7mjRTgs0NsV6piUCJX9YxMsB9An33wnkMmU2sD2Zg==}
+    peerDependencies:
+      react: ^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0
+      react-dom: ^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0
+    dependencies:
+      fbjs: 0.8.18
+      immutable: 3.7.6
+      object-assign: 4.1.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: false
+
+  /draftjs-utils@0.9.4(draft-js@0.10.5)(immutable@3.7.6):
+    resolution: {integrity: sha512-KYjABSbGpJrwrwmxVj5UhfV37MF/p0QRxKIyL+/+QOaJ8J9z1FBKxkblThbpR0nJi9lxPQWGg+gh+v0dAsSCCg==}
+    peerDependencies:
+      draft-js: ^0.10.x
+      immutable: 3.x.x || 4.x.x
+    dependencies:
+      draft-js: 0.10.5(react-dom@18.2.0)(react@18.2.0)
+      immutable: 3.7.6
+    dev: false
+
   /duplexer@0.1.2:
     resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
     dev: false
@@ -4832,6 +4942,12 @@ packages:
     engines: {node: '>= 0.8'}
     dev: false
 
+  /encoding@0.1.13:
+    resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
+    dependencies:
+      iconv-lite: 0.6.3
+    dev: false
+
   /enhanced-resolve@5.15.0:
     resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==}
     engines: {node: '>=10.13.0'}
@@ -5500,6 +5616,18 @@ packages:
       bser: 2.1.1
     dev: false
 
+  /fbjs@0.8.18:
+    resolution: {integrity: sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA==}
+    dependencies:
+      core-js: 1.2.7
+      isomorphic-fetch: 2.2.1
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      promise: 7.3.1
+      setimmediate: 1.0.5
+      ua-parser-js: 0.7.37
+    dev: false
+
   /file-entry-cache@6.0.1:
     resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
     engines: {node: ^10.12.0 || >=12.0.0}
@@ -6125,6 +6253,11 @@ packages:
     resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
     dev: false
 
+  /immutable@3.7.6:
+    resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==}
+    engines: {node: '>=0.8.0'}
+    dev: false
+
   /immutable@4.3.4:
     resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
 
@@ -6183,6 +6316,12 @@ packages:
       side-channel: 1.0.4
     dev: false
 
+  /invariant@2.2.4:
+    resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+    dependencies:
+      loose-envify: 1.4.0
+    dev: false
+
   /ip@1.1.8:
     resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==}
     dev: false
@@ -6377,6 +6516,11 @@ packages:
       call-bind: 1.0.5
     dev: false
 
+  /is-stream@1.1.0:
+    resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
+    engines: {node: '>=0.10.0'}
+    dev: false
+
   /is-stream@2.0.1:
     resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
     engines: {node: '>=8'}
@@ -6442,6 +6586,13 @@ packages:
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
+  /isomorphic-fetch@2.2.1:
+    resolution: {integrity: sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA==}
+    dependencies:
+      node-fetch: 1.7.3
+      whatwg-fetch: 3.6.19
+    dev: false
+
   /istanbul-lib-coverage@3.2.2:
     resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
     engines: {node: '>=8'}
@@ -7525,6 +7676,13 @@ packages:
       tslib: 2.6.2
     dev: false
 
+  /node-fetch@1.7.3:
+    resolution: {integrity: sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==}
+    dependencies:
+      encoding: 0.1.13
+      is-stream: 1.1.0
+    dev: false
+
   /node-forge@0.10.0:
     resolution: {integrity: sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==}
     engines: {node: '>= 6.0.0'}
@@ -8660,6 +8818,12 @@ packages:
     engines: {node: '>=0.4.0'}
     dev: false
 
+  /promise@7.3.1:
+    resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
+    dependencies:
+      asap: 2.0.6
+    dev: false
+
   /promise@8.3.0:
     resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==}
     dependencies:
@@ -9954,6 +10118,10 @@ packages:
       has-property-descriptors: 1.0.1
     dev: false
 
+  /setimmediate@1.0.5:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+    dev: false
+
   /setprototypeof@1.1.0:
     resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
     dev: false
@@ -10704,6 +10872,10 @@ packages:
     hasBin: true
     dev: false
 
+  /ua-parser-js@0.7.37:
+    resolution: {integrity: sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==}
+    dev: false
+
   /unbox-primitive@1.0.2:
     resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
     dependencies:

+ 51 - 0
src/components/Z_RichText/index.module.scss

@@ -0,0 +1,51 @@
+.RichText {
+  :global {
+    .txtBox {
+      width: 1000px;
+      border: 1px solid #ccc;
+
+      // 隐藏媒体功能
+      .control-item.media {
+        display: none;
+      }
+
+      .bf-controlbar {
+        position: relative;
+
+        .upImgBox {
+          position: absolute;
+          bottom: 13px;
+          right: 15px;
+          cursor: pointer;
+          color: var(--themeColor);
+        }
+
+        .upImgBoxNo {
+          display: none;
+        }
+      }
+    }
+
+    .noUpThumb {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top 0.2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+
+    .bf-media .bf-image {
+      float: initial !important;
+      display: block;
+      margin: 10px auto;
+      max-width: 500px;
+      max-height: 500px;
+    }
+  }
+}

+ 194 - 0
src/components/Z_RichText/index.tsx

@@ -0,0 +1,194 @@
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import styles from "./index.module.scss";
+
+// 引入编辑器组件
+
+// 安装---npm install braft-editor --save --force
+// npm install braft-utils --save --force
+// @ts-ignore
+// import { ContentUtils } from "braft-utils";
+// @ts-ignore
+import BraftEditor from "braft-editor";
+// 引入编辑器样式
+import "braft-editor/dist/index.css";
+
+import classNames from "classnames";
+
+import { forwardRef, useImperativeHandle } from "react";
+import { message } from "antd";
+
+type Props = {
+  check: boolean; //表单校验,为fasle表示不校验
+  dirCode: string; //文件的code码
+  isLook: boolean; //是否是查看进来
+  ref: any; //当前自己的ref,给父组件调用
+  myUrl: string; //上传的api地址
+};
+
+function RichText({ check, dirCode, isLook, myUrl }: Props, ref: any) {
+  const [messageApi] = message.useMessage();
+
+  // 添加 上传 图片的dom
+  useEffect(() => {
+    if (!isLook) {
+      setTimeout(() => {
+        const dom = document.querySelector(".bf-controlbar")!;
+        const div = document.createElement("div");
+        div.className = "upImgBox";
+        // div.title = "上传图片";
+        div.innerHTML = "上传图片";
+        div.onclick = async () => {
+          myInput.current?.click();
+        };
+        dom.appendChild(div);
+      }, 200);
+
+      // 监听 富文本 的 class 变化,在全屏的时候会 富文本会添加上 fullscreen 的类
+      // 修复顶部样式冲突问题
+
+      const editorDom = document.querySelector(
+        ".bf-container"
+      ) as HTMLDivElement;
+
+      const observer = new MutationObserver(() => {
+        // console.log("change");
+        const dom = document.querySelector(".layoutRightTop") as HTMLDivElement;
+
+        if (editorDom.className.includes("fullscreen")) dom.style.zIndex = "-1";
+        else dom.style.zIndex = "100";
+      });
+
+      observer.observe(editorDom, {
+        attributes: true,
+      });
+
+      // 销毁监听
+      return () => {
+        observer.disconnect();
+      };
+    }
+  }, [isLook]);
+
+  // 编辑器文本
+  const [editorValue, setEditorValue] = useState(
+    // 初始内容
+    BraftEditor.createEditorState("")
+  );
+
+  // 判断 富文本是否为空
+  const isTxtFlag = useMemo(() => {
+    const txt: string = editorValue.toHTML();
+    if (
+      txt.replaceAll("<p>", "").replaceAll("</p>", "").replaceAll(" ", "") ===
+      ""
+    ) {
+      return true;
+    } else return false;
+  }, [editorValue]);
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 上传图片
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+        // 校验格式
+        const type = ["image/jpeg", "image/png"];
+        if (!type.includes(filesInfo.type)) {
+          e.target.value = "";
+          return messageApi.warning("只支持png、jpg和jpeg格式!");
+        }
+        // 校验大小
+        if (filesInfo.size > 5 * 1024 * 1024) {
+          e.target.value = "";
+          return messageApi.warning("最大支持5M!");
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append("type", "img");
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        try {
+          console.log(fd, myUrl);
+          // const res = await A1_APIupFile(fd, myUrl);
+          // if (res.code === 0) {
+          //   messageApi.success("上传成功!");
+          //   // 在光标位置插入图片
+          //   const newTxt = ContentUtils.insertMedias(editorValue, [
+          //     {
+          //       type: "IMAGE",
+          //       url: process.env.REACT_APP_API_URL + res.data.filePath,
+          //     },
+          //   ]);
+
+          //   setEditorValue(newTxt);
+          // }
+        } catch (error) {}
+      }
+    },
+    [dirCode, myUrl, messageApi]
+  );
+
+  // 让父组件调用的 回显 富文本
+  const ritxtShowFu = useCallback((val: string) => {
+    setEditorValue(BraftEditor.createEditorState(val));
+  }, []);
+
+  // 让父组件调用的返回 富文本信息 和 表单校验
+  const fatherBtnOkFu = useCallback(() => {
+    return { val: editorValue.toHTML(), flag: isTxtFlag };
+  }, [editorValue, isTxtFlag]);
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    ritxtShowFu,
+    fatherBtnOkFu,
+  }));
+
+  return (
+    <div className={styles.RichText}>
+      <input
+        id="upInput"
+        type="file"
+        accept=".png,.jpg,.jpeg"
+        ref={myInput}
+        onChange={(e) => handeUpPhoto(e)}
+        style={{ display: "none" }}
+      />
+
+      <div className="txtBox">
+        <BraftEditor
+          readOnly={isLook}
+          placeholder="请输入内容"
+          value={editorValue}
+          onChange={(e: any) => setEditorValue(e)}
+          imageControls={["remove"]}
+        />
+      </div>
+      <div
+        className={classNames(
+          "noUpThumb",
+          check && isTxtFlag ? "noUpThumbAc" : ""
+        )}
+      >
+        请输入正文!
+      </div>
+    </div>
+  );
+}
+
+// const MemoRichText = React.memo(RichText);
+
+export default forwardRef(RichText);

+ 171 - 0
src/pages/Collection/create-or-edit/index.tsx

@@ -0,0 +1,171 @@
+import { FormPageFooter, MemoSpinLoding } from "@/components";
+import {
+  DageFileCheckbox,
+  DageFileCheckboxMethods,
+  DageFileResponseType,
+  DageUpload,
+  DageUploadConsumer,
+  DageUploadProvider,
+  DageUploadType,
+} from "@dage/pc-components";
+import { DatePicker, Form, FormInstance, Input, Select } from "antd";
+import { useCallback, useRef, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+export default function CollectionCreateOrEditPage() {
+  const dageFileCheckboxRef = useRef<DageFileCheckboxMethods>(null);
+  const fileList = useRef<DageFileResponseType[]>([]);
+  const formRef = useRef<FormInstance>(null);
+  const navigate = useNavigate();
+  const params = useParams();
+  const [loading, setLoading] = useState(false);
+  const richTxtRef = useRef<any>(null);
+  const [check, setCheck] = useState(false);
+  // 文件的code码
+  const [dirCode, setDirCode] = useState("");
+
+  const handleCancel = useCallback(() => {
+    navigate(-1);
+  }, [navigate]);
+
+  const handleSubmit = useCallback(async () => {
+    if (!(await formRef.current?.validateFields())) return;
+
+    const { banner = [], ...rest } = formRef.current?.getFieldsValue();
+
+    if (params.id) {
+      rest.id = params.id;
+    }
+
+    handleCancel();
+  }, [handleCancel, params]);
+
+  const handleFileChange = useCallback((list: DageFileResponseType[]) => {
+    fileList.current = list;
+  }, []);
+
+  return (
+    <div style={{ position: "relative" }}>
+      {loading && <MemoSpinLoding />}
+      <DageUploadProvider>
+        <DageUploadConsumer>
+          {(data) => (
+            <>
+              <Form ref={formRef} labelCol={{ span: 2 }}>
+                <Form.Item
+                  label="标题"
+                  name="title"
+                  rules={[{ required: true, message: "请输入内容" }]}
+                >
+                  <Input
+                    className="w220"
+                    placeholder="请输入内容"
+                    maxLength={20}
+                    showCount
+                  />
+                </Form.Item>
+                <Form.Item label="时代" name="title">
+                  <Input
+                    className="w220"
+                    placeholder="请输入内容"
+                    maxLength={10}
+                    showCount
+                  />
+                </Form.Item>
+                <Form.Item label="质地" name="title">
+                  <Input
+                    className="w220"
+                    placeholder="请输入内容"
+                    maxLength={10}
+                    showCount
+                  />
+                </Form.Item>
+                <Form.Item label="级别" name="type">
+                  <Select
+                    defaultValue="lucy"
+                    style={{ width: 220 }}
+                    options={[
+                      { value: "jack", label: "Jack" },
+                      { value: "lucy", label: "Lucy" },
+                      { value: "Yiminghe", label: "yiminghe" },
+                      { value: "disabled", label: "Disabled", disabled: true },
+                    ]}
+                  />
+                </Form.Item>
+                <Form.Item
+                  label="来源"
+                  name="title"
+                  rules={[{ required: true, message: "请输入内容" }]}
+                >
+                  <Input
+                    className="w220"
+                    placeholder="请输入内容"
+                    maxLength={10}
+                    showCount
+                  />
+                </Form.Item>
+                <Form.Item label="简介" name="content">
+                  <Input.TextArea
+                    className="w450"
+                    placeholder="请输入内容,最多200字"
+                    maxLength={200}
+                    showCount
+                    rows={6}
+                  />
+                </Form.Item>
+                <Form.Item
+                  label="封面图"
+                  name="banner"
+                  rules={[{ required: true, message: "请上传封面图" }]}
+                >
+                  <DageUpload
+                    tips="支持png、jpg和jpeg格式;最多1张,最大20M"
+                    action={
+                      process.env.REACT_APP_API_URL + "/api/cms/history/upload"
+                    }
+                    maxSize={20}
+                    maxCount={1}
+                  />
+                </Form.Item>
+                <Form.Item
+                  label="文件类型"
+                  name="fileTypes"
+                  rules={[
+                    {
+                      validator: (...args) => {
+                        dageFileCheckboxRef.current!.validate(...args);
+                      },
+                    },
+                  ]}
+                  validateTrigger="onSubmit"
+                >
+                  <DageFileCheckbox
+                    ref={dageFileCheckboxRef}
+                    hasMobileModel={true}
+                    action="https://sit-shgybwg.4dage.com/api/cms/goods/upload"
+                    onFileChange={handleFileChange}
+                  />
+                </Form.Item>
+                <Form.Item
+                  label="发布日期"
+                  name="date"
+                  rules={[{ required: true, message: "请选择发布日期" }]}
+                >
+                  <DatePicker className="w220" />
+                </Form.Item>
+              </Form>
+
+              {!loading && (
+                <FormPageFooter
+                  disabled={data?.uploading}
+                  onSubmit={handleSubmit}
+                  onCancel={handleCancel}
+                />
+              )}
+            </>
+          )}
+        </DageUploadConsumer>
+      </DageUploadProvider>
+    </div>
+  );
+}

+ 126 - 2
src/pages/Collection/index.tsx

@@ -1,3 +1,127 @@
-export default function BannerPage() {
-  return <div></div>;
+import { Button, Form, FormInstance, Input, Radio, Select, Table } from "antd";
+import { useCallback, useMemo, useRef, useState } from "react";
+import { debounce } from "lodash";
+import { DageTableActions } from "@dage/pc-components";
+import { useNavigate } from "react-router-dom";
+
+const DEFAULT_PARAMS = {
+  pageNum: 1,
+  pageSize: 20,
+};
+
+export default function CollectionPage() {
+  const navigate = useNavigate();
+  const formRef = useRef<FormInstance>(null);
+  const [loading, setLoading] = useState(false);
+  const [list, setList] = useState<[]>([]);
+  const [params, setParams] = useState({
+    ...DEFAULT_PARAMS,
+  });
+  const [total, setTotal] = useState(0);
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "标题",
+        dataIndex: "title",
+      },
+      {
+        title: "年代",
+        dataIndex: "nickName",
+      },
+      {
+        title: "类别",
+        dataIndex: "date",
+      },
+      {
+        title: "级别",
+        dataIndex: "date",
+      },
+      {
+        title: "来源",
+        dataIndex: "date",
+      },
+      {
+        title: "封面",
+        dataIndex: "date",
+      },
+      {
+        title: "简介",
+        dataIndex: "date",
+      },
+      {
+        title: "发布日期",
+        dataIndex: "date",
+      },
+      {
+        title: "操作",
+        render: (item: any) => {
+          return <DageTableActions onEdit={() => {}} onDelete={() => {}} />;
+        },
+      },
+    ];
+  }, []);
+
+  const handleReset = useCallback(() => {
+    formRef.current?.resetFields();
+  }, [formRef]);
+
+  const debounceSearch = useMemo(
+    () => debounce((changedVal: unknown, vals: any) => {}, 500),
+    []
+  );
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
+    },
+    [params]
+  );
+
+  return (
+    <div className="information">
+      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
+        <Form.Item label="搜索项" name="stage">
+          <Input placeholder="请输入" maxLength={10} showCount allowClear />
+        </Form.Item>
+        <Form.Item label="级别" name="type">
+          <Select
+            defaultValue="lucy"
+            style={{ width: 220 }}
+            options={[
+              { value: "jack", label: "Jack" },
+              { value: "lucy", label: "Lucy" },
+              { value: "Yiminghe", label: "yiminghe" },
+              { value: "disabled", label: "Disabled", disabled: true },
+            ]}
+          />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" onClick={() => navigate("/collection/create")}>
+            新增
+          </Button>
+        </Form.Item>
+        <Form.Item>
+          <Button onClick={handleReset}>重置</Button>
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={COLUMNS}
+        rowKey="id"
+        pagination={{
+          showQuickJumper: true,
+          position: ["bottomCenter"],
+          showSizeChanger: true,
+          current: params.pageNum,
+          pageSize: params.pageSize,
+          total,
+          onChange: paginationChange(),
+        }}
+      />
+    </div>
+  );
 }

+ 114 - 2
src/pages/Exhibition/index.tsx

@@ -1,3 +1,115 @@
-export default function BannerPage() {
-  return <div></div>;
+import { Button, Form, FormInstance, Input, Radio, Select, Table } from "antd";
+import { useCallback, useMemo, useRef, useState } from "react";
+import { debounce } from "lodash";
+import { DageTableActions } from "@dage/pc-components";
+import { useNavigate } from "react-router-dom";
+
+const DEFAULT_PARAMS = {
+  pageNum: 1,
+  pageSize: 20,
+};
+
+export default function InformationPage() {
+  const navigate = useNavigate();
+  const formRef = useRef<FormInstance>(null);
+  const [loading, setLoading] = useState(false);
+  const [list, setList] = useState<[]>([]);
+  const [params, setParams] = useState({
+    ...DEFAULT_PARAMS,
+  });
+  const [total, setTotal] = useState(0);
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "展览标题",
+        dataIndex: "title",
+      },
+      {
+        title: "展览类型",
+        dataIndex: "nickName",
+      },
+      {
+        title: "展览封面",
+        dataIndex: "date",
+      },
+      {
+        title: "展览位置",
+        dataIndex: "date",
+      },
+      {
+        title: "发布日期",
+        dataIndex: "date",
+      },
+      {
+        title: "操作",
+        render: (item: any) => {
+          return <DageTableActions onEdit={() => {}} onDelete={() => {}} />;
+        },
+      },
+    ];
+  }, []);
+
+  const handleReset = useCallback(() => {
+    formRef.current?.resetFields();
+  }, [formRef]);
+
+  const debounceSearch = useMemo(
+    () => debounce((changedVal: unknown, vals: any) => {}, 500),
+    []
+  );
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
+    },
+    [params]
+  );
+
+  return (
+    <div className="information">
+      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
+        <Form.Item label="搜索项" name="stage">
+          <Input placeholder="请输入" maxLength={10} showCount allowClear />
+        </Form.Item>
+        <Form.Item label="展览类型" name="type">
+          <Select
+            defaultValue="lucy"
+            style={{ width: 220 }}
+            options={[
+              { value: "jack", label: "Jack" },
+              { value: "lucy", label: "Lucy" },
+              { value: "Yiminghe", label: "yiminghe" },
+              { value: "disabled", label: "Disabled", disabled: true },
+            ]}
+          />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" onClick={() => navigate("/exhibition/create")}>
+            新增
+          </Button>
+        </Form.Item>
+        <Form.Item>
+          <Button onClick={handleReset}>重置</Button>
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={COLUMNS}
+        rowKey="id"
+        pagination={{
+          showQuickJumper: true,
+          position: ["bottomCenter"],
+          showSizeChanger: true,
+          current: params.pageNum,
+          pageSize: params.pageSize,
+          total,
+          onChange: paginationChange(),
+        }}
+      />
+    </div>
+  );
 }

+ 119 - 0
src/pages/Information/create-or-edit/index.tsx

@@ -0,0 +1,119 @@
+import { FormPageFooter, MemoSpinLoding } from "@/components";
+import {
+  DageUpload,
+  DageUploadConsumer,
+  DageUploadProvider,
+  DageUploadType,
+} from "@dage/pc-components";
+import { DatePicker, Form, FormInstance, Input } from "antd";
+import { useCallback, useRef, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import RichText from "@/components/Z_RichText";
+
+export default function InformationCreateOrEditPage() {
+  const formRef = useRef<FormInstance>(null);
+  const navigate = useNavigate();
+  const params = useParams();
+  const [loading, setLoading] = useState(false);
+  const richTxtRef = useRef<any>(null);
+  const [check, setCheck] = useState(false);
+  // 文件的code码
+  const [dirCode, setDirCode] = useState("");
+
+  const handleCancel = useCallback(() => {
+    navigate(-1);
+  }, [navigate]);
+
+  const handleSubmit = useCallback(async () => {
+    if (!(await formRef.current?.validateFields())) return;
+
+    const { banner = [], ...rest } = formRef.current?.getFieldsValue();
+
+    if (params.id) {
+      rest.id = params.id;
+    }
+
+    handleCancel();
+  }, [handleCancel, params]);
+
+  return (
+    <div style={{ position: "relative" }}>
+      {loading && <MemoSpinLoding />}
+      <DageUploadProvider>
+        <DageUploadConsumer>
+          {(data) => (
+            <>
+              <Form ref={formRef} labelCol={{ span: 2 }}>
+                <Form.Item
+                  label="标题"
+                  name="title"
+                  rules={[{ required: true, message: "请输入内容" }]}
+                >
+                  <Input
+                    className="w220"
+                    placeholder="请输入内容,最多20字"
+                    maxLength={20}
+                    showCount
+                  />
+                </Form.Item>
+                <Form.Item
+                  label="正文"
+                  name="content"
+                  rules={[{ required: true, message: "请输入正文" }]}
+                >
+                  <RichText
+                    myUrl="cms/goods/upload"
+                    ref={richTxtRef}
+                    check={check}
+                    dirCode={dirCode}
+                    isLook={false}
+                  />
+                </Form.Item>
+                <Form.Item
+                  label="发布日期"
+                  name="date"
+                  rules={[{ required: true, message: "请选择发布日期" }]}
+                >
+                  <DatePicker className="w220" />
+                </Form.Item>
+                <Form.Item
+                  label="封面图"
+                  name="banner"
+                  rules={[{ required: true, message: "请上传封面图" }]}
+                >
+                  <DageUpload
+                    tips="支持png、jpg和jpeg格式;最多1张,最大20M"
+                    action={
+                      process.env.REACT_APP_API_URL + "/api/cms/history/upload"
+                    }
+                    maxSize={20}
+                    maxCount={1}
+                  />
+                </Form.Item>
+                <Form.Item label="视频" name="video">
+                  <DageUpload
+                    dType={DageUploadType.VIDEO}
+                    tips="支持avi,mp4,mkv,wmv等格式;最大200M,最多1个"
+                    action={
+                      process.env.REACT_APP_API_URL + "/api/cms/history/upload"
+                    }
+                    maxSize={200}
+                    maxCount={1}
+                  />
+                </Form.Item>
+              </Form>
+
+              {!loading && (
+                <FormPageFooter
+                  disabled={data?.uploading}
+                  onSubmit={handleSubmit}
+                  onCancel={handleCancel}
+                />
+              )}
+            </>
+          )}
+        </DageUploadConsumer>
+      </DageUploadProvider>
+    </div>
+  );
+}

+ 8 - 1
src/pages/Information/index.tsx

@@ -2,6 +2,7 @@ import { Button, Form, FormInstance, Input, Radio, Table } from "antd";
 import { useCallback, useMemo, useRef, useState } from "react";
 import { debounce } from "lodash";
 import { DageTableActions } from "@dage/pc-components";
+import { useNavigate } from "react-router-dom";
 
 const TYPE_LIST = [
   {
@@ -28,6 +29,7 @@ const DEFAULT_PARAMS = {
 };
 
 export default function InformationPage() {
+  const navigate = useNavigate();
   const formRef = useRef<FormInstance>(null);
   const [loading, setLoading] = useState(false);
   const [list, setList] = useState<[]>([]);
@@ -91,7 +93,12 @@ export default function InformationPage() {
           <Input className="w220" placeholder="请输入标题或正文" allowClear />
         </Form.Item>
         <Form.Item>
-          <Button type="primary">新增</Button>
+          <Button
+            type="primary"
+            onClick={() => navigate("/information/create")}
+          >
+            新增
+          </Button>
         </Form.Item>
         <Form.Item>
           <Button onClick={handleReset}>重置</Button>

+ 93 - 2
src/pages/Message/index.tsx

@@ -1,3 +1,94 @@
-export default function BannerPage() {
-  return <div></div>;
+import { Button, Form, FormInstance, Input, Select, Table } from "antd";
+import { useCallback, useMemo, useRef, useState } from "react";
+import { debounce } from "lodash";
+import { DageTableActions } from "@dage/pc-components";
+import { useNavigate } from "react-router-dom";
+
+const DEFAULT_PARAMS = {
+  pageNum: 1,
+  pageSize: 20,
+};
+
+export default function MessagePage() {
+  const navigate = useNavigate();
+  const formRef = useRef<FormInstance>(null);
+  const [loading, setLoading] = useState(false);
+  const [list, setList] = useState<[]>([]);
+  const [params, setParams] = useState({
+    ...DEFAULT_PARAMS,
+  });
+  const [total, setTotal] = useState(0);
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "留言时间",
+        dataIndex: "title",
+      },
+      {
+        title: "称号",
+        dataIndex: "nickName",
+      },
+      {
+        title: "联系方式",
+        dataIndex: "date",
+      },
+      {
+        title: "正文",
+        dataIndex: "date",
+      },
+      {
+        title: "操作",
+        render: (item: any) => {
+          return <DageTableActions onDelete={() => {}} />;
+        },
+      },
+    ];
+  }, []);
+
+  const handleReset = useCallback(() => {
+    formRef.current?.resetFields();
+  }, [formRef]);
+
+  const debounceSearch = useMemo(
+    () => debounce((changedVal: unknown, vals: any) => {}, 500),
+    []
+  );
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
+    },
+    [params]
+  );
+
+  return (
+    <div className="information">
+      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
+        <Form.Item label="搜索项" name="stage">
+          <Input placeholder="请输入" maxLength={10} showCount allowClear />
+        </Form.Item>
+        <Form.Item>
+          <Button onClick={handleReset}>重置</Button>
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={COLUMNS}
+        rowKey="id"
+        pagination={{
+          showQuickJumper: true,
+          position: ["bottomCenter"],
+          showSizeChanger: true,
+          current: params.pageNum,
+          pageSize: params.pageSize,
+          total,
+          onChange: paginationChange(),
+        }}
+      />
+    </div>
+  );
 }

+ 132 - 0
src/pages/Questionnaire/components/topicDrawer.tsx

@@ -0,0 +1,132 @@
+import {
+  Button,
+  Drawer,
+  Form,
+  FormInstance,
+  Input,
+  Radio,
+  Space,
+  Switch,
+} from "antd";
+import { FC, useRef, useState } from "react";
+
+export interface TopicDrawerProps {
+  open: boolean;
+  close: () => void;
+}
+
+let id = 0;
+
+export const TopicDrawer: FC<TopicDrawerProps> = ({ open, close }) => {
+  const optionsFormRef = useRef<FormInstance>(null);
+
+  const [options, setOptions] = useState<
+    {
+      id: number;
+    }[]
+  >([]);
+
+  const handleAddOption = () => {
+    setOptions((prev) => [
+      ...prev,
+      {
+        id: id++,
+      },
+    ]);
+  };
+
+  const handleRemoveOption = (idx: number) => {
+    setOptions((prev) => {
+      prev.splice(idx, 1);
+      return [...prev];
+    });
+  };
+
+  const handleSubmit = async () => {
+    if (!(await optionsFormRef.current?.validateFields())) return;
+
+    const options = optionsFormRef.current?.getFieldsValue();
+    console.log(options);
+  };
+
+  return (
+    <Drawer
+      title="新增题目"
+      placement="right"
+      width={500}
+      open={open}
+      onClose={close}
+      extra={
+        <Space>
+          <Button onClick={close}>取消</Button>
+          <Button type="primary" onClick={handleSubmit}>
+            保存
+          </Button>
+        </Space>
+      }
+    >
+      <Form layout="vertical">
+        <Form.Item
+          label="题目描述"
+          name="title"
+          rules={[{ required: true, message: "请输入内容" }]}
+        >
+          <Input.TextArea
+            placeholder="请输入内容,最多50字"
+            maxLength={50}
+            showCount
+            rows={4}
+          />
+        </Form.Item>
+
+        <Form.Item label="类型" name="type">
+          <Radio.Group buttonStyle="solid">
+            <Radio.Button value="1">单选</Radio.Button>
+            <Radio.Button value="2">多选</Radio.Button>
+          </Radio.Group>
+        </Form.Item>
+
+        <Form.Item label="允许自定义答案" name="cus">
+          <Switch />
+        </Form.Item>
+      </Form>
+
+      <Form ref={optionsFormRef}>
+        <Button
+          type="primary"
+          ghost
+          onClick={handleAddOption}
+          style={{ marginBottom: "15px" }}
+        >
+          新增选项
+        </Button>
+
+        {options.map((i, idx) => (
+          <Form.Item
+            key={i.id}
+            label={`选项${idx + 1}`}
+            name={`option_${i.id}`}
+            rules={[{ required: true, message: "请输入" }]}
+          >
+            <div
+              style={{
+                display: "flex",
+                alignItems: "center",
+              }}
+            >
+              <Input placeholder="请输入" style={{ marginRight: "10px" }} />
+              <Button
+                type="text"
+                danger
+                size="small"
+                onClick={handleRemoveOption.bind(undefined, idx)}
+              >
+                删除
+              </Button>
+            </div>
+          </Form.Item>
+        ))}
+      </Form>
+    </Drawer>
+  );
+};

+ 109 - 0
src/pages/Questionnaire/create-or-edit/index.tsx

@@ -0,0 +1,109 @@
+import { FormPageFooter, MemoSpinLoding } from "@/components";
+import { Button, DatePicker, Form, FormInstance, Input, Select } from "antd";
+import { useCallback, useRef, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { TopicDrawer } from "../components/topicDrawer";
+
+export default function QuestionnaireCreateOrEditPage() {
+  const formRef = useRef<FormInstance>(null);
+  const navigate = useNavigate();
+  const params = useParams();
+  const [loading, setLoading] = useState(false);
+  const [drawerVisible, setDrawerVisible] = useState(false);
+
+  const handleCancel = useCallback(() => {
+    navigate(-1);
+  }, [navigate]);
+
+  const handleSubmit = useCallback(async () => {
+    if (!(await formRef.current?.validateFields())) return;
+
+    const { ...rest } = formRef.current?.getFieldsValue();
+
+    if (params.id) {
+      rest.id = params.id;
+    }
+
+    handleCancel();
+  }, [handleCancel, params]);
+
+  const handleAddTopic = () => {
+    setDrawerVisible(true);
+  };
+
+  const handleCloseDrawer = () => {
+    setDrawerVisible(false);
+  };
+
+  return (
+    <div style={{ position: "relative" }}>
+      {loading && <MemoSpinLoding />}
+
+      <Form ref={formRef} labelCol={{ span: 2 }}>
+        <Form.Item
+          label="标题"
+          name="title"
+          rules={[{ required: true, message: "请输入内容" }]}
+        >
+          <Input
+            className="w220"
+            placeholder="请输入内容,最多20字"
+            maxLength={20}
+            showCount
+          />
+        </Form.Item>
+
+        <Form.Item
+          label="摘要"
+          name="content"
+          rules={[{ required: true, message: "请输入内容" }]}
+        >
+          <Input.TextArea
+            className="w450"
+            placeholder="请输入内容,最多200字"
+            maxLength={200}
+            showCount
+            rows={6}
+          />
+        </Form.Item>
+
+        <Form.Item
+          label="题目"
+          name="content"
+          rules={[{ required: true, message: "请新增题目" }]}
+        >
+          <Button type="primary" onClick={handleAddTopic}>
+            新增
+          </Button>
+        </Form.Item>
+
+        <Form.Item
+          label="发布日期"
+          name="date"
+          rules={[{ required: true, message: "请选择日期" }]}
+        >
+          <DatePicker className="w220" />
+        </Form.Item>
+
+        <Form.Item label="展示状态" name="type">
+          <Select
+            defaultValue="lucy"
+            style={{ width: 220 }}
+            options={[
+              { value: "jack", label: "Jack" },
+              { value: "lucy", label: "Lucy" },
+              { value: "Yiminghe", label: "yiminghe" },
+              { value: "disabled", label: "Disabled", disabled: true },
+            ]}
+          />
+        </Form.Item>
+      </Form>
+
+      {!loading && (
+        <FormPageFooter onSubmit={handleSubmit} onCancel={handleCancel} />
+      )}
+
+      <TopicDrawer open={drawerVisible} close={handleCloseDrawer} />
+    </div>
+  );
+}

+ 110 - 2
src/pages/Questionnaire/index.tsx

@@ -1,3 +1,111 @@
-export default function BannerPage() {
-  return <div></div>;
+import { Button, Form, FormInstance, Input, Radio, Select, Table } from "antd";
+import { useCallback, useMemo, useRef, useState } from "react";
+import { debounce } from "lodash";
+import { DageTableActions } from "@dage/pc-components";
+import { useNavigate } from "react-router-dom";
+
+const DEFAULT_PARAMS = {
+  pageNum: 1,
+  pageSize: 20,
+};
+
+export default function InformationPage() {
+  const navigate = useNavigate();
+  const formRef = useRef<FormInstance>(null);
+  const [loading, setLoading] = useState(false);
+  const [list, setList] = useState<[]>([]);
+  const [params, setParams] = useState({
+    ...DEFAULT_PARAMS,
+  });
+  const [total, setTotal] = useState(0);
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "标题",
+        dataIndex: "title",
+      },
+      {
+        title: "问题数量",
+        dataIndex: "nickName",
+      },
+      {
+        title: "已收集份数",
+        dataIndex: "date",
+      },
+      {
+        title: "发布日期",
+        dataIndex: "date",
+      },
+      {
+        title: "展示状态",
+        dataIndex: "date",
+      },
+      {
+        title: "操作",
+        render: (item: any) => {
+          return <DageTableActions onEdit={() => {}} onDelete={() => {}} />;
+        },
+      },
+    ];
+  }, []);
+
+  const handleReset = useCallback(() => {
+    formRef.current?.resetFields();
+  }, [formRef]);
+
+  const debounceSearch = useMemo(
+    () => debounce((changedVal: unknown, vals: any) => {}, 500),
+    []
+  );
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
+    },
+    [params]
+  );
+
+  return (
+    <div className="information">
+      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
+        <Form.Item label="搜索项" name="stage">
+          <Input
+            placeholder="请输入标题,最多10字"
+            maxLength={10}
+            showCount
+            allowClear
+          />
+        </Form.Item>
+        <Form.Item>
+          <Button
+            type="primary"
+            onClick={() => navigate("/questionnaire/create")}
+          >
+            新增
+          </Button>
+        </Form.Item>
+        <Form.Item>
+          <Button onClick={handleReset}>重置</Button>
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={COLUMNS}
+        rowKey="id"
+        pagination={{
+          showQuickJumper: true,
+          position: ["bottomCenter"],
+          showSizeChanger: true,
+          current: params.pageNum,
+          pageSize: params.pageSize,
+          total,
+          onChange: paginationChange(),
+        }}
+      />
+    </div>
+  );
 }

+ 30 - 0
src/router/index.tsx

@@ -33,6 +33,16 @@ export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
     title: "资讯管理",
     icon: <CommentOutlined />,
     Component: React.lazy(() => import("../pages/Information")),
+    children: [
+      {
+        path: "/information/create",
+        title: "新增",
+        hide: true,
+        Component: React.lazy(
+          () => import("../pages/Information/create-or-edit")
+        ),
+      },
+    ],
   },
   {
     path: "/exhibition",
@@ -45,12 +55,32 @@ export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
     title: "藏品管理",
     icon: <BookOutlined />,
     Component: React.lazy(() => import("../pages/Collection")),
+    children: [
+      {
+        path: "/collection/create",
+        title: "新增",
+        hide: true,
+        Component: React.lazy(
+          () => import("../pages/Collection/create-or-edit")
+        ),
+      },
+    ],
   },
   {
     path: "/questionnaire",
     title: "问卷管理",
     icon: <ReadOutlined />,
     Component: React.lazy(() => import("../pages/Questionnaire")),
+    children: [
+      {
+        path: "/questionnaire/create",
+        title: "新增",
+        hide: true,
+        Component: React.lazy(
+          () => import("../pages/Questionnaire/create-or-edit")
+        ),
+      },
+    ],
   },
   {
     path: "/message",