chenlei 7 kuukautta sitten
vanhempi
commit
1eb1faf17b

+ 8 - 3
package.json

@@ -30,6 +30,7 @@
     "classnames": "^2.5.1",
     "css-loader": "^6.5.1",
     "css-minimizer-webpack-plugin": "^3.2.0",
+    "docxtemplater": "^3.54.0",
     "dotenv": "^10.0.0",
     "dotenv-expand": "^5.1.0",
     "echarts-for-react": "^3.0.2",
@@ -37,6 +38,7 @@
     "eslint-config-react-app": "^7.0.1",
     "eslint-webpack-plugin": "^3.1.1",
     "file-loader": "^6.2.0",
+    "file-saver": "^2.0.5",
     "fs-extra": "^10.0.0",
     "html-webpack-plugin": "^5.5.0",
     "identity-obj-proxy": "^3.0.0",
@@ -44,8 +46,11 @@
     "jest-resolve": "^27.4.2",
     "jest-watch-typeahead": "^1.0.0",
     "js-base64": "^3.7.5",
+    "jszip-utils": "^0.1.0",
     "lodash": "^4.17.21",
     "mini-css-extract-plugin": "^2.4.5",
+    "path-to-regexp": "^8.2.0",
+    "pizzip": "^3.1.7",
     "postcss": "^8.4.4",
     "postcss-flexbugs-fixes": "^5.0.2",
     "postcss-loader": "^6.2.1",
@@ -78,8 +83,8 @@
     "workbox-webpack-plugin": "^6.4.1"
   },
   "scripts": {
-    "start": "cross-env REACT_APP_API_URL=https://sit-shgybwg.4dage.com node scripts/start.js",
-    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shgybwg.4dage.com node scripts/build.js"
+    "start": "cross-env REACT_APP_API_URL=http://192.168.20.61:8090 REACT_APP_IMG_PUBLIC=/api node scripts/start.js",
+    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shgybwg.4dage.com REACT_APP_IMG_PUBLIC= node scripts/build.js"
   },
   "eslintConfig": {
     "extends": [
@@ -159,4 +164,4 @@
       "react-app"
     ]
   }
-}
+}

+ 50 - 0
pnpm-lock.yaml

@@ -86,6 +86,9 @@ dependencies:
   css-minimizer-webpack-plugin:
     specifier: ^3.2.0
     version: 3.4.1(webpack@5.96.1)
+  docxtemplater:
+    specifier: ^3.54.0
+    version: 3.54.0
   dotenv:
     specifier: ^10.0.0
     version: 10.0.0
@@ -107,6 +110,9 @@ dependencies:
   file-loader:
     specifier: ^6.2.0
     version: 6.2.0(webpack@5.96.1)
+  file-saver:
+    specifier: ^2.0.5
+    version: 2.0.5
   fs-extra:
     specifier: ^10.0.0
     version: 10.1.0
@@ -128,12 +134,21 @@ dependencies:
   js-base64:
     specifier: ^3.7.5
     version: 3.7.7
+  jszip-utils:
+    specifier: ^0.1.0
+    version: 0.1.0
   lodash:
     specifier: ^4.17.21
     version: 4.17.21
   mini-css-extract-plugin:
     specifier: ^2.4.5
     version: 2.9.2(webpack@5.96.1)
+  path-to-regexp:
+    specifier: ^8.2.0
+    version: 8.2.0
+  pizzip:
+    specifier: ^3.1.7
+    version: 3.1.7
   postcss:
     specifier: ^8.4.4
     version: 8.4.47
@@ -4156,6 +4171,11 @@ packages:
       '@xtuc/long': 4.2.2
     dev: false
 
+  /@xmldom/xmldom@0.9.5:
+    resolution: {integrity: sha512-6g1EwSs8cr8JhP1iBxzyVAWM6BIDvx9Y3FZRIQiMDzgG43Pxi8YkWOZ0nQj2NHgNzgXDZbJewFx/n+YAvMZrfg==}
+    engines: {node: '>=14.6'}
+    dev: false
+
   /@xtuc/ieee754@1.2.0:
     resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
     dev: false
