chenlei пре 4 месеци
родитељ
комит
e6e00a7fb6

+ 1 - 0
package.json

@@ -40,6 +40,7 @@
     "file-loader": "^6.2.0",
     "file-saver": "^2.0.5",
     "fs-extra": "^10.0.0",
+    "html-docx-js": "^0.3.1",
     "html-webpack-plugin": "^5.5.0",
     "identity-obj-proxy": "^3.0.0",
     "jest": "^27.4.3",

Разлика између датотеке није приказан због своје велике величине
+ 7689 - 9085
pnpm-lock.yaml


BIN
public/template.docx


+ 1 - 0
src/api/index.ts

@@ -17,3 +17,4 @@ export * from "./log";
 export * from "./user";
 export * from "./assessment";
 export * from "./management";
+export * from "./performance";

+ 56 - 3
src/api/performance.ts

@@ -1,5 +1,58 @@
-import { requestByGet } from "@dage/service";
+import {
+  ASS_INDEX_TYPE,
+  AssIndexTreeItemType,
+  IManageIndexDetail,
+  IPerformanceRank,
+  IRepoDetail,
+  IRepoListParams,
+} from "@/types";
+import { requestByGet, requestByPost } from "@dage/service";
 
-export const getPerformanceDetailApi = () => {
-  return requestByGet(``);
+// 指标分析-指标列表
+export const getPerNormListApi = (type: ASS_INDEX_TYPE) => {
+  return requestByGet<AssIndexTreeItemType[]>(
+    `/api/cms/normAnalyse/getList/${type}`
+  );
+};
+
+export const getPerRankApi = (normId: number, type?: "dept") => {
+  return requestByGet<IPerformanceRank[]>(
+    `/api/cms/normAnalyse/rank/${normId}`,
+    {
+      type,
+    }
+  );
+};
+
+// 考核报告列表
+export const getReportListApi = (params: IRepoListParams) => {
+  return requestByPost("/api/cms/report/pageList", params);
+};
+
+// 考核单列表
+export const getReportAssessListApi = (searchKey?: string) => {
+  return requestByGet<IManageIndexDetail[]>("/api/cms/report/assessList", {
+    searchKey,
+  });
+};
+
+// 生成报告
+export const createReportApi = (assessId: number | string) => {
+  return requestByGet<IRepoDetail>(`/api/cms/report/create/${assessId}`);
+};
+
+export const saveReportApi = (params: {
+  id?: number;
+  name: string;
+  rtf: string;
+}) => {
+  return requestByPost("/api/cms/report/save", params);
+};
+
+export const getReportApi = (id: number | string) => {
+  return requestByGet(`/api/cms/report/detail/${id}`);
+};
+
+export const deleteReportApi = (id: number | string) => {
+  return requestByGet(`/api/cms/report/remove/${id}`);
 };

+ 22 - 1
src/api/user.ts

@@ -1,10 +1,31 @@
-import { GetUserListParams, SaveUserType } from "@/types";
+import {
+  GetUserListParams,
+  IRoleItem,
+  PermItemType,
+  SaveRoleParams,
+  SaveUserType,
+} from "@/types";
 import { requestByGet, requestByPost } from "@dage/service";
 
 export const userApi = {
   getList(params: GetUserListParams) {
     return requestByPost("/api/sys/user/list", params);
   },
+  getRoleList() {
+    return requestByGet<IRoleItem[]>("/api/sys/user/getRole");
+  },
+  getPermTree() {
+    return requestByGet<PermItemType[]>("/api/sys/user/perm/getTree");
+  },
+  saveRole(params: SaveRoleParams) {
+    return requestByPost("/api/sys/role/save", params);
+  },
+  deleteRole(id: number | string) {
+    return requestByGet(`/api/sys/role/remove/${id}`);
+  },
+  getRole(id: number | string) {
+    return requestByGet(`/api/sys/role/detail/${id}`);
+  },
   handleType(id: number, isEnabled: number) {
     return requestByGet(`/api/sys/user/editStatus/${id}/${isEnabled}`);
   },

+ 5 - 3
src/pages/Assessment/Index/components/Sidebar/index.tsx

@@ -18,6 +18,7 @@ export interface SidebarProps {
   checkable?: boolean;
   type: ASS_INDEX_TYPE;
   currentId: null | number;
+  treeApi?: () => Promise<AssIndexTreeItemType[]>;
   setCurrentId: (id: number | null) => void;
 }
 
@@ -25,6 +26,7 @@ export const Sidebar: FC<SidebarProps> = ({
   checkable,
   type,
   currentId,
+  treeApi,
   setCurrentId,
 }) => {
   const navigate = useNavigate();
@@ -50,7 +52,7 @@ export const Sidebar: FC<SidebarProps> = ({
   const getAssIndexTree = async () => {
     try {
       setLoading(true);
-      const data = await getAssIndexTreeApi(type);
+      const data = treeApi ? await treeApi() : await getAssIndexTreeApi(type);
       if (data.length) {
         // 防止再次选择
         data[0].disabled = true;
@@ -110,8 +112,8 @@ export const Sidebar: FC<SidebarProps> = ({
           treeData={treeData}
           selectedKeys={checkedKeys}
           onAdd={checkable ? undefined : handleAddNode}
-          onEdit={handleEditNode}
-          onDelete={handleDeleteNode}
+          onEdit={checkable ? undefined : handleEditNode}
+          onDelete={checkable ? undefined : handleDeleteNode}
           onSelect={(keys, { selectedNodes }) => {
             const checkedId = keys[keys.length - 1] as number;
 

+ 2 - 1
src/pages/Layout/components/Header/index.tsx

@@ -3,7 +3,7 @@ import style from "./index.module.scss";
 import { App, Avatar, Breadcrumb, Button, Popover } from "antd";
 import { Header } from "antd/es/layout/layout";
 import { useSelector } from "react-redux";
-import { RootState } from "@/store";
+import store, { RootState } from "@/store";
 import { ResetPassword } from "./components/ResetPassword";
 import { getImgFullPath, logout } from "@/utils";
 import { DageRouteItem } from "@/router";
@@ -68,6 +68,7 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({ menuData }) => {
       content: "确定退出吗?",
       async onOk() {
         await logout();
+        store.dispatch({ type: "setUserInfo", payload: null });
       },
     });
   }, [modal]);

+ 137 - 76
src/pages/Layout/index.tsx

@@ -17,6 +17,9 @@ import LogoImage from "@/assets/images/logo.png";
 import { DEFAULT_ADMIN_MENU, DEFAULT_MENU, DageRouteItem } from "@/router";
 import "./index.scss";
 import { findRouteByPath } from "./utils";
+import { userApi } from "@/api";
+import { getAuthorizedIds } from "@/utils";
+import { YES_OR_NO } from "@/types";
 
 const NotFound = React.lazy(() => import("@/components/NotFound"));
 
@@ -27,14 +30,35 @@ export default function CustomLayout() {
     (state) => state.base
   );
   const [curMeta, setCurMeta] = useState<null | DageRouteItem["meta"]>(null);
-  const menuList = useMemo<DageRouteItem[]>(() => {
-    return baseStore.userInfo?.user.isAdmin
-      ? [...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU]
-      : [...DEFAULT_MENU];
-  }, [baseStore.userInfo]);
+  const [menuList, setMenuList] = useState<DageRouteItem[]>([]);
 
   // 自定义 Content 样式
   const isCustomStyle = curMeta && curMeta.custom;
+  const [menuLoading, setMenuLoading] = useState(false);
+
+  const getPermissions = async () => {
+    try {
+      setMenuLoading(true);
+      const data = await userApi.getRole(baseStore.userInfo!.user.roleId);
+      const permissonIds = getAuthorizedIds(data.permission);
+      const menus = filterAuthorizedRoutes(DEFAULT_MENU, permissonIds);
+      setMenuList(menus);
+      const target = getFirstPath(menus);
+      target && navigate(target);
+    } finally {
+      setMenuLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!baseStore.userInfo) return;
+
+    if (baseStore.userInfo?.user.isAdmin === YES_OR_NO.YES) {
+      setMenuList([...DEFAULT_MENU, ...DEFAULT_ADMIN_MENU]);
+    } else {
+      getPermissions();
+    }
+  }, [baseStore]);
 
   useEffect(() => {
     if (!hasToken()) {
@@ -44,7 +68,7 @@ export default function CustomLayout() {
     } else {
       store.dispatch({ type: "setUserInfo", payload: getTokenInfo() });
     }
-  }, [navigate]);
+  }, []);
 
   useEffect(() => {
     const route = findRouteByPath(menuList, location.pathname);
@@ -53,80 +77,86 @@ export default function CustomLayout() {
 
   return (
     <App>
-      <Layout hasSider className="layout">
-        {/* 菜单 */}
-        <Layout.Sider
-          width={220}
-          style={{
-            position: "fixed",
-            top: 0,
-            left: 0,
-            bottom: 0,
-            background: "#242424",
-          }}
-        >
-          <div className="logo">
-            <img draggable="false" alt="logo" src={LogoImage} />
-          </div>
-
-          <LayoutMenu
-            className="layout-menu"
-            theme="dark"
-            inlineIndent={20}
-            menuData={menuList}
-          />
-        </Layout.Sider>
-
-        <Layout style={{ marginLeft: 220 }}>
-          {/* 头部 */}
-          <LayoutHeader menuData={menuList} />
-
-          {/* 主体 */}
-          <Content
+      {menuLoading ? (
+        <DageLoading />
+      ) : (
+        <Layout hasSider className="layout">
+          {/* 菜单 */}
+          <Layout.Sider
+            width={220}
             style={{
-              margin: "15px",
-              overflow: "auto",
-              position: "relative",
-              ...(isCustomStyle
-                ? {}
-                : {
-                    background: "#ffffff",
-                    padding: "0 25px 25px",
-                    borderRadius: 4,
-                  }),
+              position: "fixed",
+              top: 0,
+              left: 0,
+              bottom: 0,
+              background: "#242424",
             }}
           >
-            <Suspense fallback={<DageLoading />}>
-              {menuList.length && (
-                <Routes>
-                  <Route
-                    path="/"
-                    element={
-                      <Navigate to={menuList[0].redirect || menuList[0].path} />
-                    }
-                  />
-                  {renderRoutes(menuList).map((menu) =>
-                    menu.redirect ? (
-                      <Route
-                        key={menu.path}
-                        path={menu.path}
-                        element={<Navigate replace to={menu.redirect} />}
-                      />
-                    ) : (
-                      <Route
-                        key={menu.path}
-                        path={menu.path}
-                        Component={menu.Component}
-                      />
-                    )
-                  )}
-                  <Route path="*" Component={NotFound} />
-                </Routes>
-              )}
-            </Suspense>
-          </Content>
+            <div className="logo">
+              <img draggable="false" alt="logo" src={LogoImage} />
+            </div>
+
+            <LayoutMenu
+              className="layout-menu"
+              theme="dark"
+              inlineIndent={20}
+              menuData={menuList}
+            />
+          </Layout.Sider>
+
+          <Layout style={{ marginLeft: 220 }}>
+            {/* 头部 */}
+            <LayoutHeader menuData={menuList} />
+
+            {/* 主体 */}
+            <Content
+              style={{
+                margin: "15px",
+                overflow: "auto",
+                position: "relative",
+                ...(isCustomStyle
+                  ? {}
+                  : {
+                      background: "#ffffff",
+                      padding: "0 25px 25px",
+                      borderRadius: 4,
+                    }),
+              }}
+            >
+              <Suspense fallback={<DageLoading />}>
+                {menuList.length && (
+                  <Routes>
+                    <Route
+                      path="/"
+                      element={
+                        <Navigate
+                          to={menuList[0].redirect || menuList[0].path}
+                        />
+                      }
+                    />
+                    {renderRoutes(menuList).map((menu) =>
+                      menu.redirect ? (
+                        <Route
+                          key={menu.path}
+                          path={menu.path}
+                          element={<Navigate replace to={menu.redirect} />}
+                        />
+                      ) : (
+                        <Route
+                          key={menu.path}
+                          path={menu.path}
+                          Component={menu.Component}
+                        />
+                      )
+                    )}
+                    <Route path="*" Component={NotFound} />
+                  </Routes>
+                )}
+              </Suspense>
+            </Content>
+          </Layout>
         </Layout>
-      </Layout>
+      )}
     </App>
   );
 }
@@ -148,3 +178,34 @@ function renderRoutes(routes: DageRouteItem[]) {
 
   return deep(routes);
 }
+
+function filterAuthorizedRoutes(routes: DageRouteItem[], permIds: number[]) {
+  const filterRoutes = (routeList: DageRouteItem[]): DageRouteItem[] => {
+    return routeList
+      .map((route) => {
+        const newRoute = { ...route };
+
+        if (newRoute.children) {
+          newRoute.children = filterRoutes(newRoute.children);
+        }
+
+        const shouldKeep = permIds.includes(route.mapId || -1);
+
+        return shouldKeep ? newRoute : null;
+      })
+      .filter(Boolean) as DageRouteItem[];
+  };
+
+  return filterRoutes(routes);
+}
+
+function getFirstPath(menus: DageRouteItem[]): string | null {
+  if (!menus.length) return null;
+
+  const item = menus[0];
+  if (item.children) {
+    if (item.children.every((i) => i.hide)) return item.path;
+    return getFirstPath(item.children);
+  }
+  return item.path;
+}

+ 1 - 0
src/pages/Login/index.tsx

@@ -67,6 +67,7 @@ export default function Login() {
               placeholder="请输入用户名"
               maxLength={15}
               variant="filled"
+              autoComplete="username"
             />
           </Form.Item>
           <Form.Item

+ 230 - 6
src/pages/Performance/Form/Edit/index.tsx

@@ -1,17 +1,241 @@
+import { createReportApi, getReportApi, saveReportApi } from "@/api";
 import { FormPageFooter, PageContainer } from "@/components";
+import { DageEditor, DageLoading } from "@dage/pc-components";
 import { Form, Input } from "antd";
-import { FC } from "react";
+import { FC, useEffect, useRef, useState } from "react";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
 
 const PerformanceFormEditPage: FC = () => {
+  const location = useLocation();
+  const params = useParams();
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState(false);
+  const detail = useRef<any>(null);
+  const [form] = Form.useForm();
+  const isEdit = location.pathname.indexOf("edit") > -1;
+
+  const getAssDetail = async () => {
+    try {
+      setLoading(true);
+      const data = await createReportApi(params.id as string);
+      const jsonAdd = data.reAssess.jsonAdd
+        ? JSON.parse(data.reAssess.jsonAdd)
+        : null;
+      const jsonSub = data.reAssess.jsonSub
+        ? JSON.parse(data.reAssess.jsonSub)
+        : null;
+      form.setFieldsValue({
+        name: `${data.reAssess.name}考核报告(${data.reAssess.dateStart}~${data.reAssess.dateEnd})`,
+        rtf: `<h1><strong>${data.reAssess.name}考核报告</strong></h1>
+        <h3 style="text-align: left;">基本信息</h3>
+        <ul>
+            <li style="text-align: left;"><strong>考核名称</strong>:${
+              data.reAssess.name
+            }</li>
+            <li style="text-align: left;"><strong>考核周期</strong>:${
+              data.reAssess.dateStart
+            } - ${data.reAssess.dateEnd}</li>
+            <li style="text-align: left;"><strong>考核说明</strong>:${
+              data.reAssess.remark
+            }</li>
+            <li style="text-align: left;"><strong>发布状态</strong>:${
+              data.reAssess.publishStatus
+            }</li>
+            <li style="text-align: left;"><strong>编辑时间</strong>:${
+              data.createTime
+            }</li>
+            <li style="text-align: left;"><strong>编辑人</strong>:${
+              data.creatorName
+            }</li>
+            <li style="text-align: left;"><strong>总分值</strong>:${
+              data.reAssess.score
+            }</li>
+            <li style="text-align: left;"><strong>评定得分</strong>:${
+              data.reAssess.opinionScore
+            }</li>
+            <li style="text-align: left;"><strong>评定意见</strong>:${
+              data.reAssess.opinion
+            }</li>
+        </ul>
+        <hr />
+        <h3 style="text-align: left;">考核角色</h3>
+        <ul>
+            ${data.reDept.map(
+              (dept, index) => `
+            <li style="text-align: left;"><strong>责任部门${
+              index + 1
+            }</strong>:</li>
+            <ul>
+                <li style="text-align: left;"><strong>部门名称</strong>:${
+                  dept.name
+                }</li>
+                <li style="text-align: left;"><strong>博物馆级别</strong>:${
+                  dept.levelMuseum
+                }</li>
+                <li style="text-align: left;"><strong>主管</strong>:${
+                  dept.leaderName
+                }</li>
+                <li style="text-align: left;"><strong>成员</strong>:${
+                  dept.crewName
+                }</li>
+            </ul>
+            `
+            )}
+            <li style="text-align: left;"><strong>评定组成员</strong>:</li>
+            <ul>
+                <li style="text-align: left;"><strong>评定组</strong>:${
+                  data.groupName
+                }</li>
+                <li style="text-align: left;"><strong>评定人</strong>:${
+                  data.groupUser
+                }</li>
+            </ul>
+        </ul>
+        <h3 style="text-align: left;">指标清单</h3>
+        <table style="width: 100%;">
+            <tbody>
+                <tr>
+                    <td colSpan="1" rowSpan="1" width="100">序号</td>
+                    <td colSpan="1" rowSpan="1" width="100">标题</td>
+                    <td colSpan="1" rowSpan="1" width="100">说明</td>
+                    <td colSpan="1" rowSpan="1" width="100">分值</td>
+                    <td colSpan="1" rowSpan="1" width="100">打分点</td>
+                    <td colSpan="1" rowSpan="1" width="100">责任部门</td>
+                    <td colSpan="1" rowSpan="1" width="100">评定得分</td>
+                    <td colSpan="1" rowSpan="1" width="100">评定意见</td>
+                </tr>
+                ${data.reNorm.map(
+                  (norm, index) => `
+                <tr>
+                    <td colSpan="1" rowSpan="1" width="100">${index + 1}</td>
+                    <td colSpan="1" rowSpan="1" width="100">${norm.name}</td>
+                    <td colSpan="1" rowSpan="1" width="100">${norm.remark}</td>
+                    <td colSpan="1" rowSpan="1" width="100">${norm.num}</td>
+                    <td colSpan="1" rowSpan="1" width="100">${
+                      norm.isPoint === "1" ? "是" : "否"
+                    }</td>
+                    <td colSpan="1" rowSpan="1" width="100">${
+                      norm.deptName
+                    }</td>
+                    <td colSpan="1" rowSpan="1" width="100">${
+                      norm.opinionScore
+                    }</td>
+                    <td colSpan="1" rowSpan="1" width="100">${norm.opinion}</td>
+                </tr>  
+                `
+                )}
+            </tbody>
+        </table>
+        <h3 style="text-align: left;">预警指标</h3>
+        <ul>
+            <li style="text-align: left;"><strong>安全管理</strong>:该指标得分低于设定的预警阈值,需立即采取改进措施。</li>
+            <li style="text-align: left;"><strong>说明</strong>:该指标反映了博物馆在安全管理方面存在的潜在风险,建议加强培训与演练。</li>
+        </ul>
+        ${
+          jsonAdd
+            ? `
+        <h3 style="text-align: left;"><strong>加分项</strong></h3>
+        <table style="width: 100%;">
+            <tbody>
+                <tr>
+                    <td colSpan="1" rowSpan="1" width="179">名称</td>
+                    <td colSpan="1" rowSpan="1" width="100">指标分值</td>
+                    <td colSpan="1" rowSpan="1" width="239">评定得分</td>
+                </tr>
+                ${jsonAdd.map(
+                  (add: any) => `
+                <tr>
+                    <td colSpan="1" rowSpan="1" width="179">${add.name}</td>
+                    <td colSpan="1" rowSpan="1" width="100">${add.num}</td>
+                    <td colSpan="1" rowSpan="1" width="239">${add.score}</td>
+                </tr>
+                `
+                )}
+            </tbody>
+        </table>  
+        `
+            : ``
+        }
+        ${
+          jsonSub
+            ? `
+        <h3 style="text-align: left;"><strong>减分项</strong></h3>
+        <table style="width: 100%;">
+            <tbody>
+                <tr>
+                    <td colSpan="1" rowSpan="1" width="179">名称</td>
+                    <td colSpan="1" rowSpan="1" width="100">可扣分值</td>
+                    <td colSpan="1" rowSpan="1" width="239">评定得分</td>
+                </tr>
+                ${jsonSub.map(
+                  (add: any) => `
+                <tr>
+                    <td colSpan="1" rowSpan="1" width="179">${add.name}</td>
+                    <td colSpan="1" rowSpan="1" width="100">${add.num}</td>
+                    <td colSpan="1" rowSpan="1" width="239">${add.score}</td>
+                </tr>
+                `
+                )}
+            </tbody>
+        </table>
+        `
+            : ""
+        }
+        `,
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const getDetail = async () => {
+    try {
+      setLoading(true);
+      const data = await getReportApi(params.id as string);
+      form.setFieldsValue({
+        name: data.name,
+        rtf: data.rtf,
+      });
+      detail.current = data;
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSubmit = async () => {
+    await saveReportApi({
+      ...form.getFieldsValue(),
+      id: isEdit ? detail.current?.id : undefined,
+    });
+    navigate(-1);
+  };
+
+  useEffect(() => {
+    isEdit ? getDetail() : getAssDetail();
+  }, []);
+
   return (
-    <PageContainer title="编辑报告">
-      <Form labelCol={{ span: 3 }}>
-        <Form.Item label="报告标题" required>
-          <Input className="w450" placeholder="请输入" />
+    <PageContainer title={isEdit ? "编辑报告" : "新增报告"}>
+      {loading && <DageLoading />}
+
+      <Form form={form} labelCol={{ span: 3 }}>
+        <Form.Item
+          label="报告标题"
+          required
+          name="name"
+          rules={[{ required: true, message: "请输入报告标题" }]}
+        >
+          <Input className="w450" placeholder="请输入" allowClear />
+        </Form.Item>
+        <Form.Item required label="报告正文" name="rtf">
+          <DageEditor
+            action="/api/cms/assess/file/upload"
+            excludeKeys={["group-image", "group-video", "emotion"]}
+          />
         </Form.Item>
       </Form>
 
-      <FormPageFooter />
+      <FormPageFooter onSubmit={handleSubmit} onCancel={() => navigate(-1)} />
     </PageContainer>
   );
 };

+ 155 - 0
src/pages/Performance/Form/components/ChooseFormModal/index.tsx

@@ -0,0 +1,155 @@
+import { FC, useEffect, useMemo, useRef, useState } from "react";
+import { Form, Input, Modal, ModalProps, Table } from "antd";
+import style from "@/components/AddIndexModal/index.module.scss";
+import { getReportAssessListApi } from "@/api";
+import { debounce } from "lodash";
+import { IManageIndexDetail } from "@/types";
+import { ASSESSMENT_TYPE_OPTIONS, PUBLISH_STATUS_MAP } from "@/constants";
+import useApp from "antd/es/app/useApp";
+
+export interface ChooseFormModalProps extends Omit<ModalProps, "onOk"> {
+  onCancel?: () => void;
+  onOk?: (id: number) => void;
+}
+
+export const ChooseFormModal: FC<ChooseFormModalProps> = ({
+  open,
+  onOk,
+  onCancel,
+  ...rest
+}) => {
+  const { message } = useApp();
+  const [submitLoading, setSubmitLoading] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [params, setParams] = useState({
+    searchKey: "",
+  });
+  const [list, setList] = useState<IManageIndexDetail[]>([]);
+  const checkedId = useRef<null | number>(null);
+
+  const handleCancel = () => {
+    onCancel?.();
+  };
+
+  const handleConfirm = async () => {
+    if (!checkedId.current) {
+      message.warning("请选择考核单");
+      return;
+    }
+
+    try {
+      setSubmitLoading(true);
+      onOk?.(checkedId.current);
+    } finally {
+      setSubmitLoading(false);
+    }
+  };
+
+  const getList = async () => {
+    try {
+      setLoading(true);
+      const data = await getReportAssessListApi(params.searchKey);
+      setList(data);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const debounceSearch = useMemo(
+    () =>
+      debounce((changedVal: unknown, vals: any) => {
+        setParams({ ...params, ...vals });
+      }, 500),
+    [params]
+  );
+
+  useEffect(() => {
+    if (!open) return;
+    getList();
+  }, [open, params]);
+
+  return (
+    <Modal
+      className={style.modal}
+      title="请选择考核单"
+      okText="提交"
+      cancelText="取消"
+      open={open}
+      width={1000}
+      okButtonProps={{
+        disabled: submitLoading,
+      }}
+      forceRender
+      maskClosable={false}
+      onOk={handleConfirm}
+      onCancel={handleCancel}
+      {...rest}
+    >
+      <Form
+        layout="inline"
+        style={{ marginBottom: 20 }}
+        onValuesChange={debounceSearch}
+      >
+        <Form.Item label="搜索" name="searchKey">
+          <Input placeholder="请输入考核标题" className="w220" allowClear />
+        </Form.Item>
+      </Form>
+
+      <Table
+        loading={loading}
+        dataSource={list}
+        rowKey="id"
+        rowSelection={{
+          type: "radio",
+          onChange(selectedRowKeys) {
+            checkedId.current = selectedRowKeys[0] as number;
+          },
+        }}
+        pagination={false}
+        columns={[
+          {
+            title: "名称",
+            dataIndex: "name",
+            align: "center",
+          },
+          {
+            title: "类别",
+            align: "center",
+            minWidth: 100,
+            render: (item: IManageIndexDetail) => {
+              return ASSESSMENT_TYPE_OPTIONS.find((i) => i.value === item.type)
+                ?.label;
+            },
+          },
+          {
+            title: "说明",
+            dataIndex: "remark",
+            align: "center",
+            minWidth: 100,
+            ellipsis: true,
+          },
+          {
+            title: "考核周期",
+            align: "center",
+            minWidth: 100,
+            render: (item: IManageIndexDetail) => {
+              return `${item.dateStart}-${item.dateEnd}`;
+            },
+          },
+          {
+            title: "发布状态",
+            align: "center",
+            minWidth: 100,
+            render: (item: IManageIndexDetail) => {
+              return (
+                <p style={{ color: PUBLISH_STATUS_MAP[item.status].color }}>
+                  {PUBLISH_STATUS_MAP[item.status].label}
+                </p>
+              );
+            },
+          },
+        ]}
+      />
+    </Modal>
+  );
+};

+ 101 - 48
src/pages/Performance/Form/index.tsx

@@ -1,63 +1,97 @@
 import classNames from "classnames";
-import { useState } from "react";
+import { useCallback, useEffect, useMemo, 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";
+import { deleteReportApi, getReportApi, getReportListApi } from "@/api";
+import { IRepoListParams } from "@/types";
+import { debounce } from "lodash";
+import { ChooseFormModal } from "./components/ChooseFormModal";
+// @ts-ignore
+import htmlDocx from "html-docx-js/dist/html-docx";
+import { saveAs } from "file-saver";
+
+const DEFAULT_PARAMS: IRepoListParams = {
+  searchKey: "",
+  pageNum: 1,
+  pageSize: 20,
+};
 
 const ManagementEvaluationPage = () => {
   const navigate = useNavigate();
-  const [list] = useState([
-    {
-      id: 1,
-      name: "模板名称",
-      a: "定级评估",
-      description: "模板说明",
-      b: "",
-      c: "",
-      d: "",
-      e: "",
-      f: "2024-09-24 16:50:30",
-      g: "钱韵澄",
+  const [loading, setLoading] = useState(false);
+  const [params, setParams] = useState<IRepoListParams>({
+    ...DEFAULT_PARAMS,
+  });
+  const [total, setTotal] = useState(0);
+  const [list, setList] = useState([]);
+  const [chooseFormVisible, setChooseFormVisible] = useState(false);
+
+  const getList = useCallback(async () => {
+    setLoading(true);
+    try {
+      const data = await getReportListApi(params);
+      setList(data.records);
+      setTotal(data.total);
+    } finally {
+      setLoading(false);
+    }
+  }, [params]);
+
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setParams({ ...params, pageNum, pageSize });
     },
-  ]);
+    [params]
+  );
+
+  const debounceSearch = useMemo(
+    () =>
+      debounce((changedVal: unknown, vals: any) => {
+        setParams({ ...params, ...vals });
+      }, 500),
+    [params]
+  );
+
+  useEffect(() => {
+    getList();
+  }, []);
+
+  const handleExport = async (id: number) => {
+    const data = await getReportApi(id);
+    const docx = htmlDocx.asBlob(data.rtf);
+    saveAs(docx, `${data.name}.docx`);
+  };
 
-  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"
-    );
+  const handleDelete = async (id: number) => {
+    await deleteReportApi(id);
+    getList();
   };
 
   return (
     <PageContainer title="考核评定">
       <div className={style.filter}>
-        <Form layout="inline" className="inline-form">
-          <Form.Item label="搜索">
+        <Form
+          layout="inline"
+          className="inline-form"
+          onValuesChange={debounceSearch}
+        >
+          <Form.Item label="搜索" name="searchKey">
             <Input placeholder="请输入报告标题" className="w160" />
           </Form.Item>
           <Form.Item>
-            <Button type="primary">查询</Button>
+            <Button type="primary" onClick={getList}>
+              查询
+            </Button>
           </Form.Item>
           <Form.Item>
-            <Button type="primary" ghost onClick={handleExport}>
+            <Button
+              type="primary"
+              ghost
+              onClick={() => setChooseFormVisible(true)}
+            >
               生成报告
             </Button>
           </Form.Item>
@@ -66,6 +100,7 @@ const ManagementEvaluationPage = () => {
 
       <div className={style.table}>
         <Table
+          loading={loading}
           className={classNames("cus-table large")}
           dataSource={list}
           rowKey="id"
@@ -74,45 +109,63 @@ const ManagementEvaluationPage = () => {
             {
               title: "报告标题",
               dataIndex: "name",
-              key: "name",
               align: "center",
               minWidth: 100,
             },
             {
               title: "编辑时间",
-              dataIndex: "a",
-              key: "a",
+              dataIndex: "updateTime",
               align: "center",
               minWidth: 100,
             },
             {
               title: "编辑人",
-              dataIndex: "description",
-              key: "description",
+              dataIndex: "creatorName",
               align: "center",
               minWidth: 100,
             },
             {
               title: "操作",
-              key: "h",
               align: "center",
               fixed: "right",
-              render: (item: (typeof list)[0]) => {
+              render: (item: any) => {
                 return (
                   <DageTableActions
                     renderBefore={
-                      <Button size="small" type="link">
+                      <Button
+                        size="small"
+                        type="link"
+                        onClick={handleExport.bind(undefined, item.id)}
+                      >
                         导出
                       </Button>
                     }
-                    onEdit={() => navigate("/perfomance/form/edit")}
+                    onDelete={handleDelete.bind(undefined, item.id)}
+                    onEdit={() => navigate("/perfomance/form/edit/" + item.id)}
                   />
                 );
               },
             },
           ]}
+          pagination={{
+            showQuickJumper: true,
+            position: ["bottomCenter"],
+            showSizeChanger: true,
+            current: params.pageNum,
+            pageSize: params.pageSize,
+            total,
+            onChange: paginationChange(),
+          }}
         />
       </div>
+
+      <ChooseFormModal
+        open={chooseFormVisible}
+        onOk={(id) => {
+          navigate(`/perfomance/form/create/${id}`);
+        }}
+        onCancel={() => setChooseFormVisible(false)}
+      />
     </PageContainer>
   );
 };

+ 112 - 42
src/pages/Performance/Report/components/Container/index.tsx

@@ -1,11 +1,72 @@
 import { Tabs } from "antd";
-import { FC } from "react";
+import { FC, useEffect, useMemo, useRef, useState } from "react";
 import ReactECharts from "echarts-for-react";
 import style from "./index.module.scss";
+import { IPerformanceRank } from "@/types";
+import { getPerRankApi } from "@/api";
+import { SYMBOL_OPTIONS } from "@/pages/Assessment/Index/constants";
+
+export interface ContainerProps {
+  currentId: number | null;
+}
+
+export const Container: FC<ContainerProps> = ({ currentId }) => {
+  const [loading, setLoading] = useState(false);
+  const [list, setList] = useState<IPerformanceRank[]>([]);
+  const activeTab = useRef("0");
+
+  const warningLine = useMemo(() => {
+    if (!list.length || !list[0].jsonWarn) return [];
+
+    const data = JSON.parse(list[0].jsonWarn);
+    const temp = [
+      {
+        ...SYMBOL_OPTIONS.find((i) => i.value === data.symbol),
+        silent: true,
+        lineStyle: {
+          type: "solid",
+          color: "red",
+        },
+        yAxis: data.num,
+      },
+    ];
+
+    if (data.symbol2)
+      temp.push({
+        ...SYMBOL_OPTIONS.find((i) => i.value === data.symbol2),
+        silent: true,
+        lineStyle: {
+          type: "solid",
+          color: "red",
+        },
+        yAxis: data.num2,
+      });
+
+    return temp;
+  }, [list]);
+
+  const getRank = async () => {
+    if (!currentId) return;
+
+    try {
+      setLoading(true);
+      const data = await getPerRankApi(
+        currentId,
+        activeTab.current === "1" ? "dept" : undefined
+      );
+      setList(data);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getRank();
+  }, [currentId]);
 
-export const Container: FC = () => {
   return (
     <Tabs
+      defaultActiveKey={activeTab.current}
       className={style.tabs}
       items={[
         {
@@ -19,16 +80,23 @@ export const Container: FC = () => {
                   trigger: "axis",
                   axisPointer: {
                     type: "shadow",
-                    label: {
-                      show: true,
-                    },
+                  },
+                  formatter: (params: any) => {
+                    const item = params[0];
+                    return `
+                      <p>${item.name}</p>
+                      <p>${list[item.dataIndex].dateStart} - ${
+                      list[item.dataIndex].dateEnd
+                    }</p>
+                      <p>${item.seriesName}:${item.value}</p>
+                    `;
                   },
                 },
                 calculable: true,
                 xAxis: [
                   {
                     type: "category",
-                    data: ["a", "b", "c"],
+                    data: list.map((i) => i.assessName),
                   },
                 ],
                 yAxis: [
@@ -40,13 +108,11 @@ export const Container: FC = () => {
                 dataZoom: [
                   {
                     show: true,
-                    start: 50,
-                    end: 100,
+                    start: 0,
                   },
                   {
                     type: "inside",
                     start: 0,
-                    end: 100,
                   },
                   {
                     show: true,
@@ -62,7 +128,7 @@ export const Container: FC = () => {
                   {
                     name: "评定得分",
                     type: "bar",
-                    data: [20, 40, 50],
+                    data: list.map((i) => i.opinionScore || 0),
                     markLine: {
                       symbol: "none",
                       silent: true,
@@ -70,18 +136,11 @@ export const Container: FC = () => {
                         position: "end",
                         color: "red",
                         fontSize: 14,
-                        formatter: "预警值 700",
-                      },
-                      data: [
-                        {
-                          silent: true,
-                          lineStyle: {
-                            type: "solid",
-                            color: "red",
-                          },
-                          yAxis: 40,
+                        formatter: (params: any) => {
+                          return `预警值${params.data.label}${params.data.value}`;
                         },
-                      ],
+                      },
+                      data: warningLine,
                     },
                   },
                 ],
@@ -101,6 +160,16 @@ export const Container: FC = () => {
                   axisPointer: {
                     type: "shadow",
                   },
+                  formatter: (params: any) => {
+                    const item = params[0];
+                    return `
+                      <p>${list[item.dataIndex].assessName}</p>
+                      <p>${list[item.dataIndex].dateStart} - ${
+                      list[item.dataIndex].dateEnd
+                    }</p>
+                      <p>${item.seriesName}:${item.value}</p>
+                    `;
+                  },
                 },
                 legend: {},
                 grid: {
@@ -115,20 +184,18 @@ export const Container: FC = () => {
                 },
                 yAxis: {
                   type: "category",
-                  data: [
-                    "Brazil",
-                    "Indonesia",
-                    "USA",
-                    "India",
-                    "China",
-                    "World",
-                  ],
+                  data: list.map(
+                    (i) => `
+                      ${i.assessName}
+                      ${i.deptName}
+                    `
+                  ),
                 },
                 series: [
                   {
-                    name: "2011",
+                    name: "评定得分",
                     type: "bar",
-                    data: [18203, 23489, 29034, 104970, 131744, 630230],
+                    data: list.map((i) => i.opinionScore || 0),
                     markLine: {
                       symbol: "none",
                       silent: true,
@@ -136,18 +203,17 @@ export const Container: FC = () => {
                         position: "end",
                         color: "red",
                         fontSize: 14,
-                        formatter: "预警值 700",
-                      },
-                      data: [
-                        {
-                          silent: true,
-                          lineStyle: {
-                            type: "solid",
-                            color: "red",
-                          },
-                          yAxis: 30000,
+                        formatter: (params: any) => {
+                          return `预警值${params.data.label}${params.data.value}`;
                         },
-                      ],
+                      },
+                      data: warningLine.map((i) => {
+                        const { yAxis, ...rest } = i;
+                        return {
+                          xAxis: yAxis,
+                          ...rest,
+                        };
+                      }),
                     },
                   },
                 ],
@@ -156,6 +222,10 @@ export const Container: FC = () => {
           ),
         },
       ]}
+      onChange={(v) => {
+        activeTab.current = v;
+        getRank();
+      }}
     />
   );
 };

+ 44 - 8
src/pages/Performance/Report/index.tsx

@@ -3,26 +3,62 @@ import { PageContainer } from "@/components";
 import { Sidebar } from "@/pages/Assessment/Index/components/Sidebar";
 import { Container } from "./components/Container";
 import style from "@/pages/Assessment/Index/index.module.scss";
+import { useHashQuery } from "@/hooks";
+import { ASS_INDEX_TYPE } from "@/types";
+import { useState } from "react";
+import { getPerNormListApi } from "@/api";
+
+const TABS = [
+  {
+    value: ASS_INDEX_TYPE.FIXED,
+    label: "定级评估指标",
+  },
+  {
+    value: ASS_INDEX_TYPE.OPERATION,
+    label: "运行评估指标",
+  },
+];
 
 const ManagementReportPage = () => {
+  const [type, setType] = useHashQuery("tab", ASS_INDEX_TYPE.FIXED);
+  const [currentId, setCurrentId] = useState<number | null>(null);
+
+  const getTreeData = (_type?: ASS_INDEX_TYPE) => {
+    return getPerNormListApi(_type || type);
+  };
+
   return (
     <PageContainer
       title="指标分析"
       headerSlot={
         <div className={style.pageTools}>
-          <Button type="primary" size="large">
-            定级评估指标
-          </Button>
-          <Button type="primary" ghost size="large">
-            运行评估指标
-          </Button>
+          {TABS.map((tab) => (
+            <Button
+              key={tab.value}
+              type="primary"
+              ghost={tab.value === type}
+              size="large"
+              onClick={() => {
+                setType(tab.value);
+                getTreeData(tab.value);
+              }}
+            >
+              {tab.label}
+            </Button>
+          ))}
         </div>
       }
     >
       <div className={style.page}>
-        {/* <Sidebar checkable /> */}
+        <Sidebar
+          type={type}
+          currentId={currentId}
+          checkable
+          setCurrentId={setCurrentId}
+          treeApi={getTreeData}
+        />
 
-        <Container />
+        <Container currentId={currentId} />
       </div>
     </PageContainer>
   );

+ 159 - 0
src/pages/User/RoleEdit.tsx

@@ -0,0 +1,159 @@
+import { userApi } from "@/api";
+import { FormPageFooter, PageContainer } from "@/components";
+import { PermItemType } from "@/types";
+import { getAuthorizedIds } from "@/utils";
+import { DageLoading } from "@dage/pc-components";
+import { Form, Input, Tree } from "antd";
+import { FC, Key, useEffect, useMemo, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+const { TextArea } = Input;
+
+const getCheckedIds = (treeData: PermItemType[], checkedIds: number[]) => {
+  const fullyCheckedIds: number[] = [];
+  const halfCheckedIds: number[] = [];
+
+  const checkNode = (node: PermItemType): boolean => {
+    let allChildrenChecked = true;
+    let someChildrenChecked = false;
+
+    if (node.children && node.children.length > 0) {
+      for (const child of node.children) {
+        const isChildChecked = checkNode(child);
+        if (!isChildChecked) allChildrenChecked = false;
+        if (isChildChecked) someChildrenChecked = true;
+      }
+    }
+
+    const isNodeExplicitlyChecked = checkedIds.includes(node.id);
+
+    if (
+      isNodeExplicitlyChecked &&
+      (allChildrenChecked || node.children?.length === 0)
+    ) {
+      fullyCheckedIds.push(node.id);
+      return true;
+    }
+
+    if (
+      (someChildrenChecked && !isNodeExplicitlyChecked) ||
+      (isNodeExplicitlyChecked && !allChildrenChecked)
+    ) {
+      halfCheckedIds.push(node.id);
+      return true;
+    }
+
+    return isNodeExplicitlyChecked;
+  };
+
+  treeData.forEach((node) => checkNode(node));
+  return { fullyCheckedIds, halfCheckedIds };
+};
+
+const RoleEditPage: FC = () => {
+  const [form] = Form.useForm();
+  const params = useParams();
+  const navigate = useNavigate();
+  const isEdit = useMemo(() => location.href.indexOf("edit") > -1, [location]);
+  const [permTree, setPermTree] = useState<PermItemType[]>([]);
+  const [checkedKeys, setCheckedKeys] = useState<Key[]>([]);
+  const [halfCheckedKeys, setHalfCheckedKeys] = useState<Key[]>([]);
+  const [loading, setLoading] = useState(false);
+
+  const getTree = async () => {
+    const data = await userApi.getPermTree();
+    setPermTree(data);
+  };
+
+  const getDetail = async () => {
+    try {
+      setLoading(true);
+      const data = await userApi.getRole(params.id as string);
+      form.setFieldsValue({
+        roleName: data.role.roleName,
+        roleDesc: data.role.roleDesc,
+      });
+
+      const idsRes = getCheckedIds(permTree, getAuthorizedIds(data.permission));
+      setCheckedKeys(idsRes.fullyCheckedIds);
+      setHalfCheckedKeys(idsRes.halfCheckedIds);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSubmit = async () => {
+    if (!(await form.validateFields())) return;
+
+    const val = form.getFieldsValue();
+    await userApi.saveRole({
+      ...val,
+      resources: [...checkedKeys, ...halfCheckedKeys],
+      id: params.id,
+    });
+    navigate(-1);
+  };
+
+  useEffect(() => {
+    getTree();
+  }, []);
+
+  useEffect(() => {
+    if (params.id && permTree.length) getDetail();
+  }, [permTree]);
+
+  return (
+    <PageContainer title={isEdit ? "编辑角色" : "新增角色"}>
+      {loading && <DageLoading />}
+
+      <Form labelCol={{ span: 4 }} form={form}>
+        <Form.Item
+          label="角色名称"
+          name="roleName"
+          required
+          rules={[{ required: true, message: "请输入内容" }]}
+        >
+          <Input
+            className="w450"
+            showCount
+            maxLength={10}
+            placeholder="请输入内容,最多10字;不能重复"
+          />
+        </Form.Item>
+        <Form.Item label="角色说明" name="roleDesc">
+          <TextArea
+            className="w450"
+            showCount
+            style={{ height: 200 }}
+            maxLength={200}
+            placeholder="请输入内容,最多200字;不能重复"
+          />
+        </Form.Item>
+        <Form.Item label="用户权限">
+          <div className="w450">
+            <Tree
+              checkable
+              // @ts-ignore
+              treeData={permTree}
+              checkedKeys={{
+                checked: checkedKeys,
+                halfChecked: halfCheckedKeys,
+              }}
+              fieldNames={{ title: "name", key: "id" }}
+              onCheck={(keys, halfKeys) => {
+                setCheckedKeys(keys as number[]);
+                if (halfKeys.halfCheckedKeys) {
+                  setHalfCheckedKeys(halfKeys.halfCheckedKeys as number[]);
+                }
+              }}
+            />
+          </div>
+        </Form.Item>
+      </Form>
+
+      <FormPageFooter onSubmit={handleSubmit} onCancel={() => navigate(-1)} />
+    </PageContainer>
+  );
+};
+
+export default RoleEditPage;

+ 35 - 2
src/pages/User/components/UserAdd/index.tsx

@@ -1,4 +1,4 @@
-import { SaveUserType } from "@/types";
+import { IRoleItem, SaveUserType } from "@/types";
 import {
   Button,
   Form,
@@ -6,9 +6,10 @@ import {
   Input,
   Modal,
   Popconfirm,
+  Select,
   message,
 } from "antd";
-import React, { useCallback, useEffect, useRef } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
 import styles from "./index.module.scss";
 import { userApi } from "@/api";
 
@@ -20,6 +21,8 @@ type Props = {
 };
 
 function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
+  const [roleLoading, setRoleLoading] = useState(false);
+  const [roleList, setRoleList] = useState<IRoleItem[]>([]);
   // 设置表单初始数据(区分编辑和新增)
   const FormBoxRef = useRef<FormInstance>(null);
 
@@ -61,6 +64,20 @@ function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
     [addTableList, closePage, id, upTableList]
   );
 
+  const getRoleList = async () => {
+    try {
+      setRoleLoading(true);
+      const data = await userApi.getRoleList();
+      setRoleList(data);
+    } finally {
+      setRoleLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getRoleList();
+  }, []);
+
   return (
     <Modal
       wrapClassName={styles.userAdd}
@@ -112,6 +129,22 @@ function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
             <Input maxLength={8} showCount placeholder="请输入内容" />
           </Form.Item>
 
+          <Form.Item
+            label="用户角色"
+            name="roleId"
+            rules={[{ required: true, message: "请选择用户角色!" }]}
+          >
+            <Select
+              loading={roleLoading}
+              options={roleList}
+              fieldNames={{
+                label: "roleName",
+                value: "id",
+              }}
+              placeholder="请选择"
+            />
+          </Form.Item>
+
           {id ? null : <div className="passTit">* 默认密码 123456</div>}
 
           {/* 确定和取消按钮 */}

+ 79 - 0
src/pages/User/role.tsx

@@ -0,0 +1,79 @@
+import { userApi } from "@/api";
+import { IRoleItem } from "@/types";
+import { Button, Table } from "antd";
+import { DageTableActions } from "@dage/pc-components";
+import { useEffect, useState } from "react";
+import { PageContainer } from "@/components";
+import { PlusOutlined } from "@ant-design/icons";
+import { useNavigate } from "react-router-dom";
+
+export default function RolePage() {
+  const [list, setList] = useState<IRoleItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const navigate = useNavigate();
+
+  const getList = async () => {
+    setLoading(true);
+    try {
+      const data = await userApi.getRoleList();
+      setList(data);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleDelete = async (id: number) => {
+    await userApi.deleteRole(id);
+    getList();
+  };
+
+  useEffect(() => {
+    getList();
+  }, []);
+
+  return (
+    <PageContainer
+      title="角色管理"
+      headerSlot={
+        <Button
+          type="primary"
+          icon={<PlusOutlined />}
+          size="large"
+          onClick={() => navigate("/setting/role/create")}
+        >
+          新增
+        </Button>
+      }
+    >
+      <Table
+        loading={loading}
+        className="page-table"
+        dataSource={list}
+        columns={[
+          {
+            title: "角色名称",
+            dataIndex: "roleName",
+            align: "center",
+          },
+          {
+            title: "角色说明",
+            dataIndex: "roleDesc",
+            align: "center",
+          },
+          {
+            title: "操作",
+            align: "center",
+            render: (val) => (
+              <DageTableActions
+                onEdit={() => navigate(`/setting/role/edit/${val.id}`)}
+                onDelete={handleDelete.bind(undefined, val.id)}
+              />
+            ),
+          },
+        ]}
+        rowKey="id"
+        pagination={false}
+      />
+    </PageContainer>
+  );
+}

+ 53 - 1
src/router/index.tsx

@@ -8,17 +8,20 @@ import { ReactComponent as PerfomanceIcon } from "@/assets/icons/icon_achievemen
 
 export const DEFAULT_MENU: DageRouteItem[] = [
   {
+    mapId: 100,
     path: "/assessment",
     title: "考核设置",
     icon: <Icon component={AssessmentIcon} />,
     redirect: "/assessment/index",
     children: [
       {
+        mapId: 110,
         path: "/assessment/index",
         title: "指标设置",
         Component: React.lazy(() => import("../pages/Assessment/Index")),
         children: [
           {
+            mapId: 111,
             hide: true,
             path: "/assessment/index/create/:type",
             title: "新增指标",
@@ -27,6 +30,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             ),
           },
           {
+            mapId: 112,
             hide: true,
             path: "/assessment/index/edit/:type/:id",
             title: "编辑指标",
@@ -37,11 +41,13 @@ export const DEFAULT_MENU: DageRouteItem[] = [
         ],
       },
       {
+        mapId: 120,
         path: "/assessment/template",
         title: "考核模板",
         Component: React.lazy(() => import("../pages/Assessment/Template")),
         children: [
           {
+            mapId: 121,
             hide: true,
             path: "/assessment/template/create/:type",
             title: "新增模板",
@@ -50,6 +56,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             ),
           },
           {
+            mapId: 122,
             hide: true,
             path: "/assessment/template/edit/:type/:id",
             title: "编辑模板",
@@ -62,17 +69,20 @@ export const DEFAULT_MENU: DageRouteItem[] = [
     ],
   },
   {
+    mapId: 200,
     path: "/management",
     title: "考核管理",
     icon: <Icon component={ManagementIcon} />,
     redirect: "/management/index",
     children: [
       {
+        mapId: 210,
         path: "/management/index",
         title: "考核管理",
         Component: React.lazy(() => import("../pages/Management/Index")),
         children: [
           {
+            mapId: 211,
             hide: true,
             path: "/management/index/create/:type",
             title: "新增考核",
@@ -81,6 +91,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             ),
           },
           {
+            mapId: 212,
             hide: true,
             path: "/management/index/edit/:type/:id",
             title: "编辑考核",
@@ -89,6 +100,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             ),
           },
           {
+            mapId: 213,
             hide: true,
             path: "/management/index/setting-index/:type/:id",
             title: "设置指标",
@@ -97,6 +109,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             ),
           },
           {
+            mapId: 214,
             hide: true,
             path: "/management/index/setting-role/:type/:id/:status",
             title: "设置角色",
@@ -105,6 +118,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             ),
           },
           {
+            mapId: 215,
             hide: true,
             meta: {
               custom: true,
@@ -114,6 +128,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             Component: React.lazy(() => import("../pages/AssessmentDetail")),
           },
           {
+            mapId: 216,
             hide: true,
             path: "/management/index/detail/:id/index",
             title: "考核指标详情",
@@ -124,11 +139,13 @@ export const DEFAULT_MENU: DageRouteItem[] = [
         ],
       },
       {
+        mapId: 220,
         path: "/management/form",
         title: "考核填报",
         Component: React.lazy(() => import("../pages/Management/Form")),
         children: [
           {
+            mapId: 221,
             hide: true,
             meta: {
               custom: true,
@@ -138,6 +155,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             Component: React.lazy(() => import("../pages/AssessmentDetail")),
           },
           {
+            mapId: 222,
             hide: true,
             path: "/management/form/detail/index",
             title: "考核指标详情",
@@ -148,11 +166,13 @@ export const DEFAULT_MENU: DageRouteItem[] = [
         ],
       },
       {
+        mapId: 230,
         path: "/management/evaluation",
         title: "考核评定",
         Component: React.lazy(() => import("../pages/Management/Evaluation")),
         children: [
           {
+            mapId: 231,
             hide: true,
             meta: {
               custom: true,
@@ -162,6 +182,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             Component: React.lazy(() => import("../pages/AssessmentDetail")),
           },
           {
+            mapId: 232,
             hide: true,
             path: "/management/evaluation/detail/index",
             title: "考核指标详情",
@@ -172,6 +193,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
         ],
       },
       {
+        mapId: 240,
         path: "/management/files",
         title: "附件管理",
         Component: React.lazy(() => import("../pages/Management/Files")),
@@ -179,24 +201,35 @@ export const DEFAULT_MENU: DageRouteItem[] = [
     ],
   },
   {
+    mapId: 300,
     path: "/perfomance",
     title: "绩效分析",
     redirect: "/perfomance/report",
     icon: <Icon component={PerfomanceIcon} />,
     children: [
       {
+        mapId: 310,
         path: "/perfomance/report",
         title: "指标分析",
         Component: React.lazy(() => import("../pages/Performance/Report")),
       },
       {
+        mapId: 320,
         path: "/perfomance/form",
         title: "考核报告",
         Component: React.lazy(() => import("../pages/Performance/Form")),
         children: [
           {
             hide: true,
-            path: "/perfomance/form/edit",
+            path: "/perfomance/form/create/:id",
+            title: "新增报告",
+            Component: React.lazy(
+              () => import("../pages/Performance/Form/Edit")
+            ),
+          },
+          {
+            hide: true,
+            path: "/perfomance/form/edit/:id",
             title: "编辑报告",
             Component: React.lazy(
               () => import("../pages/Performance/Form/Edit")
@@ -220,6 +253,25 @@ export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
         Component: React.lazy(() => import("../pages/User")),
       },
       {
+        path: "/setting/role",
+        title: "角色管理",
+        Component: React.lazy(() => import("../pages/User/role")),
+        children: [
+          {
+            hide: true,
+            path: "/setting/role/create",
+            title: "新增角色",
+            Component: React.lazy(() => import("../pages/User/RoleEdit")),
+          },
+          {
+            hide: true,
+            path: "/setting/role/edit/:id",
+            title: "编辑角色",
+            Component: React.lazy(() => import("../pages/User/RoleEdit")),
+          },
+        ],
+      },
+      {
         path: "/setting/log",
         title: "操作日志",
         Component: React.lazy(() => import("../pages/Log")),

+ 2 - 0
src/router/types.ts

@@ -4,6 +4,8 @@ export interface DageRouteItem {
   title: string;
   path: string;
   Component?: FC;
+  /** 数据库id映射 */
+  mapId?: number;
   /** 重定向地址 */
   redirect?: string;
   /**

+ 1 - 1
src/store/reducer/base.ts

@@ -8,7 +8,7 @@ const initState = {
 // 定义 action 类型
 type BaseActionType = {
   type: "setUserInfo";
-  payload: LoginResponse;
+  payload: LoginResponse | null;
 };
 
 // 频道 reducer

+ 3 - 1
src/types/index.ts

@@ -16,8 +16,9 @@ export interface LoginResponse {
     realName: string;
     phone: string;
     thumb: string;
-    isAdmin: boolean;
+    isAdmin: YES_OR_NO;
     isEnabled: boolean;
+    roleId: number;
   };
 }
 
@@ -78,3 +79,4 @@ export * from "./log";
 export * from "./user";
 export * from "./assessment";
 export * from "./management";
+export * from "./performance";

+ 49 - 0
src/types/performance.ts

@@ -0,0 +1,49 @@
+import { PaginationParams } from "@dage/service";
+
+export interface IPerformanceRank {
+  id: number;
+  assessName: string;
+  deptName: string;
+  opinionScore: string;
+  dateEnd: string;
+  dateStart: string;
+  jsonWarn: string;
+}
+
+export interface IRepoListParams extends PaginationParams {
+  searchKey: string;
+}
+
+export interface IRepoDetail {
+  reAssess: {
+    dateEnd: string;
+    dateStart: string;
+    name: string;
+    remark: string;
+    publishStatus: string;
+    score: string;
+    opinionScore: string;
+    opinion: string;
+    jsonAdd: string;
+    jsonSub: string;
+  };
+  reDept: {
+    leaderName: string;
+    name: string;
+    crewName: string;
+    levelMuseum: string;
+  }[];
+  reNorm: {
+    deptName: string;
+    isPoint: string;
+    name: string;
+    opinion: string;
+    remark: string;
+    num: string;
+    opinionScore: string;
+  }[];
+  createTime: string;
+  creatorName: string;
+  groupName: string;
+  groupUser: string;
+}

+ 22 - 1
src/types/user.ts

@@ -1,9 +1,11 @@
+import { YES_OR_NO } from ".";
+
 export type UserTableListType = {
   createTime: string;
   creatorId: null;
   creatorName: string;
   id: number;
-  isAdmin: number;
+  isAdmin: YES_OR_NO;
   isEnabled: number;
   nickName: string;
   phone: string;
@@ -29,3 +31,22 @@ export type SaveUserType = {
   roleId: number;
   realName: string;
 };
+
+export type PermItemType = {
+  id: number;
+  name: string;
+  authority: boolean;
+  parentId: number | null;
+  children: PermItemType[];
+};
+
+export interface SaveRoleParams {
+  id?: number;
+  resources?: number[];
+  roleDesc?: string;
+  roleName: string;
+}
+
+export interface IRoleItem extends Required<SaveRoleParams> {
+  isEnabled: YES_OR_NO;
+}

+ 35 - 1
src/utils/index.ts

@@ -1,7 +1,7 @@
 import { removeTokenInfo } from "@dage/pc-components";
 import { logoutApi } from "@/api";
 import { Key } from "react";
-import { AssIndexTreeItemType } from "@/types";
+import { AssIndexTreeItemType, PermItemType } from "@/types";
 import { RcFile } from "antd/es/upload";
 import { message } from "antd";
 
@@ -65,3 +65,37 @@ export const beforeUpload = (suffix: string, file: RcFile) => {
   }
   return result;
 };
+
+export const getAuthorizedIds = (permItems: PermItemType[]) => {
+  const result: number[] = [];
+  const stack: PermItemType[] = [...permItems];
+
+  while (stack.length > 0) {
+    const item = stack.pop()!;
+    if (item.authority) {
+      result.push(item.id);
+    }
+    if (item.children && item.children.length > 0) {
+      stack.push(...[...item.children].reverse());
+    }
+  }
+
+  return result;
+};
+
+export const filterAuthorizedItems = (permItems: PermItemType[]) => {
+  return permItems
+    .map((item) => {
+      const newItem: PermItemType = { ...item };
+
+      if (newItem.children && newItem.children.length > 0) {
+        newItem.children = filterAuthorizedItems(newItem.children);
+      }
+
+      const shouldKeep =
+        newItem.authority || (newItem.children && newItem.children.length > 0);
+
+      return shouldKeep ? newItem : null;
+    })
+    .filter(Boolean) as PermItemType[];
+};