@@ -5795,6 +5815,13 @@ packages:
       esutils: 2.0.3
     dev: false
 
+  /docxtemplater@3.54.0:
+    resolution: {integrity: sha512-nmQu2znB6h6WrRSnbK66DtP9CqYM/Ig/nafEQbSHq8Q7VGho1HmbJLdzyMMzavIgZClS6OYfjuVAL7deKHdlTA==}
+    engines: {node: '>=0.10'}
+    dependencies:
+      '@xmldom/xmldom': 0.9.5
+    dev: false
+
   /dom-accessibility-api@0.5.16:
     resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
     dev: false
@@ -6729,6 +6756,10 @@ packages:
       webpack: 5.96.1
     dev: false
 
+  /file-saver@2.0.5:
+    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+    dev: false
+
   /filelist@1.0.4:
     resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
     dependencies:
@@ -8476,6 +8507,10 @@ packages:
       object.values: 1.2.0
     dev: false
 
+  /jszip-utils@0.1.0:
+    resolution: {integrity: sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==}
+    dev: false
+
   /keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
     dependencies:
@@ -9160,6 +9195,10 @@ packages:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
     dev: false
 
+  /pako@2.1.0:
+    resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+    dev: false
+
   /param-case@3.0.4:
     resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
     dependencies:
@@ -9240,6 +9279,11 @@ packages:
     engines: {node: '>=16'}
     dev: false
 
+  /path-to-regexp@8.2.0:
+    resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
+    engines: {node: '>=16'}
+    dev: false
+
   /path-type@4.0.0:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
@@ -9270,6 +9314,12 @@ packages:
     engines: {node: '>= 6'}
     dev: false
 
+  /pizzip@3.1.7:
+    resolution: {integrity: sha512-VemVeAQtdIA74AN1Fsd5OmbMbEeS4YOwwlcudgzvmUrOIOPrk1idYC5Tw5FUFq/I0c26ziNOw9z//iPmGfp1jA==}
+    dependencies:
+      pako: 2.1.0
+    dev: false
+
   /pkg-dir@4.2.0:
     resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
     engines: {node: '>=8'}

BIN
public/template.docx


+ 3 - 0
src/css.d.ts

@@ -7,3 +7,6 @@ declare module "*.scss" {
   const classes: { readonly [key: string]: string };
   export default classes;
 }
+
+declare module "jszip-utils";
+declare module "file-saver";

+ 3 - 3
src/pages/AssessmentDetail/components/IndexAssessment/index.tsx

@@ -85,12 +85,12 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
                     navigate(
                       isReportDetail
                         ? // 考核填报
-                          "/management/form/detail/index"
+                          "/management/form/detail/1/index"
                         : isEvalutionDetail
                         ? // 考核评定
-                          "/management/evaluation/detail/index"
+                          "/management/evaluation/detail/1/index"
                         : // 考核管理
-                          "/management/index/detail/index"
+                          "/management/index/detail/1/index"
                     )
                   }
                 >

+ 13 - 10
src/pages/Layout/components/Header/index.tsx

@@ -5,7 +5,7 @@ import { Header } from "antd/es/layout/layout";
 import { useSelector } from "react-redux";
 import { RootState } from "@/store";
 import { ResetPassword } from "./components/ResetPassword";
-import { logout } from "@/utils";
+import { getImgFullPath, logout } from "@/utils";
 import { DageRouteItem } from "@/router";
 import { useLocation } from "react-router-dom";
 import { findRouteByPath } from "../../utils";
@@ -93,15 +93,18 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({ menuData }) => {
           </ul>
         }
       >
-        <div className={style.user}>
-          <Avatar
-            size={40}
-            shape="square"
-            icon={<UserOutlined />}
-            alt={userInfo?.user.nickName}
-          />
-          <span>{userInfo?.user.nickName}</span>
-        </div>
+        {userInfo && (
+          <div className={style.user}>
+            <Avatar
+              size={40}
+              shape="square"
+              icon={<UserOutlined />}
+              src={getImgFullPath(userInfo.user.thumb)}
+              alt={userInfo.user.realName}
+            />
+            <span>{userInfo.user.realName}</span>
+          </div>
+        )}
       </Popover>
 
       <ResetPassword

+ 4 - 17
src/pages/Layout/utils.ts

@@ -1,3 +1,4 @@
+import { match } from "path-to-regexp";
 import { DageRouteItem, DEFAULT_MENU, DEFAULT_ADMIN_MENU } from "@/router";
 
 const routes = [...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU];
@@ -29,22 +30,7 @@ export const findVisibleRouteParent = (
 ): DageRouteItem | null => {
   let result: DageRouteItem | null = null;
 
-  function search(list: DageRouteItem[], path: string): DageRouteItem | null {
-    for (const route of list) {
-      if (route.path === path) {
-        return route;
-      }
-      if (route.children) {
-        const found = search(route.children, path);
-        if (found) {
-          return found;
-        }
-      }
-    }
-    return null;
-  }
-
-  const targetNode = search(list, targetPath);
+  const targetNode = findRouteByPath(list, targetPath);
   if (targetNode) {
     result = findRouteParent(targetNode, list);
   }
@@ -57,7 +43,8 @@ export const findRouteByPath = (
   path: string
 ): DageRouteItem | null => {
   for (const route of list) {
-    if (route.path === path) {
+    const isMatch = match(route.path, { decode: decodeURIComponent });
+    if (isMatch(path)) {
       return route;
     }
     if (route.children) {

BIN
src/pages/Login/images/icon_code.png


+ 4 - 0
src/pages/Login/index.scss

@@ -72,5 +72,9 @@
         height: 70px;
       }
     }
+
+    .icon-code {
+      cursor: pointer;
+    }
   }
 }

+ 28 - 2
src/pages/Login/index.tsx

@@ -3,23 +3,27 @@ import { Button, Form, Input } from "antd";
 import { useNavigate } from "react-router-dom";
 import { Base64 } from "@dage/utils";
 import { encodeStr, setTokenInfo } from "@dage/pc-components";
+import { getBaseURL } from "@dage/service";
 import { login } from "@/api";
 import { LoginRequest } from "@/types";
 import { DEFAULT_ADMIN_MENU, DEFAULT_MENU } from "@/router";
 import IconAccount from "./images/icon_ac-min.png";
 import IconPassword from "./images/icon_pw-min.png";
+import IconCode from "./images/icon_code.png";
 import LogoIcon from "./images/logo_black-min.png";
 import "./index.scss";
 
 export default function Login() {
-  const [loading, setLoading] = useState(false);
   const navigate = useNavigate();
+  const baseUrl = getBaseURL();
+  const [loading, setLoading] = useState(false);
+  const [timestamp, setTimestamp] = useState(new Date().getTime());
 
   const handleLogin = async (vals: LoginRequest) => {
     setLoading(true);
 
     const obj = {
-      userName: vals.userName,
+      ...vals,
       passWord: encodeStr(Base64.encode(vals.passWord as string)),
     };
 
@@ -79,6 +83,28 @@ export default function Login() {
               variant="filled"
             />
           </Form.Item>
+          <Form.Item
+            name="randCode"
+            rules={[{ required: true, message: "请输入验证码!" }]}
+          >
+            <Input
+              prefix={
+                <img className="icon-password" src={IconCode} alt="验证码" />
+              }
+              suffix={
+                <img
+                  className="icon-code"
+                  src={`${baseUrl}/api/admin/getRandCode?t=${timestamp}`}
+                  alt="验证码"
+                  onClick={() => {
+                    setTimestamp(new Date().getTime());
+                  }}
+                />
+              }
+              placeholder="请输入验证码"
+              variant="filled"
+            />
+          </Form.Item>
 
           {/* 登录按钮 */}
           <div className="login-form__btn">

+ 1 - 1
src/pages/Management/Index/index.tsx

@@ -176,7 +176,7 @@ const ManagementIndexPage = () => {
                         <Button
                           type="text"
                           className={style.button}
-                          onClick={() => navigate("/management/index/detail")}
+                          onClick={() => navigate("/management/index/detail/1")}
                         >
                           查看
                         </Button>

+ 19 - 0
src/pages/Performance/Form/Edit/index.tsx

@@ -0,0 +1,19 @@
+import { FormPageFooter, PageContainer } from "@/components";
+import { Form, Input } from "antd";
+import { FC } from "react";
+
+const PerformanceFormEditPage: FC = () => {
+  return (
+    <PageContainer title="编辑报告">
+      <Form labelCol={{ span: 3 }}>
+        <Form.Item label="报告标题" required>
+          <Input className="w450" placeholder="请输入" />
+        </Form.Item>
+      </Form>
+
+      <FormPageFooter />
+    </PageContainer>
+  );
+};
+
+export default PerformanceFormEditPage;

+ 27 - 1
src/pages/Performance/Form/index.tsx

@@ -1,11 +1,14 @@
 import classNames from "classnames";
 import { useState } from "react";
 import { Button, Form, Input, Table } from "antd";
+import { useNavigate } from "react-router-dom";
 import { DageTableActions } from "@dage/pc-components";
 import { PageContainer } from "@/components";
+import { exportWordDocx } from "@/utils/exportWordDocx";
 import style from "../../Management/Form/index.module.scss";
 
 const ManagementEvaluationPage = () => {
+  const navigate = useNavigate();
   const [list] = useState([
     {
       id: 1,
@@ -21,6 +24,28 @@ const ManagementEvaluationPage = () => {
     },
   ]);
 
+  const handleExport = () => {
+    exportWordDocx(
+      "/template.docx",
+      {
+        name: "test",
+        indexList: [
+          {
+            indexIndex: 1,
+            indexTitle: "title",
+            indexDescription: "description",
+            indexValue: 10,
+            indexValue2: 20,
+            indexDepartment: "department",
+            indexScore: 30,
+            indexAdvice: "no advice",
+          },
+        ],
+      },
+      "test"
+    );
+  };
+
   return (
     <PageContainer title="考核评定">
       <div className={style.filter}>
@@ -32,7 +57,7 @@ const ManagementEvaluationPage = () => {
             <Button type="primary">查询</Button>
           </Form.Item>
           <Form.Item>
-            <Button type="primary" ghost>
+            <Button type="primary" ghost onClick={handleExport}>
               生成报告
             </Button>
           </Form.Item>
@@ -80,6 +105,7 @@ const ManagementEvaluationPage = () => {
                         导出
                       </Button>
                     }
+                    onEdit={() => navigate("/perfomance/form/edit")}
                   />
                 );
               },

+ 62 - 0
src/pages/Performance/Report/components/Container/index.tsx

@@ -92,6 +92,68 @@ export const Container: FC = () => {
         {
           label: "按部门",
           key: "1",
+          children: (
+            <ReactECharts
+              style={{ height: "100%" }}
+              option={{
+                tooltip: {
+                  trigger: "axis",
+                  axisPointer: {
+                    type: "shadow",
+                  },
+                },
+                legend: {},
+                grid: {
+                  left: "3%",
+                  right: "4%",
+                  bottom: "3%",
+                  containLabel: true,
+                },
+                xAxis: {
+                  type: "value",
+                  boundaryGap: [0, 0.01],
+                },
+                yAxis: {
+                  type: "category",
+                  data: [
+                    "Brazil",
+                    "Indonesia",
+                    "USA",
+                    "India",
+                    "China",
+                    "World",
+                  ],
+                },
+                series: [
+                  {
+                    name: "2011",
+                    type: "bar",
+                    data: [18203, 23489, 29034, 104970, 131744, 630230],
+                    markLine: {
+                      symbol: "none",
+                      silent: true,
+                      label: {
+                        position: "end",
+                        color: "red",
+                        fontSize: 14,
+                        formatter: "预警值 700",
+                      },
+                      data: [
+                        {
+                          silent: true,
+                          lineStyle: {
+                            type: "solid",
+                            color: "red",
+                          },
+                          yAxis: 30000,
+                        },
+                      ],
+                    },
+                  },
+                ],
+              }}
+            />
+          ),
         },
       ]}
     />

+ 13 - 3
src/router/index.tsx

@@ -85,13 +85,13 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             meta: {
               custom: true,
             },
-            path: "/management/index/detail",
+            path: "/management/index/detail/:id",
             title: "考核详情",
             Component: React.lazy(() => import("../pages/AssessmentDetail")),
           },
           {
             hide: true,
-            path: "/management/index/detail/index",
+            path: "/management/index/detail/:id/index",
             title: "考核指标详情",
             Component: React.lazy(
               () => import("../pages/AssessmentDetail/IndexDetail")
@@ -163,7 +163,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
     ],
   },
   {
-    path: "perfomance",
+    path: "/perfomance",
     title: "绩效分析",
     redirect: "/perfomance/report",
     icon: <Icon component={PerfomanceIcon} />,
@@ -177,6 +177,16 @@ export const DEFAULT_MENU: DageRouteItem[] = [
         path: "/perfomance/form",
         title: "考核报告",
         Component: React.lazy(() => import("../pages/Performance/Form")),
+        children: [
+          {
+            hide: true,
+            path: "/perfomance/form/edit",
+            title: "编辑报告",
+            Component: React.lazy(
+              () => import("../pages/Performance/Form/Edit")
+            ),
+          },
+        ],
       },
     ],
   },

+ 57 - 0
src/utils/exportWordDocx.ts

@@ -0,0 +1,57 @@
+import JSZipUtils from "jszip-utils";
+import docxtemplater from "docxtemplater";
+import { saveAs } from "file-saver";
+import PizZip from "pizzip";
+
+export const exportWordDocx = (
+  url: string,
+  docxData: Record<string, any>,
+  fileName: string
+) => {
+  JSZipUtils.getBinaryContent(url, function (error: unknown, content: any) {
+    // 抛出异常
+    if (error) {
+      throw error;
+    }
+
+    // 创建一个PizZip实例,内容为模板的内容
+    let zip = new PizZip(content);
+    // 创建并加载docxtemplater实例对象
+    let doc = new docxtemplater().loadZip(zip);
+    // 去除未定义值所显示的undefined
+    doc.setOptions({
+      nullGetter: function () {
+        return "";
+      },
+    }); // 设置角度解析器Could not find a declaration file for module 'jszip-utils'.
+    // 设置模板变量的值,对象的键需要和模板上的变量名一致,值就是你要放在模板上的值
+
+    doc.setData({
+      ...docxData,
+    });
+
+    try {
+      // 用模板变量的值替换所有模板变量
+      doc.render();
+    } catch (error: any) {
+      // 抛出异常
+      let e = {
+        message: error.message,
+        name: error.name,
+        stack: error.stack,
+        properties: error.properties,
+      };
+      console.log(JSON.stringify({ error: e }));
+      throw error;
+    }
+
+    // 生成一个代表docxtemplater对象的zip文件(不是一个真实的文件,而是在内存中的表示)
+    let out = doc.getZip().generate({
+      type: "blob",
+      mimeType:
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+    });
+    // 将目标文件对象保存为目标类型的文件,并命名
+    saveAs(out, fileName);
+  });
+};

+ 3 - 0
src/utils/index.ts

@@ -26,3 +26,6 @@ export const getSelectedNodes = (selectedKeys: Key[], treeData: any[]) => {
   findNodes(selectedKeys, treeData);
   return selectedNodes;
 };
+
+export const getImgFullPath = (path: string) =>
+  `${process.env.REACT_APP_API_URL}${process.env.REACT_APP_IMG_PUBLIC}${path}`;