chenlei 1 неделя назад
Родитель
Сommit
b8cbe7c7e2
38 измененных файлов с 2234 добавлено и 1625 удалено
  1. 3 1
      README.md
  2. 3 2
      package.json
  3. 2 2
      public/index.html
  4. 79 41
      src/api/management.ts
  5. BIN
      src/assets/images/logo.png
  6. 58 8
      src/components/AddIndexModal/index.tsx
  7. 223 217
      src/components/FileTemplateModal/index.tsx
  8. 1 0
      src/components/FileTemplateTable/index.tsx
  9. 16 0
      src/constants.ts
  10. 158 151
      src/pages/Assessment/Index/CreateOrEdit/components/InspectionEditable/index.tsx
  11. 549 533
      src/pages/Assessment/Index/CreateOrEdit/index.tsx
  12. 3 3
      src/pages/Assessment/Index/components/Container/index.tsx
  13. 4 1
      src/pages/Assessment/Template/CreateOrEdit/index.tsx
  14. 140 66
      src/pages/AssessmentDetail/components/IndexAssessment/index.tsx
  15. 25 22
      src/pages/AssessmentDetail/components/IndexDetailModal/form.tsx
  16. 86 47
      src/pages/AssessmentDetail/components/IndexDetailModal/index.tsx
  17. 128 106
      src/pages/AssessmentDetail/components/OverallAssessment/index.tsx
  18. 13 4
      src/pages/AssessmentDetail/components/SelfReportScoreModal/index.tsx
  19. 14 1
      src/pages/AssessmentDetail/components/SubEvaluationModal/index.tsx
  20. 22 16
      src/pages/AssessmentDetail/index.tsx
  21. 11 4
      src/pages/Layout/index.tsx
  22. BIN
      src/pages/Login/images/logo_black-min.png
  23. 1 1
      src/pages/Login/index.tsx
  24. 11 9
      src/pages/Management/Evaluation/index.tsx
  25. 5 5
      src/pages/Management/Files/index.tsx
  26. 8 8
      src/pages/Management/Form/index.tsx
  27. 80 6
      src/pages/Management/Index/CreateOrEdit/index.tsx
  28. 51 12
      src/pages/Management/Index/SettingIndex/index.tsx
  29. 2 2
      src/pages/Management/Index/SettingRole/index.tsx
  30. 15 4
      src/pages/Management/Index/components/AddDeptModal/index.tsx
  31. 1 0
      src/pages/Management/Index/components/AllocationOfDataModal/index.tsx
  32. 6 4
      src/pages/Management/Index/components/AllocationOfIndexDataModal/index.tsx
  33. 28 10
      src/pages/Management/Index/components/AllocationOfIndexModal/index.tsx
  34. 1 1
      src/pages/Management/Index/index.tsx
  35. 30 29
      src/pages/User/RoleEdit.tsx
  36. 18 14
      src/router/index.tsx
  37. 218 194
      src/types/management.ts
  38. 221 101
      src/utils/index.ts

+ 3 - 1
README.md

@@ -1 +1,3 @@
-# ZHS2409030-1	2024首都大运河博物馆数字化
+# ZHS2409030-1 2024首都大运河博物馆数字化
+
+测试环境 /data/data/museum_beijing_dayunhe_data/backstage

+ 3 - 2
package.json

@@ -84,9 +84,10 @@
     "workbox-webpack-plugin": "^6.4.1"
   },
   "scripts": {
-    "start": "cross-env REACT_APP_API_URL=https://sit-shoubodyh.4dage.com REACT_APP_IMG_PUBLIC=/api node scripts/start.js",
+    "start": "cross-env REACT_APP_API_URL=http://192.168.20.61:8090 REACT_APP_IMG_PUBLIC=/api node scripts/start.js",
+    "start:test": "cross-env REACT_APP_API_URL=https://sit-shoubodyh.4dage.com REACT_APP_IMG_PUBLIC=/api node scripts/start.js",
     "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shoubodyh.4dage.com REACT_APP_IMG_PUBLIC= node scripts/build.js",
-    "build:prod": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=http://192.124.82.43:8091 REACT_APP_IMG_PUBLIC= node scripts/build.js"
+    "build:prod": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://portal.canalmuseum.org.cn/backstage REACT_APP_IMG_PUBLIC= node scripts/build.js"
   },
   "eslintConfig": {
     "extends": [

+ 2 - 2
public/index.html

@@ -1,10 +1,10 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
   <head>
     <meta charset="utf-8" />
     <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <title>博物馆运行考核评估系统</title>
+    <title>综合运行指标管理系统</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 79 - 41
src/api/management.ts

@@ -21,7 +21,7 @@ import {
   REVIEW_MATERIAL_TYPE,
   ARCHIVE_TYPE,
 } from "@/types";
-import { requestByGet, requestByPost, requestPagination } from "@dage/service";
+import { requestByGet, requestByPost } from "@dage/service";
 
 export const getManageIndexListApi = (params: any) => {
   return requestByPost("/api/cms/assess/pageList", params);
@@ -41,7 +41,7 @@ export const getManageIndexDetailApi = (id: string | number) => {
 
 export const publishManageIndexApi = (
   id: string | number,
-  status = PUBLISH_ENUM.PUBLISHED
+  status = PUBLISH_ENUM.PUBLISHED,
 ) => {
   return requestByGet(`/api/cms/assess/publish/${id}/${status}`);
 };
@@ -64,67 +64,73 @@ export const deleteManageRoleDeptApi = (ids: number | string) => {
 
 export const getManageAssFixedListApi = (
   id: number | string,
-  searchKey?: string
+  searchKey?: string,
 ) => {
   return requestByGet<IManageAssessmentIndex[]>(
     `/api/cms/assessFixed/getList/${id}`,
     {
       searchKey,
-    }
+    },
   );
 };
 
 export const getManageAssOperationListApi = (
   id: number | string,
-  searchKey?: string
+  searchKey?: string,
 ) => {
   return requestByGet<IManageAssOperationResponse>(
     `/api/cms/assessOperation/getList/${id}`,
     {
       searchKey,
-    }
+    },
   );
 };
 
 export const setManageAssFixedApi = (
   id: string | number,
-  indexIds: string[]
+  indexIds: string[],
 ) => {
   return requestByPost(`/api/cms/assessFixed/save/${id}`, indexIds);
 };
 
 export const setManageAssOperationApi = (
   id: string | number,
-  indexIds: string[]
+  indexIds: string[],
 ) => {
   return requestByPost(`/api/cms/assessOperation/save/${id}`, indexIds);
 };
 
 export const getManageFormListApi = (params: IManageFormListParams) => {
-  return requestPagination<IManageFormItem>("/api/cms/fill/pageList", params);
+  return requestByPost<{ records: IManageFormItem[]; total: number }>(
+    "/api/cms/fill/pageList",
+    params,
+  );
 };
 
 // 部门考核单概况
 export const getManageFormDetailApi = (
   id: number | string,
-  assessId: number | string
+  assessId: number | string,
 ) => {
   return requestByGet<IManageFormDetail>(
-    `/api/cms/fill/detail/${id}/${assessId}`
+    `/api/cms/fill/detail/${id}/${assessId}`,
   );
 };
 
 export const getManageEvaluationListApi = (params: IManageFormListParams) => {
-  return requestPagination<IManageFormItem>("/api/cms/review/pageList", params);
+  return requestByPost<{ records: IManageFormItem[]; total: number }>(
+    "/api/cms/review/pageList",
+    params,
+  );
 };
 
 export const getManageEvaluationDetailApi = (id: number | string) => {
-  return requestByGet<IManageFormDetail>(`/api/cms/review/detail/${id}`);
+  return requestByGet<IManageFormDetail>(`/api/cms/review/assessDetail/${id}`);
 };
 
 export const getManageDeptAllocationOfDataListApi = (id: number | string) => {
   return requestByGet<null | IManageDeptMaterialItem[]>(
-    `/api/cms/dept/material/getList/${id}`
+    `/api/cms/dept/material/getList/${id}`,
   );
 };
 
@@ -132,18 +138,19 @@ export const saveManageDeptAllocationOfDataApi = (params: any) => {
   return requestByPost("/api/cms/dept/material/save", params);
 };
 
-export const getManageDeptAllocationOfIndexListApi = (
-  assessId: number | string,
-  deptId: number | string,
-  normType: ASS_INDEX_TYPE,
+/**
+ * 设置角色-分配指标-列表
+ */
+export const getManageDeptAllocationOfIndexListApi = (params: {
+  assessId: number | string;
+  deptId: number | string;
+  type: ASS_INDEX_TYPE;
   // 查看未分配
-  assign?: number
-) => {
-  return requestByGet<IManageDeptAllocationOfIndexItem[]>(
-    `/api/cms/dept/norm/getTree/${assessId}/${deptId}/${normType}`,
-    {
-      assign,
-    }
+  assign?: number;
+}) => {
+  return requestByPost<IManageDeptAllocationOfIndexItem[]>(
+    "/api/cms/dept/getNormScopeTree",
+    params,
   );
 };
 
@@ -168,7 +175,7 @@ export const deleteManageRoleGroupApi = (ids: string | number) => {
 
 export const saveManageAssOperationWeightApi = (
   id: number | string,
-  weight: number | string
+  weight: number | string,
 ) => {
   return requestByPost(`/api/cms/assessOperation/setWeight/${id}/${weight}`);
 };
@@ -176,7 +183,8 @@ export const saveManageAssOperationWeightApi = (
 // 分配指标-检查指标被分配给多个部门
 export const checkManageIndexApi = (params: {
   assessId: number | string;
-  normIds: string;
+  deptId: number | string;
+  normIds: string[];
 }) => {
   return requestByPost<IAssIndexDetail[]>("/api/cms/dept/norm/check", params);
 };
@@ -186,13 +194,30 @@ export const saveManageFileApi = (params: ISaveManageFileParams) => {
   return requestByPost("/api/cms/fill/file/saveEntity", params);
 };
 
-// 指标考核-列表
+// 考核管理-指标考核-列表
+export const getManageAccessNormListApi = (params: {
+  deptId: string | number;
+  searchKey?: string;
+  uploadStatus?: number;
+}) => {
+  return requestByPost<IManageNormItem[]>("/api/cms/assess/getNormPermTree", {
+    assessId: params.deptId,
+    searchKey: params.searchKey,
+    uploadStatus: params.uploadStatus,
+  });
+};
+
+// 考核填报-指标考核-列表
 export const getManageNormListApi = (params: {
   deptId: string | number;
   searchKey?: string;
-  uploadStatus?: YES_OR_NO;
+  uploadStatus?: number;
 }) => {
-  return requestByPost<IManageNormItem[]>("/api/cms/fill/norm/getList", params);
+  return requestByPost<IManageNormItem[]>("/api/cms/fill/getNormPermTree", {
+    assessId: params.deptId,
+    searchKey: params.searchKey,
+    uploadStatus: params.uploadStatus,
+  });
 };
 
 // 附件管理-列表
@@ -203,22 +228,34 @@ export const getManageFileListAPi = (params: any) => {
 // 自评得分
 export const saveSelfScoreApi = (
   id: number | string,
-  score: number | string
+  score: number | string,
 ) => {
   return requestByGet(`/api/cms/fill/norm/updateScore/${id}/${score}`);
 };
 
 // 查看指标&考核单资料列表
 export const getFileListApi = (
+  /** 考核id */
   assessId: number,
   type: "norm" | "assess",
-  moduleId: number
+  /** 指标id */
+  moduleId: number,
 ) => {
-  return requestByGet<MaterialType[]>(
-    `/api/cms/fill/norm/getFile/${moduleId}/${type}/${assessId}`
+  return requestByGet<{ fill: MaterialType[]; norm: MaterialType[] }>(
+    `/api/cms/fill/norm/getFile/${moduleId}/${type}/${assessId}`,
   );
 };
 
+export const getMaterialFileListApi = (
+  assessId: number,
+  materialIds: string[],
+) => {
+  return requestByPost<MaterialType[]>(`/api/cms/fill/getFillMaterial`, {
+    assessId,
+    materialIds,
+  });
+};
+
 // 删除资料
 export const deleteFileApi = (ids: string | number) => {
   return requestByGet(`/api/cms/fill/file/removes/${ids}`);
@@ -232,7 +269,7 @@ export const submitAssessmentApi = (deptId?: string) => {
 // 考核单审核
 export const examineAssessmentApi = (
   id: number | string,
-  status: DEPT_STATUS_ENUM
+  status: DEPT_STATUS_ENUM,
 ) => {
   return requestByPost(`/api/cms/fill/audit/${id}/${status}`);
 };
@@ -249,10 +286,10 @@ export const additionalEvaOpinionApi = (params: {
 
 // 考核评定-评定意见
 export const assessmentEvaOpinionApi = (params: {
-  deptId: number;
+  assessId: number;
   opinion: string;
 }) => {
-  return requestByPost("/api/cms/review/dept/opinion", params);
+  return requestByPost("/api/cms/review/assessOpinion", params);
 };
 
 // 考核评定-设置资料是否合格
@@ -289,13 +326,14 @@ export const normEvaOpinionApi = (params: {
 
 // 考核评定-资料列表
 export const getReviewNormListApi = (params: {
-  deptId: string | number;
+  assessId: string | number;
+  status?: PUBLISH_ENUM;
   searchKey?: string;
   uploadStatus?: YES_OR_NO;
 }) => {
   return requestByPost<IManageNormItem[]>(
-    "/api/cms/review/norm/getList",
-    params
+    "/api/cms/review/getNormPermTree",
+    params,
   );
 };
 
@@ -312,7 +350,7 @@ export const refundReviewApi = (assessId: number, deptId: number) => {
 // 附件归档
 export const changeArchiveApi = (
   condition: ARCHIVE_TYPE,
-  materialId: number
+  materialId: number,
 ) => {
   return requestByGet(`/api/cms/annex/archive/${materialId}/${condition}`);
 };

BIN
src/assets/images/logo.png


+ 58 - 8
src/components/AddIndexModal/index.tsx

@@ -9,13 +9,45 @@ import { DageLoading } from "@dage/pc-components";
 export interface AddIndexModalProps extends Omit<ModalProps, "onOk"> {
   onCancel?: () => void;
   onOk?: (keys: string[]) => void;
+  initialCheckedKeys?: Key[];
   // onOk?: (keys: Key[], items: AssIndexTreeItemType[]) => void;
 }
 
+const normalizeCheckedKeys = (
+  checked: Key[] | { checked: Key[]; halfChecked: Key[] },
+) => (Array.isArray(checked) ? checked : checked.checked);
+
+const collectTreeKeys = (data: AssIndexTreeItemType[]): Set<Key> => {
+  const keys = new Set<Key>();
+
+  const walk = (nodes: AssIndexTreeItemType[]) => {
+    nodes.forEach((node) => {
+      keys.add(node.id);
+      if (node.children?.length) walk(node.children);
+    });
+  };
+
+  walk(data);
+  return keys;
+};
+
+const filterExistingTreeKeys = (keys: Key[], data: AssIndexTreeItemType[]) => {
+  if (!data.length) return [];
+
+  const existing = collectTreeKeys(data);
+  return keys.filter(
+    (key) =>
+      existing.has(key) ||
+      existing.has(Number(key)) ||
+      existing.has(String(key)),
+  );
+};
+
 export const AddIndexModal: FC<AddIndexModalProps> = ({
   open,
   onOk,
   onCancel,
+  initialCheckedKeys,
   ...rest
 }) => {
   const params = useParams();
@@ -24,11 +56,22 @@ export const AddIndexModal: FC<AddIndexModalProps> = ({
   const [treeData, setTreeData] = useState<AssIndexTreeItemType[]>([]);
   const [loading, setLoading] = useState(false);
 
+  const syncCheckedKeys = (keys?: Key[]) => {
+    const nextKeys = filterExistingTreeKeys(keys || [], treeData);
+    setCheckedKeys(nextKeys);
+    form.setFieldsValue({ onlyChildKeys: nextKeys });
+  };
+
   const getAssIndexTree = async () => {
     try {
       setLoading(true);
       const data = await getAssIndexTreeApi(params.type as ASS_INDEX_TYPE);
       setTreeData(data);
+      if (initialCheckedKeys?.length) {
+        const nextKeys = filterExistingTreeKeys(initialCheckedKeys, data);
+        setCheckedKeys(nextKeys);
+        form.setFieldsValue({ onlyChildKeys: nextKeys });
+      }
     } finally {
       setLoading(false);
     }
@@ -51,7 +94,17 @@ export const AddIndexModal: FC<AddIndexModalProps> = ({
   };
 
   const handleAfterOpenChange = (open: boolean) => {
-    if (open && !treeData.length) getAssIndexTree();
+    if (open) {
+      if (treeData.length) {
+        syncCheckedKeys(initialCheckedKeys);
+      } else {
+        getAssIndexTree();
+      }
+      return;
+    }
+
+    setCheckedKeys([]);
+    form.setFieldsValue({ onlyChildKeys: [] });
   };
 
   return (
@@ -87,14 +140,11 @@ export const AddIndexModal: FC<AddIndexModalProps> = ({
             fieldNames={{ title: "name", key: "id" }}
             checkedKeys={_checkedKeys}
             treeData={treeData}
-            onCheck={(checkedKeys, { checkedNodes }) => {
-              const onlyChildKeys = (checkedKeys as Key[]).filter((i) => {
-                const node = checkedNodes.find((n) => n.id === i);
-                return !node?.children.length;
-              });
-              setCheckedKeys(onlyChildKeys);
+            onCheck={(checked) => {
+              const keys = normalizeCheckedKeys(checked);
+              setCheckedKeys(keys);
               form.setFieldsValue({
-                onlyChildKeys,
+                onlyChildKeys: keys,
               });
             }}
           />

+ 223 - 217
src/components/FileTemplateModal/index.tsx

@@ -1,217 +1,223 @@
-import {
-  Checkbox,
-  CheckboxProps,
-  Form,
-  Input,
-  Modal,
-  ModalProps,
-  Radio,
-} from "antd";
-import { FC, useEffect, useMemo, useState } from "react";
-import style from "./index.module.scss";
-import {
-  DageUpload,
-  DageUploadConsumer,
-  DageUploadProvider,
-  DageUploadType,
-} from "@dage/pc-components";
-import { FILE_TYPE_ENUM } from "./constants";
-import { IFileTemplateForm, IFileTemplateFormParams } from "@/types";
-import { saveEntityApi } from "@/api";
-import { getBaseURL } from "@dage/service";
-
-export interface FileTemplateModalProps extends ModalProps {
-  item?: IFileTemplateFormParams | null;
-  /**
-   * 层级
-   * @default 1
-   */
-  level?: number;
-  module: "norm" | "assess";
-  moduleId?: number;
-  // 如果level为2,则是上传附件,需要传所属资料id
-  parentId?: number;
-  onCancel?: () => void;
-  onOk?: (val: any, isEdit?: boolean) => void;
-}
-
-export const FileTemplateModal: FC<FileTemplateModalProps> = ({
-  item,
-  open,
-  level = 1,
-  module,
-  moduleId,
-  parentId,
-  onOk,
-  onCancel,
-  ...rest
-}) => {
-  const [form] = Form.useForm<IFileTemplateForm>();
-  const typesVal = Form.useWatch("suffix", form);
-  const baseUrl = getBaseURL();
-  const [loading, setLoading] = useState(false);
-
-  const checkAll = useMemo(
-    () => FILE_TYPE_ENUM.length === typesVal?.length,
-    [typesVal]
-  );
-  const indeterminate = useMemo(
-    () =>
-      (typesVal?.length || 0) > 0 &&
-      (typesVal?.length || 0) < FILE_TYPE_ENUM.length,
-    [typesVal]
-  );
-
-  const handleCancel = () => {
-    form.resetFields();
-    onCancel?.();
-  };
-
-  const handleConfirm = () => {
-    form.submit();
-  };
-
-  const handleSubmit = async (values: IFileTemplateForm) => {
-    const { file, suffix, ...rest } = values;
-
-    try {
-      setLoading(true);
-      const data = await saveEntityApi(
-        {
-          ...rest,
-          level,
-          module,
-          moduleId,
-          parentId,
-          fileName: file?.[0].response?.fileName,
-          filePath: file?.[0].response?.filePath,
-          suffix: suffix?.join(","),
-          id: item?.id,
-        },
-        module
-      );
-
-      onOk?.(data, Boolean(item));
-      handleCancel();
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const onCheckAllChange: CheckboxProps["onChange"] = (e) => {
-    form.setFieldValue("suffix", e.target.checked ? [...FILE_TYPE_ENUM] : []);
-  };
-
-  useEffect(() => {
-    if (!item) {
-      form.resetFields();
-      return;
-    }
-
-    const { suffix, fileName, filePath, ...rest } = item;
-    form.setFieldsValue({
-      ...rest,
-      file: fileName
-        ? [
-            {
-              name: fileName,
-              url: baseUrl + process.env.REACT_APP_IMG_PUBLIC + filePath,
-              response: {
-                fileName,
-                filePath,
-              },
-            },
-          ]
-        : undefined,
-      suffix: suffix?.split(","),
-    });
-  }, [item, form]);
-
-  return (
-    <DageUploadProvider>
-      <DageUploadConsumer>
-        {(consumer) => (
-          <Modal
-            title="上传资料"
-            okText="提交"
-            cancelText="取消"
-            open={open}
-            width={640}
-            okButtonProps={{
-              disabled: consumer?.uploading || loading,
-            }}
-            onOk={handleConfirm}
-            onCancel={handleCancel}
-            className={style.modal}
-            {...rest}
-          >
-            <Form
-              labelCol={{ span: 4, offset: 1 }}
-              form={form}
-              onFinish={handleSubmit}
-            >
-              <Form.Item
-                label="资料名称"
-                rules={[{ required: true }]}
-                name="name"
-              >
-                <Input
-                  placeholder="请输入内容,最多20字"
-                  showCount
-                  autoComplete="off"
-                  maxLength={20}
-                />
-              </Form.Item>
-              <Form.Item
-                label="填报指标时必须上传"
-                name="isUpload"
-                initialValue={0}
-              >
-                <Radio.Group
-                  size="large"
-                  options={[
-                    {
-                      label: "是",
-                      value: 1,
-                    },
-                    {
-                      label: "否",
-                      value: 0,
-                    },
-                  ]}
-                  optionType="button"
-                  buttonStyle="solid"
-                />
-              </Form.Item>
-              <Form.Item
-                label="资料模板"
-                name="file"
-                rules={[{ required: true }]}
-              >
-                <DageUpload
-                  action="/api/cms/norm/file/upload"
-                  dType={DageUploadType.DOC}
-                  maxCount={1}
-                  tips="最多1个附件"
-                />
-              </Form.Item>
-              <Form.Item label="资料格式">
-                <Checkbox
-                  checked={checkAll}
-                  indeterminate={indeterminate}
-                  onChange={onCheckAllChange}
-                >
-                  全部
-                </Checkbox>
-
-                <Form.Item name="suffix" noStyle>
-                  <Checkbox.Group options={FILE_TYPE_ENUM} />
-                </Form.Item>
-              </Form.Item>
-            </Form>
-          </Modal>
-        )}
-      </DageUploadConsumer>
-    </DageUploadProvider>
-  );
-};
+import {
+  Checkbox,
+  CheckboxProps,
+  Form,
+  Input,
+  Modal,
+  ModalProps,
+  Radio,
+} from "antd";
+import { FC, useEffect, useMemo, useState } from "react";
+import style from "./index.module.scss";
+import {
+  DageUpload,
+  DageUploadConsumer,
+  DageUploadProvider,
+  DageUploadType,
+} from "@dage/pc-components";
+import { FILE_TYPE_ENUM } from "./constants";
+import { IFileTemplateForm, IFileTemplateFormParams } from "@/types";
+import { saveEntityApi } from "@/api";
+import { getBaseURL } from "@dage/service";
+import { beforeUploadFileSize } from "@/utils";
+
+export interface FileTemplateModalProps extends ModalProps {
+  item?: IFileTemplateFormParams | null;
+  /**
+   * 层级
+   * @default 1
+   */
+  level?: number;
+  module: "norm" | "assess";
+  moduleId?: number;
+  // 如果level为2,则是上传附件,需要传所属资料id
+  parentId?: number;
+  onCancel?: () => void;
+  onOk?: (val: any, isEdit?: boolean) => void;
+}
+
+export const FileTemplateModal: FC<FileTemplateModalProps> = ({
+  item,
+  open,
+  level = 1,
+  module,
+  moduleId,
+  parentId,
+  onOk,
+  onCancel,
+  ...rest
+}) => {
+  const [form] = Form.useForm<IFileTemplateForm>();
+  const typesVal = Form.useWatch("suffix", form);
+  const baseUrl = getBaseURL();
+  const [loading, setLoading] = useState(false);
+
+  const checkAll = useMemo(
+    () => FILE_TYPE_ENUM.length === typesVal?.length,
+    [typesVal],
+  );
+  const indeterminate = useMemo(
+    () =>
+      (typesVal?.length || 0) > 0 &&
+      (typesVal?.length || 0) < FILE_TYPE_ENUM.length,
+    [typesVal],
+  );
+
+  const handleCancel = () => {
+    form.resetFields();
+    onCancel?.();
+  };
+
+  const handleConfirm = () => {
+    form.submit();
+  };
+
+  const handleSubmit = async (values: IFileTemplateForm) => {
+    const { file, suffix, ...rest } = values;
+
+    try {
+      setLoading(true);
+      const data = await saveEntityApi(
+        {
+          ...rest,
+          level,
+          module,
+          moduleId,
+          parentId,
+          fileName: file?.[0]?.response?.fileName ?? "",
+          filePath: file?.[0]?.response?.filePath ?? "",
+          suffix: suffix?.join(","),
+          id: item?.id,
+        },
+        module,
+      );
+
+      onOk?.(data, Boolean(item));
+      handleCancel();
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const onCheckAllChange: CheckboxProps["onChange"] = (e) => {
+    form.setFieldValue("suffix", e.target.checked ? [...FILE_TYPE_ENUM] : []);
+  };
+
+  useEffect(() => {
+    if (!item) {
+      form.resetFields();
+      return;
+    }
+
+    const { suffix, fileName, filePath, ...rest } = item;
+    form.setFieldsValue({
+      ...rest,
+      file: fileName
+        ? [
+            {
+              name: fileName,
+              url: baseUrl + process.env.REACT_APP_IMG_PUBLIC + filePath,
+              response: {
+                fileName,
+                filePath,
+              },
+            },
+          ]
+        : undefined,
+      suffix: suffix?.split(","),
+    });
+  }, [item, form]);
+
+  return (
+    <DageUploadProvider>
+      <DageUploadConsumer>
+        {(consumer) => (
+          <Modal
+            title="上传资料"
+            okText="提交"
+            cancelText="取消"
+            open={open}
+            width={640}
+            okButtonProps={{
+              disabled: consumer?.uploading || loading,
+            }}
+            onOk={handleConfirm}
+            onCancel={handleCancel}
+            className={style.modal}
+            {...rest}
+          >
+            <Form
+              labelCol={{ span: 4, offset: 1 }}
+              form={form}
+              onFinish={handleSubmit}
+            >
+              <Form.Item
+                label="资料名称"
+                rules={[{ required: true, message: "请输入资料名称" }]}
+                name="name"
+                normalize={(value) =>
+                  typeof value === "string" ? value.trim() : value
+                }
+              >
+                <Input
+                  placeholder="请输入内容,最多20字"
+                  showCount
+                  autoComplete="off"
+                  maxLength={20}
+                />
+              </Form.Item>
+              <Form.Item
+                label="填报指标时必须上传"
+                name="isUpload"
+                initialValue={0}
+              >
+                <Radio.Group
+                  size="large"
+                  options={[
+                    {
+                      label: "是",
+                      value: 1,
+                    },
+                    {
+                      label: "否",
+                      value: 0,
+                    },
+                  ]}
+                  optionType="button"
+                  buttonStyle="solid"
+                />
+              </Form.Item>
+              <Form.Item
+                label="资料模板"
+                name="file"
+                // rules={[{ required: true }]}
+              >
+                <DageUpload
+                  action="/api/cms/norm/file/upload"
+                  dType={DageUploadType.DOC}
+                  maxCount={1}
+                  tips="最多1个附件"
+                  // @ts-ignore
+                  beforeUpload={beforeUploadFileSize}
+                />
+              </Form.Item>
+              <Form.Item label="资料格式">
+                <Checkbox
+                  checked={checkAll}
+                  indeterminate={indeterminate}
+                  onChange={onCheckAllChange}
+                >
+                  全部
+                </Checkbox>
+
+                <Form.Item name="suffix" noStyle>
+                  <Checkbox.Group options={FILE_TYPE_ENUM} />
+                </Form.Item>
+              </Form.Item>
+            </Form>
+          </Modal>
+        )}
+      </DageUploadConsumer>
+    </DageUploadProvider>
+  );
+};

+ 1 - 0
src/components/FileTemplateTable/index.tsx

@@ -73,6 +73,7 @@ export const FileTemplateTable: FC<FileTemplateTableProps> = ({
       <Table
         className="cus-table mw650"
         dataSource={value}
+        pagination={false}
         rowKey="id"
         columns={[
           {

+ 16 - 0
src/constants.ts

@@ -1,6 +1,7 @@
 import {
   ARCHIVE_TYPE,
   ASS_INDEX_TYPE,
+  DEPT_REVIEW_STATUS_ENUM,
   DEPT_STATUS_ENUM,
   PUBLISH_ENUM,
   REVIEW_MATERIAL_TYPE,
@@ -140,9 +141,24 @@ export const REVIEW_MATERIAL_STATUS_MAP = {
   [REVIEW_MATERIAL_TYPE.PENDING]: "待审核",
   [REVIEW_MATERIAL_TYPE.PASS]: "合格",
   [REVIEW_MATERIAL_TYPE.FAIL]: "不合格",
+  [REVIEW_MATERIAL_TYPE.RATED]: "已评定",
+};
+
+export const DEPT_REVIEW_STATUS_MAP = {
+  [DEPT_REVIEW_STATUS_ENUM.EMPTY]: "待填报",
+  [DEPT_REVIEW_STATUS_ENUM.PENDING]: "待审核",
+  [DEPT_REVIEW_STATUS_ENUM.FAIL]: "审批未通过",
+  [DEPT_REVIEW_STATUS_ENUM.PASS]: "审批通过(待评定)",
+  [DEPT_REVIEW_STATUS_ENUM.REJECT]: "退回",
+  [DEPT_REVIEW_STATUS_ENUM.RATED]: "评定通过",
 };
 
 export const ARCHIVE_TYPE_MAP = {
   [ARCHIVE_TYPE.ARCHIVED]: "已归档",
   [ARCHIVE_TYPE.UNARCHIVED]: "未归档",
 };
+
+export const FILL_TYPE_MAP = {
+  api: "api",
+  manual: "手动填报",
+};

+ 158 - 151
src/pages/Assessment/Index/CreateOrEdit/components/InspectionEditable/index.tsx

@@ -1,151 +1,158 @@
-import {
-  EditableProTable,
-  EditableProTableProps,
-} from "@ant-design/pro-components";
-import { Input, InputNumber } from "antd";
-import { uniqueId } from "lodash";
-import { forwardRef, Key, useImperativeHandle, useState } from "react";
-
-export interface InspectionEditableProps
-  extends EditableProTableProps<any, any> {}
-
-export interface InspectionEditableMethods {
-  setEditableRowKeys(v: Key[]): void;
-}
-
-const DEFAULT_INSPECTION_ITEM = {
-  name: "",
-  one: "",
-  two: "",
-  three: "",
-};
-
-export const InspectionEditable = forwardRef<
-  InspectionEditableMethods,
-  InspectionEditableProps
->((props, ref) => {
-  const [editableKeys, setEditableRowKeys] = useState<Key[]>();
-
-  useImperativeHandle(ref, () => ({
-    setEditableRowKeys,
-  }));
-
-  return (
-    <EditableProTable
-      {...props}
-      rowKey="_id"
-      controlled
-      className="mw650"
-      columns={[
-        {
-          title: "要点名称",
-          align: "center",
-          dataIndex: "name",
-          width: 300,
-          formItemProps: () => {
-            return {
-              rules: [{ required: true, message: "此项为必填项" }],
-            };
-          },
-          renderFormItem: () => {
-            return <Input placeholder="请输入内容,最多50字" maxLength={50} />;
-          },
-        },
-        {
-          title: "一级博物馆",
-          align: "center",
-          dataIndex: "one",
-          formItemProps: () => {
-            return {
-              rules: [{ required: true, message: "此项为必填项" }],
-            };
-          },
-          renderFormItem: () => {
-            return (
-              <InputNumber
-                placeholder="请填入正整数"
-                min={0}
-                precision={0}
-                controls={false}
-              />
-            );
-          },
-        },
-        {
-          title: "二级博物馆",
-          align: "center",
-          dataIndex: "two",
-          formItemProps: () => {
-            return {
-              rules: [{ required: true, message: "此项为必填项" }],
-            };
-          },
-          renderFormItem: () => {
-            return (
-              <InputNumber
-                placeholder="请填入正整数"
-                min={0}
-                precision={0}
-                controls={false}
-              />
-            );
-          },
-        },
-        {
-          title: "三级博物馆",
-          align: "center",
-          dataIndex: "three",
-          formItemProps: () => {
-            return {
-              rules: [{ required: true, message: "此项为必填项" }],
-            };
-          },
-          renderFormItem: () => {
-            return (
-              <InputNumber
-                placeholder="请填入正整数"
-                min={0}
-                precision={0}
-                controls={false}
-              />
-            );
-          },
-        },
-        {
-          title: "操作",
-          width: 100,
-          align: "center",
-          valueType: "option",
-          render: (text, record, _, action) => {
-            return (
-              <a
-                key="editable"
-                onClick={() => {
-                  action?.startEditable?.(record.id);
-                }}
-              >
-                编辑
-              </a>
-            );
-          },
-        },
-      ]}
-      editable={{
-        type: "multiple",
-        // editableKeys,
-        actionRender: (row, config, defaultDoms) => {
-          return [defaultDoms.save, defaultDoms.delete];
-        },
-      }}
-      recordCreatorProps={{
-        type: "primary",
-        newRecordType: "dataSource",
-        creatorButtonText: "新增考察要点",
-        record: () => ({
-          _id: uniqueId("insepction"),
-          ...DEFAULT_INSPECTION_ITEM,
-        }),
-      }}
-    />
-  );
-});
+import {
+  EditableProTable,
+  EditableProTableProps,
+} from "@ant-design/pro-components";
+import { Input, InputNumber } from "antd";
+import { uniqueId } from "lodash";
+import { forwardRef, Key, useImperativeHandle, useState } from "react";
+
+export interface InspectionEditableProps
+  extends EditableProTableProps<any, any> {}
+
+export interface InspectionEditableMethods {
+  setEditableRowKeys(v: Key[]): void;
+  getEditableRowKeys(): Key[];
+}
+
+const DEFAULT_INSPECTION_ITEM = {
+  name: "",
+  one: "",
+  two: "",
+  three: "",
+};
+
+export const InspectionEditable = forwardRef<
+  InspectionEditableMethods,
+  InspectionEditableProps
+>((props, ref) => {
+  const [editableKeys, setEditableRowKeys] = useState<Key[]>([]);
+
+  useImperativeHandle(
+    ref,
+    () => ({
+      setEditableRowKeys,
+      getEditableRowKeys: () => editableKeys,
+    }),
+    [editableKeys],
+  );
+
+  return (
+    <EditableProTable
+      {...props}
+      rowKey="_id"
+      controlled
+      className="mw650"
+      columns={[
+        {
+          title: "要点名称",
+          align: "center",
+          dataIndex: "name",
+          width: 300,
+          formItemProps: () => {
+            return {
+              rules: [{ required: true, message: "此项为必填项" }],
+            };
+          },
+          renderFormItem: () => {
+            return <Input placeholder="请输入内容,最多50字" maxLength={50} />;
+          },
+        },
+        {
+          title: "一级博物馆",
+          align: "center",
+          dataIndex: "one",
+          formItemProps: () => {
+            return {
+              rules: [{ required: true, message: "此项为必填项" }],
+            };
+          },
+          renderFormItem: () => {
+            return (
+              <InputNumber
+                placeholder="请填入正整数"
+                min={0}
+                precision={0}
+                controls={false}
+              />
+            );
+          },
+        },
+        {
+          title: "二级博物馆",
+          align: "center",
+          dataIndex: "two",
+          formItemProps: () => {
+            return {
+              rules: [{ required: true, message: "此项为必填项" }],
+            };
+          },
+          renderFormItem: () => {
+            return (
+              <InputNumber
+                placeholder="请填入正整数"
+                min={0}
+                precision={0}
+                controls={false}
+              />
+            );
+          },
+        },
+        {
+          title: "三级博物馆",
+          align: "center",
+          dataIndex: "three",
+          formItemProps: () => {
+            return {
+              rules: [{ required: true, message: "此项为必填项" }],
+            };
+          },
+          renderFormItem: () => {
+            return (
+              <InputNumber
+                placeholder="请填入正整数"
+                min={0}
+                precision={0}
+                controls={false}
+              />
+            );
+          },
+        },
+        {
+          title: "操作",
+          width: 100,
+          align: "center",
+          valueType: "option",
+          render: (text, record, _, action) => {
+            return (
+              <a
+                key="editable"
+                onClick={() => {
+                  action?.startEditable?.(record._id);
+                }}
+              >
+                编辑
+              </a>
+            );
+          },
+        },
+      ]}
+      editable={{
+        type: "multiple",
+        editableKeys,
+        onChange: setEditableRowKeys,
+        actionRender: (row, config, defaultDoms) => {
+          return [defaultDoms.save, defaultDoms.delete];
+        },
+      }}
+      recordCreatorProps={{
+        type: "primary",
+        newRecordType: "dataSource",
+        creatorButtonText: "新增考察要点",
+        record: () => ({
+          _id: uniqueId("insepction"),
+          ...DEFAULT_INSPECTION_ITEM,
+        }),
+      }}
+    />
+  );
+});

Разница между файлами не показана из-за своего большого размера
+ 549 - 533
src/pages/Assessment/Index/CreateOrEdit/index.tsx


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

@@ -23,7 +23,7 @@ export const Container: FC<ContainerProps> = ({ currentId, type }) => {
       detail && isPoint && detail.jsonPoint
         ? JSON.parse(detail.jsonPoint)
         : null,
-    [detail]
+    [detail],
   );
   // 告警阈值
   const isWarn = detail?.isWarn === YES_OR_NO.YES;
@@ -35,7 +35,7 @@ export const Container: FC<ContainerProps> = ({ currentId, type }) => {
     let str = `${symbol?.label}${data.num}`;
 
     const connect = CONNECT_WITH_SYMBOL_OPTIONS.find(
-      (i) => i.value === data.and
+      (i) => i.value === data.and,
     );
     const symbol2 = SYMBOL_OPTIONS.find((i) => i.value === data.symbol2);
     if (connect && symbol2 && data.num2) {
@@ -123,7 +123,7 @@ export const Container: FC<ContainerProps> = ({ currentId, type }) => {
             </div>
           </div>
           <div className={style.tableItem}>
-            <p className={style.tableItemLabel}>指标说明</p>
+            <p className={style.tableItemLabel}>指标分值</p>
             <div className={style.tableItemInner}>
               <Table
                 className="cus-table"

+ 4 - 1
src/pages/Assessment/Template/CreateOrEdit/index.tsx

@@ -57,7 +57,7 @@ const CreateOrEditTemplate: FC = () => {
   useEffect(() => {
     setLoading(true);
     Promise.all(
-      isEdit ? [getDetail(), getAssIndexTree()] : [getAssIndexTree()]
+      isEdit ? [getDetail(), getAssIndexTree()] : [getAssIndexTree()],
     ).finally(() => {
       setLoading(false);
     });
@@ -70,6 +70,9 @@ const CreateOrEditTemplate: FC = () => {
           label="模板名称"
           required
           name="name"
+          normalize={(value) =>
+            typeof value === "string" ? value.trim() : value
+          }
           rules={[{ required: true, message: "请输入模板名称" }]}
         >
           <Input

+ 140 - 66
src/pages/AssessmentDetail/components/IndexAssessment/index.tsx

@@ -1,8 +1,11 @@
-import { FC, useEffect, useMemo, useState } from "react";
+import { FC, Key, useEffect, useMemo, useState } from "react";
 import { Button, Form, Input, Select, Table } from "antd";
 import { useParams } from "react-router-dom";
-import { getManageNormListApi, getReviewNormListApi } from "@/api";
-import { debounce } from "lodash";
+import {
+  getManageNormListApi,
+  getReviewNormListApi,
+  getManageAccessNormListApi,
+} from "@/api";
 import {
   DEPT_STATUS_ENUM,
   IManageFormDetail,
@@ -14,11 +17,14 @@ import { SelfReportScoreModal } from "../SelfReportScoreModal";
 import { ColumnsType } from "antd/es/table";
 import { IndexDetailModal } from "../IndexDetailModal";
 import { IndexDetailFormModal } from "../IndexDetailModal/form";
+import { isNull } from "lodash";
 
 export interface IndexAssessmentProps {
   detail: IManageIndexDetail | IManageFormDetail | null;
   disabled?: boolean;
+  /** 是否为考核填报 */
   isReportDetail?: boolean;
+  /** 是否为考核评定 */
   isEvalutionDetail?: boolean;
 }
 
@@ -27,33 +33,69 @@ const DEFAULT_PARAMS = {
   uploadStatus: undefined,
 };
 
+const collectExpandableKeys = (data: IManageNormItem[]): Key[] => {
+  const keys: Key[] = [];
+
+  const walk = (items: IManageNormItem[]) => {
+    items.forEach((item) => {
+      if (item.children?.length) {
+        keys.push(item.id);
+        walk(item.children);
+      }
+    });
+  };
+
+  walk(data);
+  return keys;
+};
+
+const buildSerialNumberMap = (data: IManageNormItem[], prefix = "") => {
+  const map = new Map<number, string>();
+
+  data.forEach((item, index) => {
+    const serial = prefix ? `${prefix}.${index + 1}` : String(index + 1);
+    map.set(item.id, serial);
+
+    if (item.children?.length) {
+      buildSerialNumberMap(item.children, serial).forEach((value, key) => {
+        map.set(key, value);
+      });
+    }
+  });
+
+  return map;
+};
+
 export const IndexAssessment: FC<IndexAssessmentProps> = ({
   detail,
   disabled,
-  isReportDetail,
   isEvalutionDetail,
+  isReportDetail,
 }) => {
   const routeParams = useParams(); // 待填报
   const canUpload = [
     DEPT_STATUS_ENUM.PENDING_SUBMIT,
     DEPT_STATUS_ENUM.RETURN,
   ].includes((detail as IManageFormDetail)?.deptStatus);
+  const [form] = Form.useForm();
   const [loading, setLoading] = useState(false);
   const [params, setParams] = useState<any>({
     ...DEFAULT_PARAMS,
   });
   const [list, setList] = useState<IManageNormItem[]>([]);
+  const [expandedRowKeys, setExpandedRowKeys] = useState<Key[]>([]);
   const [checkedItem, setCheckedItem] = useState<null | IManageNormItem>(null);
   const [selfReportVisible, setSelfReportVisible] = useState(false);
   const [indexDetailVisible, setIndexDetailVisible] = useState(false);
   const [indexDetailFormVisible, setIndexDetailFormVisible] = useState(false);
+  const serialNumberMap = useMemo(() => buildSerialNumberMap(list), [list]);
   const columns = useMemo(() => {
     const stack: ColumnsType<IManageNormItem> = [
       {
         title: "序号",
         align: "center",
-        minWidth: 80,
-        render: (val, record, index) => index + 1,
+        minWidth: 100,
+        render: (_, record) => serialNumberMap.get(record.id) ?? "",
       },
       {
         title: "标题",
@@ -76,29 +118,39 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
         title: "分值",
         align: "center",
         minWidth: 100,
-        render: (val) => val.score || <p className="empty-text">(空)</p>,
+        render: (val) => val.score ?? <p className="empty-text">(空)</p>,
       },
       {
         title: "资料上传 ",
         minWidth: 120,
         align: "center",
-        render: (val) => (
-          <Button
-            type="link"
-            onClick={() => {
-              setCheckedItem(val);
-              setIndexDetailVisible(true);
-            }}
-          >
-            查看
-          </Button>
-        ),
+        render: (val) =>
+          val.materialIds ? (
+            <Button
+              type="link"
+              onClick={() => {
+                setCheckedItem(val);
+                setIndexDetailVisible(true);
+              }}
+            >
+              查看
+            </Button>
+          ) : (
+            <p className="empty-text">(空)</p>
+          ),
       },
       {
         title: "上传状态",
         align: "center",
         minWidth: 120,
-        render: (val) => <p className="empty-text">(空)</p>,
+        render: (val) =>
+          !val.materialIds ? (
+            <p className="empty-text">(空)</p>
+          ) : val.uploadStatus === 1 ? (
+            <p style={{ color: "#52C41A" }}>已完成</p>
+          ) : (
+            <p style={{ color: "#FF4D4F" }}>未完成</p>
+          ),
       },
       {
         title: "打分点",
@@ -116,33 +168,36 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
         title: "操作",
         align: "center",
         hidden: !isEvalutionDetail,
-        render: (val) => (
-          <Button
-            type="link"
-            onClick={() => {
-              setCheckedItem(val);
-              setIndexDetailFormVisible(true);
-            }}
-          >
-            评定
-          </Button>
-        ),
+        render: (val) =>
+          (val.isPoint === YES_OR_NO.YES || Boolean(val.materialIds)) && (
+            <Button
+              type="link"
+              onClick={() => {
+                setCheckedItem(val);
+                setIndexDetailFormVisible(true);
+              }}
+            >
+              评定
+            </Button>
+          ),
       },
     ];
 
-    if (
-      isReportDetail &&
-      (detail as IManageFormDetail)?.isSelf === YES_OR_NO.YES
-    ) {
+    // 部门设置了自评权限显示该列
+    if ((detail as IManageFormDetail)?.isSelf === YES_OR_NO.YES) {
       stack.splice(
-        7,
+        stack.length - 1,
         0,
         {
           title: "自评得分",
           align: "center",
           minWidth: 120,
           render: (val) =>
-            !val.selfScore && canUpload && !disabled ? (
+            // 设为打分点或者有考察要点可以打分
+            val.isPoint === YES_OR_NO.YES &&
+            isReportDetail &&
+            canUpload &&
+            !disabled ? (
               <Button
                 type="text"
                 color="primary"
@@ -152,7 +207,7 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
                   setSelfReportVisible(true);
                 }}
               >
-                (空)
+                {val.selfScore || "(空)"}
               </Button>
             ) : (
               val.selfScore || <p className="empty-text">(空)</p>
@@ -162,14 +217,15 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
           title: "自评人",
           align: "center",
           minWidth: 100,
-          render: (val) => val.selfName || <p className="empty-text">(空)</p>,
-        }
+          render: (val) =>
+            val.selfUserName ?? <p className="empty-text">(空)</p>,
+        },
       );
     }
 
     if (isEvalutionDetail) {
       stack.splice(
-        stack.length - 2,
+        stack.length - 1,
         0,
         {
           title: "评定得分",
@@ -200,35 +256,51 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
               查看
             </Button>
           ),
-        }
+        },
       );
     }
 
     return stack;
-  }, [detail]);
+  }, [
+    detail,
+    serialNumberMap,
+    isEvalutionDetail,
+    isReportDetail,
+    canUpload,
+    disabled,
+  ]);
 
-  const getList = async () => {
+  const getList = async (searchParams = params) => {
     try {
       setLoading(true);
-      const data = await (isEvalutionDetail
-        ? getReviewNormListApi
-        : getManageNormListApi)({
-        deptId: routeParams.id as string,
-        ...params,
+      const data = await (
+        isEvalutionDetail
+          ? getReviewNormListApi
+          : isReportDetail
+            ? getManageNormListApi
+            : getManageAccessNormListApi
+      )({
+        deptId: routeParams.assessId || routeParams.id,
+        assessId: routeParams.id,
+        ...searchParams,
       });
       setList(data);
+      setExpandedRowKeys(collectExpandableKeys(data));
     } finally {
       setLoading(false);
     }
   };
 
-  const debounceSearch = useMemo(
-    () =>
-      debounce((changedVal: unknown, vals: any) => {
-        setParams({ ...params, ...vals });
-      }, 500),
-    [params]
-  );
+  const handleSearch = () => {
+    const vals = form.getFieldsValue();
+    const nextParams = Object.fromEntries(
+      Object.entries({ ...DEFAULT_PARAMS, ...vals }).filter(
+        ([, value]) => value !== undefined && value !== "",
+      ),
+    );
+    setParams(nextParams);
+    getList(nextParams);
+  };
 
   const handleSelfScoreCancel = () => {
     setCheckedItem(null);
@@ -241,7 +313,7 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
 
   return (
     <>
-      <Form layout="inline" onValuesChange={debounceSearch}>
+      <Form form={form} layout="inline" initialValues={DEFAULT_PARAMS}>
         <Form.Item label="搜索" name="searchKey">
           <Input allowClear className="w220" placeholder="请输入指标名称" />
         </Form.Item>
@@ -252,11 +324,11 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
                 allowClear
                 options={[
                   {
-                    label: "完成",
+                    label: "完成",
                     value: 1,
                   },
                   {
-                    label: "完成",
+                    label: "完成",
                     value: 2,
                   },
                 ]}
@@ -271,7 +343,7 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
           </div>
         </Form.Item> */}
         <Form.Item>
-          <Button type="primary" onClick={getList}>
+          <Button type="primary" onClick={handleSearch}>
             查询
           </Button>
         </Form.Item>
@@ -287,6 +359,10 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
           loading={loading}
           dataSource={list}
           columns={columns}
+          expandable={{
+            expandedRowKeys,
+            onExpandedRowsChange: (keys) => setExpandedRowKeys([...keys]),
+          }}
         />
       </div>
 
@@ -295,11 +371,11 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
           <IndexDetailModal
             readonly={!canUpload}
             open={indexDetailVisible}
-            id={checkedItem?.normId || 0}
-            moduleId={detail.id}
+            id={checkedItem?.id || 0}
             restUploadParams={{
               deptId: detail.id,
-              assessId: (detail as IManageFormDetail).accessId,
+              assessId: (detail as IManageFormDetail).assessId,
+              materialIds: checkedItem?.materialIds?.split(","),
             }}
             title={checkedItem?.name || ""}
             onCancel={() => {
@@ -310,15 +386,13 @@ export const IndexAssessment: FC<IndexAssessmentProps> = ({
 
           <IndexDetailFormModal
             open={indexDetailFormVisible}
-            id={checkedItem?.normId || 0}
-            deptNormId={checkedItem?.id || 0}
+            item={checkedItem}
             initScore={
               checkedItem?.opinionScore
                 ? Number(checkedItem?.opinionScore)
                 : undefined
             }
-            moduleId={detail.id}
-            title={checkedItem?.name || ""}
+            moduleId={(detail as IManageFormDetail).assessId || detail.id}
             showScoreInput={checkedItem?.isPoint === YES_OR_NO.YES}
             onOk={() => {
               getList();

+ 25 - 22
src/pages/AssessmentDetail/components/IndexDetailModal/form.tsx

@@ -1,7 +1,6 @@
-import { FC, Key, useEffect, useRef, useState } from "react";
+import { FC, Key, useEffect, useMemo, useRef, useState } from "react";
 import {
   Button,
-  Empty,
   Input,
   InputNumber,
   Modal,
@@ -9,7 +8,12 @@ import {
   Radio,
   Tag,
 } from "antd";
-import { MaterialType, REVIEW_MATERIAL_TYPE, YES_OR_NO } from "@/types";
+import {
+  IManageNormItem,
+  MaterialType,
+  REVIEW_MATERIAL_TYPE,
+  YES_OR_NO,
+} from "@/types";
 import { getBaseURL } from "@dage/service";
 import { getFileListApi, normEvaOpinionApi } from "@/api";
 import { downloadFile } from "@/utils";
@@ -23,9 +27,7 @@ import style from "./index.module.scss";
 import { REVIEW_MATERIAL_STATUS_MAP } from "@/constants";
 
 export interface IndexDetailFormModalProps extends Omit<ModalProps, "onOk"> {
-  id: number;
-  deptNormId: number;
-  title: string;
+  item: null | IManageNormItem;
   moduleId: number;
   /** 是否显示评定得分输入 */
   showScoreInput?: boolean;
@@ -38,9 +40,7 @@ const { TextArea } = Input;
 
 export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
   open,
-  id,
-  deptNormId,
-  title,
+  item,
   moduleId,
   showScoreInput,
   initScore,
@@ -54,6 +54,10 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
   const [btnLoading, setBtnLoading] = useState(false);
   const [list, setList] = useState<MaterialType[]>([]);
   const [editableKeys, setEditableKeys] = useState<Key[]>([]);
+  const maxScore = useMemo(
+    () => (item?.jsonPoint ? JSON.parse(item.jsonPoint).score : 50),
+    [item],
+  );
 
   const handleCancel = () => {
     onCancel?.();
@@ -62,11 +66,11 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
   const getList = async () => {
     try {
       setLoading(true);
-      const data = await getFileListApi(moduleId, "norm", id);
-      setList(data.filter((i) => i.module === "norm"));
+      const data = await getFileListApi(moduleId, "norm", item!.id);
+      setList(data.norm.filter((i) => i.module === "norm"));
 
       const temp: Record<number, MaterialType[]> = {};
-      data
+      data.fill
         .filter((i) => i.module === "fill-norm")
         .forEach((item) => {
           if (!item.parentId) return;
@@ -109,8 +113,8 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
       await normEvaOpinionApi({
         opinionScore: res.score,
         mats,
-        deptNormId,
-        deptId: moduleId,
+        deptNormId: item!.id,
+        deptId: item!.deptId!,
       });
       onOk?.();
     } finally {
@@ -128,10 +132,9 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
   return (
     <Modal
       className={style.modal}
-      title={title}
+      title={item?.name}
       open={open}
       width={1000}
-      footer={!list.length ? null : undefined}
       maskClosable={false}
       okText="提交"
       okButtonProps={{
@@ -143,7 +146,7 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
     >
       {loading && <DageLoading />}
 
-      {!list.length && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
+      {/* {!list.length && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />} */}
 
       <ProForm
         formRef={formRef}
@@ -151,7 +154,7 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
         submitter={false}
         style={{ marginTop: 30 }}
       >
-        {showScoreInput && Boolean(list.length) && (
+        {showScoreInput && (
           <ProForm.Item
             name="score"
             label="评定得分"
@@ -160,10 +163,10 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
           >
             <InputNumber
               min={0}
-              max={50}
+              max={maxScore}
               precision={1}
               className="w450"
-              placeholder="请输入0~50的数字,支持小数点后一位"
+              placeholder={`请输入0~${maxScore}的数字,支持小数点后一位`}
             />
           </ProForm.Item>
         )}
@@ -190,7 +193,7 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
                       baseUrl +
                         process.env.REACT_APP_IMG_PUBLIC +
                         item.filePath,
-                      item.fileName
+                      item.fileName,
                     )
                   }
                 >
@@ -302,7 +305,7 @@ export const IndexDetailFormModal: FC<IndexDetailFormModalProps> = ({
                                 baseUrl +
                                   process.env.REACT_APP_IMG_PUBLIC +
                                   item.filePath,
-                                item.fileName
+                                item.fileName,
                               )
                             }
                           >

+ 86 - 47
src/pages/AssessmentDetail/components/IndexDetailModal/index.tsx

@@ -1,9 +1,13 @@
-import { FC, useEffect, useState } from "react";
+import { FC, useEffect, useRef, useState } from "react";
 import { Button, Empty, Modal, ModalProps, Table, Tag } from "antd";
 import { MaterialType, REVIEW_MATERIAL_TYPE, YES_OR_NO } from "@/types";
 import { getBaseURL } from "@dage/service";
-import { deleteFileApi, getFileListApi, saveManageFileApi } from "@/api";
-import { downloadFile, beforeUpload } from "@/utils";
+import {
+  deleteFileApi,
+  getMaterialFileListApi,
+  saveManageFileApi,
+} from "@/api";
+import { downloadFile, beforeUpload, claimPendingUploadFiles, hasUploadingFiles, getUploadFileSaveKey } from "@/utils";
 import {
   DageFileResponseType,
   DageLoading,
@@ -18,9 +22,8 @@ import style from "./index.module.scss";
 import { REVIEW_MATERIAL_STATUS_MAP } from "@/constants";
 
 export interface IndexDetailModalProps extends Omit<ModalProps, "onOk"> {
+  // 指标id
   id: number;
-  /** 指标所属模块的id */
-  moduleId: number;
   title: string;
   readonly?: boolean;
   restUploadParams?: Record<string, unknown>;
@@ -31,7 +34,6 @@ export interface IndexDetailModalProps extends Omit<ModalProps, "onOk"> {
 export const IndexDetailModal: FC<IndexDetailModalProps> = ({
   open,
   id,
-  moduleId,
   title,
   readonly,
   restUploadParams,
@@ -46,6 +48,16 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
   const [uploadedFileMap, setUploadedFileMap] = useState<
     Record<number, MaterialType[]>
   >({});
+  // 记录已提交保存的文件 uid,避免 Upload 内部 fileList 累积导致重复提交
+  const savedFileKeysRef = useRef<Set<string>>(new Set());
+  const [uploadKeys, setUploadKeys] = useState<Record<number, number>>({});
+
+  const resetUpload = (parentId: number) => {
+    setUploadKeys((prev) => ({
+      ...prev,
+      [parentId]: (prev[parentId] || 0) + 1,
+    }));
+  };
 
   const handleCancel = () => {
     onCancel?.();
@@ -53,38 +65,51 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
 
   const saveManageFile = async (
     item: MaterialType,
-    list: DageFileResponseType[]
+    list: DageFileResponseType[],
   ) => {
-    // @ts-ignore
-    const li = list.filter((i) => i.status === "done" && !i.uploaded);
+    const pendingFiles = claimPendingUploadFiles(
+      item.id,
+      list,
+      savedFileKeysRef.current,
+    );
 
-    for (let i = 0; i < li.length; i++) {
-      const file = li[i];
+    if (!pendingFiles.length) return;
 
-      if (!file.response) return;
-
-      await saveManageFileApi({
-        name: file.name,
-        fileName: file.response.fileName,
-        filePath: file.response.filePath,
-        level: 2,
-        suffix: item.suffix,
-        parentId: item.id,
-        module: "fill-norm",
-        moduleId: id,
-        ...restUploadParams,
+    try {
+      for (const file of pendingFiles) {
+        await saveManageFileApi({
+          name: file.name,
+          fileName: file.response!.fileName,
+          filePath: file.response!.filePath,
+          level: 2,
+          suffix: item.suffix,
+          parentId: item.id,
+          module: "fill-norm",
+          moduleId: id,
+          ...restUploadParams,
+        });
+      }
+    } catch (error) {
+      pendingFiles.forEach((file) => {
+        savedFileKeysRef.current.delete(getUploadFileSaveKey(item.id, file));
       });
-      // @ts-ignore
-      file.uploaded = true;
+      throw error;
     }
 
-    getList();
+    await getList();
+
+    if (!hasUploadingFiles(list)) {
+      resetUpload(item.id);
+    }
   };
 
   const getList = async () => {
     try {
       setLoading(true);
-      const data = await getFileListApi(moduleId, "norm", id);
+      const data = await getMaterialFileListApi(
+        restUploadParams?.assessId as number,
+        restUploadParams?.materialIds as string[],
+      );
       setList(data.filter((i) => i.module === "norm"));
 
       const temp: typeof uploadedFileMap = {};
@@ -102,14 +127,19 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
     }
   };
 
-  const handleDeleteFile = async (id: number) => {
-    await deleteFileApi(id);
+  const handleDeleteFile = async (fileId: number, parentId: number) => {
+    await deleteFileApi(fileId);
+    resetUpload(parentId);
     getList();
   };
 
   useEffect(() => {
-    if (open) getList();
-  }, [open]);
+    if (open && restUploadParams?.materialIds) {
+      savedFileKeysRef.current.clear();
+      setUploadKeys({});
+      getList();
+    }
+  }, [open, restUploadParams?.materialIds]);
 
   return (
     <Modal
@@ -139,23 +169,28 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
                   {item.name} | {item.suffix || "*"}
                 </h3>
               </div>
-              <Button
-                type="primary"
-                ghost
-                onClick={() =>
-                  downloadFile(
-                    baseUrl + process.env.REACT_APP_IMG_PUBLIC + item.filePath,
-                    item.fileName
-                  )
-                }
-              >
-                下载模板
-              </Button>
+              {Boolean(item.filePath) && (
+                <Button
+                  type="primary"
+                  ghost
+                  onClick={() =>
+                    downloadFile(
+                      baseUrl +
+                        process.env.REACT_APP_IMG_PUBLIC +
+                        item.filePath,
+                      item.fileName,
+                    )
+                  }
+                >
+                  下载模板
+                </Button>
+              )}
               {!readonly && (
                 <DageUploadProvider>
                   <DageUploadConsumer>
                     {(res) => (
                       <DageUpload
+                        key={uploadKeys[item.id] || 0}
                         className={classNames(style.uploadBtn, "no-list")}
                         dType={DageUploadType.DOC}
                         action="/api/cms/fill/file/upload"
@@ -216,8 +251,8 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
                           val.status === REVIEW_MATERIAL_TYPE.PENDING
                             ? "default"
                             : val.status === REVIEW_MATERIAL_TYPE.PASS
-                            ? "success"
-                            : "error"
+                              ? "success"
+                              : "error"
                         }
                       >
                         {REVIEW_MATERIAL_STATUS_MAP[val.status]}
@@ -246,7 +281,7 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
                               baseUrl +
                                 process.env.REACT_APP_IMG_PUBLIC +
                                 item.filePath,
-                              item.fileName
+                              item.fileName,
                             )
                           }
                         >
@@ -255,7 +290,11 @@ export const IndexDetailModal: FC<IndexDetailModalProps> = ({
                       }
                       showEdit={false}
                       showDelete={!readonly}
-                      onDelete={handleDeleteFile.bind(undefined, val.id)}
+                      onDelete={handleDeleteFile.bind(
+                        undefined,
+                        val.id,
+                        item.id,
+                      )}
                     />
                   ),
                 },

+ 128 - 106
src/pages/AssessmentDetail/components/OverallAssessment/index.tsx

@@ -1,4 +1,4 @@
-import { Button, Form, Radio, Table, Tag } from "antd";
+import { Button, Empty, Form, Radio, Table, Tag } from "antd";
 import { FC, useEffect, useMemo, useRef, useState } from "react";
 import classNames from "classnames";
 import {
@@ -19,7 +19,7 @@ import {
   REVIEW_MATERIAL_TYPE,
   YES_OR_NO,
 } from "@/types";
-import { downloadFile, beforeUpload } from "@/utils";
+import { downloadFile, beforeUpload, claimPendingUploadFiles, hasUploadingFiles, getUploadFileSaveKey } from "@/utils";
 import { getBaseURL } from "@dage/service";
 import {
   additionalEvaOpinionApi,
@@ -34,7 +34,7 @@ import { EvaluationFormModal } from "../EvaluationFormModal";
 import { ColumnsType } from "antd/es/table";
 import { useSelector } from "react-redux";
 import { RootState } from "@/store";
-import { REVIEW_MATERIAL_STATUS_MAP } from "@/constants";
+import { DEPT_REVIEW_STATUS_MAP } from "@/constants";
 
 export interface OverallAssessmentProps {
   detail: IManageIndexDetail | IManageFormDetail | null;
@@ -54,7 +54,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
   refreshDetail,
 }) => {
   const { userInfo } = useSelector<RootState, RootState["base"]>(
-    (state) => state.base
+    (state) => state.base,
   );
   const baseUrl = getBaseURL();
   // 是否能够上传附件
@@ -91,6 +91,21 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
   const [uploadedFileMap, setUploadedFileMap] = useState<
     Record<number, MaterialType[]>
   >({});
+  // 记录已提交保存的文件 key,避免 Upload 内部 fileList 累积导致重复提交
+  const savedFileKeysRef = useRef<Set<string>>(new Set());
+  const [uploadKeys, setUploadKeys] = useState<Record<number, number>>({});
+
+  const resetUpload = (parentId: number) => {
+    setUploadKeys((prev) => ({
+      ...prev,
+      [parentId]: (prev[parentId] || 0) + 1,
+    }));
+  };
+  // 资料上传列表
+  const materials = useMemo(
+    () => detail?.materials?.filter((i) => i.module === "assess") || [],
+    [detail],
+  );
   const fileColumns = useMemo(() => {
     const stack: ColumnsType<MaterialType> = [
       {
@@ -132,7 +147,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
                     : "error"
                 }
               >
-                {REVIEW_MATERIAL_STATUS_MAP[item.status]}
+                {DEPT_REVIEW_STATUS_MAP[item.status]}
               </Tag>
             ) : (
               <Radio.Group
@@ -179,7 +194,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
                 填写
               </Button>
             ),
-        }
+        },
       );
     } else {
       stack.splice(2, 0, {
@@ -202,7 +217,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
                 onClick={() => {
                   downloadFile(
                     baseUrl + process.env.REACT_APP_IMG_PUBLIC + val.filePath,
-                    val.fileName
+                    val.fileName,
                   );
                 }}
               >
@@ -211,7 +226,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
             }
             showDelete={isReportDetail && canUpload}
             showEdit={false}
-            onDelete={handleDeleteFile.bind(undefined, val.id)}
+            onDelete={handleDeleteFile.bind(undefined, val.id, val.parentId!)}
           />
         ),
       });
@@ -254,12 +269,12 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
     try {
       setLoading(true);
       const data = await getFileListApi(
-        isIndexDetail ? detail!.id : (detail as IManageFormDetail).accessId,
+        (detail as IManageFormDetail).assessId,
         "assess",
-        detail!.id
+        detail!.id,
       );
       const temp: typeof uploadedFileMap = {};
-      data.forEach((item) => {
+      data.fill.forEach((item) => {
         if (!item.parentId) return;
         if (Array.isArray(temp[item.parentId])) temp[item.parentId].push(item);
         else temp[item.parentId] = [item];
@@ -272,37 +287,48 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
 
   const saveManageFile = async (
     item: MaterialType,
-    list: DageFileResponseType[]
+    list: DageFileResponseType[],
   ) => {
-    // @ts-ignore
-    const li = list.filter((i) => i.status === "done" && !i.uploaded);
-
-    for (let i = 0; i < li.length; i++) {
-      const file = li[i];
+    const pendingFiles = claimPendingUploadFiles(
+      item.id,
+      list,
+      savedFileKeysRef.current,
+    );
 
-      if (!file.response) return;
+    if (!pendingFiles.length) return;
 
-      await saveManageFileApi({
-        name: file.name,
-        fileName: file.response.fileName,
-        filePath: file.response.filePath,
-        level: 2,
-        suffix: item.suffix,
-        parentId: item.id,
-        module: "fill-assess",
-        moduleId: detail?.id,
-        deptId: detail?.id,
-        assessId: (detail as IManageFormDetail).accessId,
+    try {
+      for (const file of pendingFiles) {
+        await saveManageFileApi({
+          name: file.name,
+          fileName: file.response!.fileName,
+          filePath: file.response!.filePath,
+          level: 2,
+          suffix: item.suffix,
+          parentId: item.id,
+          module: "fill-assess",
+          moduleId: detail?.id,
+          deptId: detail?.id,
+          assessId: (detail as IManageFormDetail).assessId,
+        });
+      }
+    } catch (error) {
+      pendingFiles.forEach((file) => {
+        savedFileKeysRef.current.delete(getUploadFileSaveKey(item.id, file));
       });
-      // @ts-ignore
-      file.uploaded = true;
+      throw error;
     }
 
-    getList();
+    await getList();
+
+    if (!hasUploadingFiles(list)) {
+      resetUpload(item.id);
+    }
   };
 
-  const handleDeleteFile = async (id: number) => {
-    await deleteFileApi(id);
+  const handleDeleteFile = async (fileId: number, parentId: number) => {
+    await deleteFileApi(fileId);
+    resetUpload(parentId);
     getList();
   };
 
@@ -311,7 +337,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
 
     if (curEvaluationData.current.type === "assessment") {
       await assessmentEvaOpinionApi({
-        deptId: curEvaluationData.current.id,
+        assessId: curEvaluationData.current.id,
         ...val,
       });
     } else {
@@ -338,7 +364,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
       assessor: userInfo?.user.realName,
     });
     await additionalEvaOpinionApi({
-      assessId: (detail as IManageFormDetail).accessId,
+      assessId: (detail as IManageFormDetail).assessId,
       deptId: detail!.id,
       opinionJson: JSON.stringify(temp),
       type: checkedAddItem.current.id.indexOf("bonus") > -1 ? "add" : "sub",
@@ -348,6 +374,8 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
 
   useEffect(() => {
     if (!detail) return;
+    savedFileKeysRef.current.clear();
+    setUploadKeys({});
     getList();
   }, [detail]);
 
@@ -392,20 +420,20 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
                   title: "部门考核状态",
                   align: "center",
                   // @ts-ignore
-                  render: (val) => REVIEW_MATERIAL_STATUS_MAP[val.status],
+                  render: (val) => DEPT_REVIEW_STATUS_MAP[val.status],
                 },
               ]}
             />
           </Form.Item>
 
           <Form.Item label="评定意见">
-            {isEvalutionDetail ? (
+            {isEvalutionDetail && (
               <Button
                 type="primary"
                 onClick={() => {
                   curEvaluationData.current = {
                     type: "assessment",
-                    id: detail!.id,
+                    id: (detail as IManageFormDetail)!.assessId,
                   };
                   setInitEvaluationContent(detail!.opinion);
                   setEvaluationVisible(true);
@@ -413,38 +441,34 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
               >
                 填写
               </Button>
+            )}
+            {detail?.opinion ? (
+              <p style={{ marginTop: 10 }}>{detail.opinion}</p>
             ) : (
-              <p>
-                1、
-                工作中具有独立思考、不断创新的能力。对工作有高度的事业心和责任感,积极主动工作,认真履行自己的职责。
-                2、
-                该同志工作态度端正,对工作有高度的事业心和责任感,政治立场坚定,方向明确,忠诚并献身于党和人民的教育事业,治学严谨,热爱学生,以身作则,为人师表。
-                3、
-                该同志遵纪守法,具有良好的社会公德和家庭美德,有高尚的职业道德,关爱学生,面向全体学生,注重学生的全面发展,认真履行教师职务职责,有强烈的服务意识。
-                4、
-                该同志政治立场坚定,教育思想端正,专业知识及基本功扎实过硬,具有很强的表达能力,善于做思想政治工作。严格遵守学校的作息时间和工作纪律,全身心地投入工作。
-              </p>
+              <Empty />
             )}
           </Form.Item>
         </>
       )}
 
       <Form.Item label="资料上传">
-        {(detail?.materials?.filter((i) => i.module === "assess") || []).map(
-          (item) => {
-            const isUpload = item.isUpload === YES_OR_NO.YES;
+        {!materials.length && <Empty />}
+
+        {materials.map((item) => {
+          const isUpload = item.isUpload === YES_OR_NO.YES;
 
-            return (
-              <div key={item.id} className={style.fileUpload}>
-                <div className={style.fileUploadHeader}>
-                  <div>
-                    <Tag color={isUpload ? "red" : ""}>
-                      {isUpload ? "必填" : "选填"}
-                    </Tag>
-                    <h3>
-                      {item.name} | {item.suffix || "*"}
-                    </h3>
-                  </div>
+          return (
+            <div key={item.id} className={style.fileUpload}>
+              <div className={style.fileUploadHeader}>
+                <div>
+                  <Tag color={isUpload ? "red" : ""}>
+                    {isUpload ? "必填" : "选填"}
+                  </Tag>
+                  <h3>
+                    {item.name} | {item.suffix || "*"}
+                  </h3>
+                </div>
+                {Boolean(item.filePath) && (
                   <Button
                     type="primary"
                     ghost
@@ -453,55 +477,52 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
                         baseUrl +
                           process.env.REACT_APP_IMG_PUBLIC +
                           item.filePath,
-                        item.fileName
+                        item.fileName,
                       )
                     }
                   >
                     下载模板
                   </Button>
-                  {canUpload && !isEvalutionDetail && !disabled && (
-                    <DageUploadProvider>
-                      <DageUploadConsumer>
-                        {(res) => (
-                          <DageUpload
-                            className={classNames(style.uploadBtn, "no-list")}
-                            dType={DageUploadType.DOC}
-                            action="/api/cms/fill/file/upload"
-                            // @ts-ignore
-                            accept={item.suffix}
-                            // @ts-ignore
-                            beforeUpload={beforeUpload.bind(
-                              undefined,
-                              item.suffix
-                            )}
-                            onChange={saveManageFile.bind(undefined, item)}
-                          >
-                            <Button
-                              type="primary"
-                              ghost
-                              loading={res?.uploading}
-                            >
-                              上传附件
-                            </Button>
-                          </DageUpload>
-                        )}
-                      </DageUploadConsumer>
-                    </DageUploadProvider>
-                  )}
-                </div>
-
-                <Table
-                  rowKey="id"
-                  loading={loading}
-                  className="cus-table"
-                  pagination={false}
-                  dataSource={uploadedFileMap[item.id]}
-                  columns={fileColumns}
-                />
+                )}
+                {canUpload && !isEvalutionDetail && !disabled && (
+                  <DageUploadProvider>
+                    <DageUploadConsumer>
+                      {(res) => (
+                        <DageUpload
+                          key={uploadKeys[item.id] || 0}
+                          className={classNames(style.uploadBtn, "no-list")}
+                          dType={DageUploadType.DOC}
+                          action="/api/cms/fill/file/upload"
+                          // @ts-ignore
+                          accept={item.suffix}
+                          // @ts-ignore
+                          beforeUpload={beforeUpload.bind(
+                            undefined,
+                            item.suffix,
+                          )}
+                          onChange={saveManageFile.bind(undefined, item)}
+                        >
+                          <Button type="primary" ghost loading={res?.uploading}>
+                            上传附件
+                          </Button>
+                        </DageUpload>
+                      )}
+                    </DageUploadConsumer>
+                  </DageUploadProvider>
+                )}
               </div>
-            );
-          }
-        )}
+
+              <Table
+                rowKey="id"
+                loading={loading}
+                className="cus-table"
+                pagination={false}
+                dataSource={uploadedFileMap[item.id]}
+                columns={fileColumns}
+              />
+            </div>
+          );
+        })}
       </Form.Item>
 
       {!isReportDetail && (
@@ -551,6 +572,7 @@ export const OverallAssessment: FC<OverallAssessmentProps> = ({
 
       <SubEvaluationModal
         open={subEvaluationVisible}
+        item={checkedAddItem.current}
         onOk={handleAdditionalEvalution}
         onCancel={() => setSubEvaluationVisible(false)}
       />

+ 13 - 4
src/pages/AssessmentDetail/components/SelfReportScoreModal/index.tsx

@@ -1,4 +1,4 @@
-import { FC, useState } from "react";
+import { FC, useEffect, useMemo, useState } from "react";
 import { Form, Input, InputNumber, Modal, ModalProps } from "antd";
 import style from "@/components/AddIndexModal/index.module.scss";
 import { IManageNormItem } from "@/types";
@@ -19,6 +19,10 @@ export const SelfReportScoreModal: FC<SelfReportScoreModalProps> = ({
 }) => {
   const [form] = Form.useForm<any>();
   const [loading, setLoading] = useState(false);
+  const maxScore = useMemo(
+    () => (item?.jsonPoint ? JSON.parse(item.jsonPoint).score : 50),
+    [item],
+  );
 
   const handleCancel = () => {
     form.resetFields();
@@ -32,7 +36,7 @@ export const SelfReportScoreModal: FC<SelfReportScoreModalProps> = ({
   const handleSubmit = async (values: any) => {
     try {
       setLoading(true);
-      await saveSelfScoreApi(item!.id, values.score);
+      await saveSelfScoreApi(item!.fillId, values.score);
       form.resetFields();
       onOk?.();
     } finally {
@@ -40,8 +44,13 @@ export const SelfReportScoreModal: FC<SelfReportScoreModalProps> = ({
     }
   };
 
+  useEffect(() => {
+    form?.setFieldValue("score", item?.selfScore);
+  }, [item, form]);
+
   return (
     <Modal
+      forceRender
       className={style.modal}
       title={item?.name}
       okText="提交"
@@ -69,9 +78,9 @@ export const SelfReportScoreModal: FC<SelfReportScoreModalProps> = ({
           <InputNumber
             className="w100"
             min={0}
-            max={50}
+            max={maxScore}
             precision={1}
-            placeholder="请输入0~50的数字,支持小数点后一位"
+            placeholder={`请输入0~${maxScore}的数字,支持小数点后一位`}
           />
         </Form.Item>
       </Form>

+ 14 - 1
src/pages/AssessmentDetail/components/SubEvaluationModal/index.tsx

@@ -1,8 +1,9 @@
-import { FC, useState } from "react";
+import { FC, useEffect, useState } from "react";
 import { Form, Input, InputNumber, Modal, ModalProps } from "antd";
 import style from "@/components/AddIndexModal/index.module.scss";
 
 export interface SubEvaluationModalProps extends Omit<ModalProps, "onOk"> {
+  item: any;
   onCancel?: () => void;
   onOk?: (val: any) => void;
 }
@@ -11,6 +12,7 @@ const { TextArea } = Input;
 
 export const SubEvaluationModal: FC<SubEvaluationModalProps> = ({
   open,
+  item,
   onOk,
   onCancel,
   ...rest
@@ -37,6 +39,17 @@ export const SubEvaluationModal: FC<SubEvaluationModalProps> = ({
     }
   };
 
+  useEffect(() => {
+    if (open) {
+      form.setFieldsValue({
+        score: item.score,
+        comment: item.comment,
+      });
+    } else {
+      form.resetFields();
+    }
+  }, [open]);
+
   return (
     <Modal
       className={style.modal}

+ 22 - 16
src/pages/AssessmentDetail/index.tsx

@@ -21,11 +21,12 @@ import {
   IManageFormDetail,
   IManageIndexDetail,
   PUBLISH_ENUM,
-  YES_OR_NO,
 } from "@/types";
 import { DageLoading } from "@dage/pc-components";
 import { isNumber } from "lodash";
 import useApp from "antd/es/app/useApp";
+import { RootState } from "@/store";
+import { useSelector } from "react-redux";
 
 const AssessmentDetailPage: FC = () => {
   const app = useApp();
@@ -33,7 +34,10 @@ const AssessmentDetailPage: FC = () => {
   const isReportDetail = useRef(window.location.hash.indexOf("form") > -1);
   // 判断是否为考核评定进入
   const isEvalutionDetail = useRef(
-    window.location.hash.indexOf("evaluation") > -1
+    window.location.hash.indexOf("evaluation") > -1,
+  );
+  const { userInfo } = useSelector<RootState, RootState["base"]>(
+    (state) => state.base,
   );
   // 判断是否为考核管理进入
   const isIndexDetail = useRef(window.location.hash.indexOf("index") > -1);
@@ -56,7 +60,7 @@ const AssessmentDetailPage: FC = () => {
     : null;
   // 当前账号是否为该考核单负责人
   const isLeader =
-    (detail as IManageFormDetail)?.leaderUserId === YES_OR_NO.YES;
+    (detail as IManageFormDetail)?.leaderUserId === userInfo?.user.id;
   const disabled = status === PUBLISH_ENUM.ENDED;
 
   const getDetail = async () => {
@@ -67,12 +71,14 @@ const AssessmentDetailPage: FC = () => {
       if (isReportDetail.current) {
         data = await getManageFormDetailApi(
           params.id as string,
-          params.accessId as string
+          params.assessId as string,
         );
       } else {
-        data = await (isEvalutionDetail.current
-          ? getManageEvaluationDetailApi
-          : getManageIndexDetailApi)(params.id as string);
+        data = await (
+          isEvalutionDetail.current
+            ? getManageEvaluationDetailApi
+            : getManageIndexDetailApi
+        )(params.id as string);
       }
       setDetail(data);
     } finally {
@@ -105,7 +111,7 @@ const AssessmentDetailPage: FC = () => {
   const handleReviewSubmit = async () => {
     try {
       setReviewSubmitLoading(true);
-      await saveReviewApi((detail as IManageFormDetail).accessId);
+      await saveReviewApi((detail as IManageFormDetail).assessId);
       app.message.success("操作成功");
       navigate(-1);
     } finally {
@@ -116,7 +122,7 @@ const AssessmentDetailPage: FC = () => {
   const handleReviewRefund = async () => {
     try {
       setReviewRefundLoading(true);
-      await refundReviewApi((detail as IManageFormDetail).accessId, detail!.id);
+      await refundReviewApi((detail as IManageFormDetail).assessId, detail!.id);
       app.message.success("操作成功");
       navigate(-1);
     } finally {
@@ -204,7 +210,7 @@ const AssessmentDetailPage: FC = () => {
             {
               key: "1",
               label: "总体考核",
-              children: (
+              children: detail ? (
                 <OverallAssessment
                   detail={detail}
                   disabled={disabled}
@@ -213,19 +219,19 @@ const AssessmentDetailPage: FC = () => {
                   isEvalutionDetail={isEvalutionDetail.current}
                   refreshDetail={getDetail}
                 />
-              ),
+              ) : null,
             },
             {
               key: "2",
               label: "指标考核",
-              children: (
+              children: detail ? (
                 <IndexAssessment
                   detail={detail}
                   disabled={disabled}
                   isReportDetail={isReportDetail.current}
                   isEvalutionDetail={isEvalutionDetail.current}
                 />
-              ),
+              ) : null,
             },
           ]}
           onChange={(v) => setTab(v)}
@@ -249,7 +255,7 @@ const AssessmentDetailPage: FC = () => {
                 提交
               </Button>
             )}
-            <Button size="large" onClick={() => navigate(-1)}>
+            <Button size="large" onClick={() => navigate("/management/form")}>
               {disabled ? "关闭" : "保存并关闭"}
             </Button>
           </FormPageFooter>
@@ -266,7 +272,7 @@ const AssessmentDetailPage: FC = () => {
               type="primary"
               onClick={handleStatus.bind(
                 undefined,
-                DEPT_STATUS_ENUM.EXAMINE_SUCCESS
+                DEPT_STATUS_ENUM.EXAMINE_SUCCESS,
               )}
             >
               审核通过
@@ -277,7 +283,7 @@ const AssessmentDetailPage: FC = () => {
               color="danger"
               onClick={handleStatus.bind(
                 undefined,
-                DEPT_STATUS_ENUM.EXAMINE_REJECT
+                DEPT_STATUS_ENUM.EXAMINE_REJECT,
               )}
             >
               审核驳回

+ 11 - 4
src/pages/Layout/index.tsx

@@ -27,7 +27,7 @@ export default function CustomLayout() {
   const navigate = useNavigate();
   const location = useLocation();
   const baseStore = useSelector<RootState, RootState["base"]>(
-    (state) => state.base
+    (state) => state.base,
   );
   const [curMeta, setCurMeta] = useState<null | DageRouteItem["meta"]>(null);
   const [menuList, setMenuList] = useState<DageRouteItem[]>([]);
@@ -43,8 +43,15 @@ export default function CustomLayout() {
       const permissonIds = getAuthorizedIds(data.permission);
       const menus = filterAuthorizedRoutes(DEFAULT_MENU, permissonIds);
       setMenuList(menus);
-      const target = getFirstPath(menus);
-      target && navigate(target);
+
+      const currentPath = location.pathname;
+      const hasMatchedRoute =
+        currentPath !== "/" && !!findRouteByPath(menus, currentPath);
+
+      if (!hasMatchedRoute) {
+        const target = getFirstPath(menus);
+        target && navigate(target, { replace: true });
+      }
     } finally {
       setMenuLoading(false);
     }
@@ -147,7 +154,7 @@ export default function CustomLayout() {
                           path={menu.path}
                           Component={menu.Component}
                         />
-                      )
+                      ),
                     )}
                     <Route path="*" Component={NotFound} />
                   </Routes>

BIN
src/pages/Login/images/logo_black-min.png


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

@@ -144,7 +144,7 @@ export default function Login() {
               type="link"
               onClick={() => {
                 window.location.href = `https://m.canalmuseum.org.cn/oauth/authorize?client_id=b80b56f9852f45e5a4d52300194ed3f6&redirect_uri=${encodeURIComponent(
-                  location.href
+                  location.href,
                 )}&scope=userinfo`;
               }}
             >

+ 11 - 9
src/pages/Management/Evaluation/index.tsx

@@ -11,7 +11,7 @@ import {
 import style from "../Form/index.module.scss";
 import { debounce, isNumber } from "lodash";
 import { getManageEvaluationListApi } from "@/api";
-import { IManageFormItem, IManageFormListParams } from "@/types";
+import { IManageFormItem, IManageFormListParams, PUBLISH_ENUM } from "@/types";
 
 const DEFAULT_PARAMS: IManageFormListParams = {
   deptStatus: undefined,
@@ -35,8 +35,8 @@ const ManagementEvaluationPage = () => {
     setLoading(true);
     try {
       const data = await getManageEvaluationListApi(params);
-      setList(data.data);
-      setTotal(data.total);
+      setList(data.records ?? []);
+      setTotal(data.total ?? 0);
     } finally {
       setLoading(false);
     }
@@ -46,7 +46,7 @@ const ManagementEvaluationPage = () => {
     () => (pageNum: number, pageSize: number) => {
       setParams({ ...params, pageNum, pageSize });
     },
-    [params]
+    [params],
   );
 
   const debounceSearch = useMemo(
@@ -54,12 +54,12 @@ const ManagementEvaluationPage = () => {
       debounce((changedVal: unknown, vals: any) => {
         setParams({ ...params, ...vals });
       }, 500),
-    [params]
+    [params],
   );
 
   useEffect(() => {
     getList();
-  }, []);
+  }, [getList]);
 
   return (
     <PageContainer title="考核评定">
@@ -89,7 +89,9 @@ const ManagementEvaluationPage = () => {
                 <Select
                   allowClear
                   placeholder="请选择"
-                  options={PUBLISH_STATUS_OPTIONS}
+                  options={PUBLISH_STATUS_OPTIONS.filter(
+                    (item) => item.value !== PUBLISH_ENUM.PENDING,
+                  )}
                 />
               </Form.Item>
             </div>
@@ -121,7 +123,7 @@ const ManagementEvaluationPage = () => {
               minWidth: 100,
               render: (item: IManageFormItem) => {
                 return ASSESSMENT_TYPE_OPTIONS.find(
-                  (i) => i.value === item.type
+                  (i) => i.value === item.type,
                 )?.label;
               },
             },
@@ -164,7 +166,7 @@ const ManagementEvaluationPage = () => {
                   <Button
                     type="link"
                     onClick={() =>
-                      navigate(`/management/evaluation/detail/${item.id}`)
+                      navigate(`/management/evaluation/detail/${item.assessId}`)
                     }
                   >
                     查看

+ 5 - 5
src/pages/Management/Files/index.tsx

@@ -32,14 +32,14 @@ const ManagementReportPage = () => {
       debounce((changedVal: unknown, vals: any) => {
         setParams({ ...params, ...vals });
       }, 500),
-    [params]
+    [params],
   );
 
   const paginationChange = useCallback(
     () => (pageNum: number, pageSize: number) => {
       setParams({ ...params, pageNum, pageSize });
     },
-    [params]
+    [params],
   );
 
   const getList = useCallback(async () => {
@@ -58,14 +58,14 @@ const ManagementReportPage = () => {
       item.archive === ARCHIVE_TYPE.ARCHIVED
         ? ARCHIVE_TYPE.UNARCHIVED
         : ARCHIVE_TYPE.ARCHIVED,
-      item.id
+      item.id,
     );
     getList();
   };
 
   useEffect(() => {
     getList();
-  }, []);
+  }, [getList]);
 
   return (
     <PageContainer title="附件管理">
@@ -186,7 +186,7 @@ const ManagementReportPage = () => {
                           baseUrl +
                             process.env.REACT_APP_IMG_PUBLIC +
                             item.filePath,
-                          item.fileName
+                          item.fileName,
                         )
                       }
                     >

+ 8 - 8
src/pages/Management/Form/index.tsx

@@ -37,8 +37,8 @@ const ManagementReportPage = () => {
     setLoading(true);
     try {
       const data = await getManageFormListApi(params);
-      setList(data.data);
-      setTotal(data.total);
+      setList(data.records ?? []);
+      setTotal(data.total ?? 0);
     } finally {
       setLoading(false);
     }
@@ -48,7 +48,7 @@ const ManagementReportPage = () => {
     () => (pageNum: number, pageSize: number) => {
       setParams({ ...params, pageNum, pageSize });
     },
-    [params]
+    [params],
   );
 
   const debounceSearch = useMemo(
@@ -56,12 +56,12 @@ const ManagementReportPage = () => {
       debounce((changedVal: unknown, vals: any) => {
         setParams({ ...params, ...vals });
       }, 500),
-    [params]
+    [params],
   );
 
   useEffect(() => {
     getList();
-  }, []);
+  }, [getList]);
 
   return (
     <PageContainer title="考核填报">
@@ -135,7 +135,7 @@ const ManagementReportPage = () => {
               minWidth: 100,
               render: (item: IManageFormItem) => {
                 return ASSESSMENT_TYPE_OPTIONS.find(
-                  (i) => i.value === item.type
+                  (i) => i.value === item.type,
                 )?.label;
               },
             },
@@ -195,12 +195,12 @@ const ManagementReportPage = () => {
               align: "center",
               fixed: "right",
               render: (item: IManageFormItem) => {
-                return item.accessId ? (
+                return item.assessId ? (
                   <Button
                     type="link"
                     onClick={() =>
                       navigate(
-                        `/management/form/detail/${item.id}/${item.accessId}`
+                        `/management/form/detail/${item.id}/${item.assessId}`,
                       )
                     }
                   >

+ 80 - 6
src/pages/Management/Index/CreateOrEdit/index.tsx

@@ -1,5 +1,5 @@
 import { FC, Key, useEffect, useRef, useState } from "react";
-import { DatePicker, Form, Input, InputNumber } from "antd";
+import { DatePicker, Form, Input, InputNumber, Table } from "antd";
 import {
   EditableFormInstance,
   EditableProTable,
@@ -11,9 +11,15 @@ import { DEFAULT_BONUS_ITEM } from "../../../../constants";
 import { TableBonusType } from "../../types";
 import style from "./index.module.scss";
 import { dayjs, formatDate } from "@dage/utils";
-import { getManageIndexDetailApi, saveManageIndexApi } from "@/api";
+import {
+  getManageAssFixedListApi,
+  getManageAssOperationListApi,
+  getManageIndexDetailApi,
+  saveManageIndexApi,
+} from "@/api";
 import { DageLoading } from "@dage/pc-components";
-import { IManageIndexDetail } from "@/types";
+import { ASS_INDEX_TYPE, IManageIndexDetail } from "@/types";
+import { calculateAssessmentIndexScore } from "@/utils";
 
 const { TextArea } = Input;
 const { RangePicker } = DatePicker;
@@ -31,6 +37,10 @@ const CreateOrEditManagementIndex: FC = () => {
   // 减分项列表
   const deductionRef = useRef<EditableFormInstance<TableBonusType>>();
   const [deductionEditableKeys, setDeductionEditableKeys] = useState<Key[]>([]);
+  const isFixed = params.type === ASS_INDEX_TYPE.FIXED;
+  // 总分值
+  const [score, setScore] = useState(0);
+  const [museumScoreList, setMuseumScoreList] = useState<any[]>([]);
 
   const getDetail = async () => {
     try {
@@ -49,7 +59,7 @@ const CreateOrEditManagementIndex: FC = () => {
       }
       if (data.jsonSub) {
         setDeductionEditableKeys(
-          JSON.parse(data.jsonSub).map((i: any) => i.id)
+          JSON.parse(data.jsonSub).map((i: any) => i.id),
         );
       }
       setDetail(data);
@@ -83,8 +93,42 @@ const CreateOrEditManagementIndex: FC = () => {
     navigate(-1);
   };
 
+  const calculateScore = (list: any[] = []) => {
+    if (isFixed) {
+      setScore(calculateAssessmentIndexScore(list, "score"));
+      return;
+    }
+
+    setMuseumScoreList([
+      {
+        id: 1,
+        one: calculateAssessmentIndexScore(list, "one"),
+        two: calculateAssessmentIndexScore(list, "two"),
+        three: calculateAssessmentIndexScore(list, "three"),
+      },
+    ]);
+  };
+
+  const getList = async () => {
+    try {
+      if (isFixed) {
+        const data = await getManageAssFixedListApi(params.id!);
+
+        calculateScore(data);
+      } else {
+        const data = await getManageAssOperationListApi(params.id!);
+
+        calculateScore(data.list);
+      }
+    } finally {
+    }
+  };
+
   useEffect(() => {
-    isEdit && getDetail();
+    if (!isEdit) return;
+
+    getDetail();
+    getList();
   }, []);
 
   return (
@@ -98,7 +142,7 @@ const CreateOrEditManagementIndex: FC = () => {
             maxLength={20}
           />
         </Form.Item>
-        <Form.Item label="考核周期" required name="date">
+        <Form.Item label="考核周期" rules={[{ required: true }]} name="date">
           <RangePicker format="YYYY-MM-DD" />
         </Form.Item>
         <Form.Item label="说明" name="remark">
@@ -118,6 +162,36 @@ const CreateOrEditManagementIndex: FC = () => {
           />
         </Form.Item>
 
+        <Form.Item label="指标总分值">
+          {isFixed ? (
+            <span style={{ paddingLeft: 15 }}>{score}分</span>
+          ) : (
+            <Table
+              rowKey="id"
+              className="custom-pro-table mw650"
+              pagination={false}
+              dataSource={museumScoreList}
+              columns={[
+                {
+                  title: "一级博物馆",
+                  dataIndex: "one",
+                  align: "center",
+                },
+                {
+                  title: "二级博物馆",
+                  dataIndex: "two",
+                  align: "center",
+                },
+                {
+                  title: "三级博物馆",
+                  dataIndex: "three",
+                  align: "center",
+                },
+              ]}
+            />
+          )}
+        </Form.Item>
+
         <Form.Item label="附加项">
           <div className={style.buttonGroup}>
             <p className={style.tips}>

+ 51 - 12
src/pages/Management/Index/SettingIndex/index.tsx

@@ -10,7 +10,7 @@ import {
   Search,
 } from "@/components";
 // import { uniq, uniqBy } from "lodash";
-import { ASS_INDEX_TYPE } from "@/types";
+import { ASS_INDEX_TYPE, YES_OR_NO } from "@/types";
 import style from "./index.module.scss";
 import {
   getManageAssFixedListApi,
@@ -20,6 +20,8 @@ import {
   setManageAssOperationApi,
 } from "@/api";
 import { DageLoading } from "@dage/pc-components";
+import { FILL_TYPE_MAP } from "@/constants";
+import { calculateAssessmentIndexScore } from "@/utils";
 
 const SettingIndexPage: FC = () => {
   const params = useParams();
@@ -36,6 +38,32 @@ const SettingIndexPage: FC = () => {
   // 总分值
   const [score, setScore] = useState(0);
   const [museumScoreList, setMuseumScoreList] = useState<any[]>([]);
+  const [addIndexCheckedKeys, setAddIndexCheckedKeys] = useState<string[]>([]);
+
+  const calculateScore = (list: any[] = []) => {
+    if (isFixed) {
+      setScore(
+        calculateAssessmentIndexScore(
+          list.filter((i) => i.isAdd === YES_OR_NO.NO),
+          "score",
+        ),
+      );
+      return;
+    }
+
+    setMuseumScoreList([
+      {
+        id: 1,
+        one: calculateAssessmentIndexScore(list, "one"),
+        two: calculateAssessmentIndexScore(list, "two"),
+        three: calculateAssessmentIndexScore(list, "three"),
+      },
+    ]);
+  };
+
+  const handleCalculate = () => {
+    calculateScore(form.getFieldValue("list") || []);
+  };
 
   const getList = async (searchKey?: string) => {
     try {
@@ -45,17 +73,12 @@ const SettingIndexPage: FC = () => {
         const data = await getManageAssFixedListApi(params.id!, searchKey);
         form.setFieldValue("list", data);
 
-        if (!searchKey)
-          setScore(data.reduce((acc, currentVal) => acc + currentVal.score, 0));
+        if (!searchKey) calculateScore(data);
       } else {
         const data = await getManageAssOperationListApi(params.id!, searchKey);
-        setMuseumScoreList([
-          {
-            id: 1,
-            ...data.gist,
-          },
-        ]);
         form.setFieldValue("list", data.list);
+
+        if (!searchKey) calculateScore(data.list);
       }
     } finally {
       setLoading(false);
@@ -95,7 +118,7 @@ const SettingIndexPage: FC = () => {
   }, []);
 
   return (
-    <PageContainer title="设置指标">
+    <PageContainer title="设置指标" showBack>
       <Form
         labelCol={{ span: 4, offset: 3 }}
         form={form}
@@ -103,7 +126,7 @@ const SettingIndexPage: FC = () => {
         className={style.settingForm}
       >
         <Form.Item label="指标总分值">
-          <Button type="primary" onClick={() => getList()}>
+          <Button type="primary" onClick={handleCalculate}>
             计算
           </Button>
 
@@ -163,6 +186,17 @@ const SettingIndexPage: FC = () => {
                         dataIndex: "fill",
                         align: "center",
                         width: "230px",
+                        render: (_, row) =>
+                          String(row.fill || "")
+                            .split(",")
+                            .filter(Boolean)
+                            .map(
+                              (item) =>
+                                FILL_TYPE_MAP[
+                                  item.trim() as keyof typeof FILL_TYPE_MAP
+                                ] || item.trim(),
+                            )
+                            .join("、"),
                         // renderFormItem: () => (
                         //   <Radio.Group>
                         //     <Radio value="point">手动填报</Radio>
@@ -247,7 +281,11 @@ const SettingIndexPage: FC = () => {
                     type="primary"
                     icon={<PlusOutlined />}
                     style={{ background: "var(--second-color)" }}
-                    onClick={() => setIndexModalVisible(true)}
+                    onClick={() => {
+                      const list = (form.getFieldValue("list") || []) as any[];
+                      setAddIndexCheckedKeys(list.map((i) => i.normId));
+                      setIndexModalVisible(true);
+                    }}
                   >
                     新建指标
                   </Button>
@@ -285,6 +323,7 @@ const SettingIndexPage: FC = () => {
 
       <AddIndexModal
         open={indexModalVisible}
+        initialCheckedKeys={addIndexCheckedKeys}
         onOk={handleAddIndexItem}
         onCancel={() => setIndexModalVisible(false)}
       />

+ 2 - 2
src/pages/Management/Index/SettingRole/index.tsx

@@ -38,7 +38,7 @@ const SettingRole: FC = () => {
   const [groupList, setGroupList] = useState<IManageRoleGroupItem[]>([]);
   const [checkedItem, setCheckedItem] = useState<null | IManageDeptItem>(null);
   const [checkedGroup, setCheckedGroup] = useState<null | IManageRoleGroupItem>(
-    null
+    null,
   );
 
   // 获取责任部门列表
@@ -95,7 +95,7 @@ const SettingRole: FC = () => {
   }, []);
 
   return (
-    <PageContainer title="设置角色">
+    <PageContainer title="设置角色" showBack>
       <Pane
         title="责任部门"
         required

+ 15 - 4
src/pages/Management/Index/components/AddDeptModal/index.tsx

@@ -27,6 +27,15 @@ export const MUSEUM_LEVEL_LIST = [
 
 const { TextArea } = Input;
 
+const filterUserByRealName = (input: string, option?: IManageUserItem) =>
+  (option?.realName ?? "").toLowerCase().includes(input.trim().toLowerCase());
+
+const userSelectCommonProps = {
+  showSearch: true,
+  filterOption: filterUserByRealName,
+  fieldNames: { label: "realName", value: "id" as const },
+};
+
 export const AddDeptModal: FC<AddDeptModalProps> = ({
   assessId,
   open,
@@ -65,7 +74,7 @@ export const AddDeptModal: FC<AddDeptModalProps> = ({
       await saveManageRoleDeptApi({
         ...values,
         assessId,
-        crewUserIds: values.crewUserIds?.join(","),
+        crewUserIds: values.crewUserIds ? values.crewUserIds.join(",") : "",
         id: item?.id,
       });
       onOk?.();
@@ -80,7 +89,9 @@ export const AddDeptModal: FC<AddDeptModalProps> = ({
       !userList.length && getUserList();
 
       if (item) {
-        const crewUserIds = item.crewUserIds.split(",").map((i) => Number(i));
+        const crewUserIds = item.crewUserIds
+          ? item.crewUserIds.split(",").map((i) => Number(i))
+          : [];
         form.setFieldsValue({
           ...item,
           crewUserIds,
@@ -146,7 +157,7 @@ export const AddDeptModal: FC<AddDeptModalProps> = ({
             placeholder="请选择"
             loading={userLoading}
             options={userList}
-            fieldNames={{ label: "realName", value: "id" }}
+            {...userSelectCommonProps}
           />
         </Form.Item>
         <Form.Item label="部门成员" name="crewUserIds">
@@ -155,7 +166,7 @@ export const AddDeptModal: FC<AddDeptModalProps> = ({
             placeholder="请选择"
             loading={userLoading}
             options={userList}
-            fieldNames={{ label: "realName", value: "id" }}
+            {...userSelectCommonProps}
           />
         </Form.Item>
         <Form.Item label="自评权限" required>

+ 1 - 0
src/pages/Management/Index/components/AllocationOfDataModal/index.tsx

@@ -125,6 +125,7 @@ export const AllocationOfDataModal: FC<AllocationOfDataModalProps> = ({
             {
               title: "文件类型",
               editable: false,
+              width: 300,
               render: (node, item) => {
                 const arr = item.suffix?.split(",") || [];
                 return (

+ 6 - 4
src/pages/Management/Index/components/AllocationOfIndexDataModal/index.tsx

@@ -15,8 +15,10 @@ import {
 } from "@/types";
 import { saveManageDeptAllocationOfDataApi } from "@/api";
 
-export interface AllocationOfIndexDataModalProps
-  extends Omit<ModalProps, "onOk"> {
+export interface AllocationOfIndexDataModalProps extends Omit<
+  ModalProps,
+  "onOk"
+> {
   indexList: IAssIndexDetail[];
   deptList: IManageDeptItem[];
   onCancel?: () => void;
@@ -29,8 +31,8 @@ export const AllocationOfIndexDataModal: FC<
   const formRef = useRef<ProFormInstance<any>>();
   const [loading, setLoading] = useState(false);
   const list = useMemo(
-    () => indexList.filter((item) => Boolean(item.materials.length)),
-    [indexList]
+    () => indexList.filter((item) => Boolean(item.materials?.length)),
+    [indexList],
   );
 
   useEffect(() => {

+ 28 - 10
src/pages/Management/Index/components/AllocationOfIndexModal/index.tsx

@@ -1,4 +1,4 @@
-import { FC, Key, useEffect, useState } from "react";
+import { FC, Key, useEffect, useRef, useState } from "react";
 import { Checkbox, Empty, Form, Modal, ModalProps, Tree } from "antd";
 import {
   IManageDeptAllocationOfIndexItem,
@@ -36,7 +36,7 @@ export const AllocationOfIndexModal: FC<AllocationOfIndexModalProps> = ({
   const [form] = Form.useForm<any>();
   const [_checkedKeys, setCheckedKeys] = useState<Key[]>([]);
   const [treeData, setTreeData] = useState<IManageDeptAllocationOfIndexItem[]>(
-    []
+    [],
   );
   const [loading, setLoading] = useState(false);
   /** 仅查看尚未分配指标 */
@@ -44,6 +44,8 @@ export const AllocationOfIndexModal: FC<AllocationOfIndexModalProps> = ({
   const [indexList, setIndexList] = useState<IAssIndexDetail[]>([]);
   const [allocationOfIndexDataVisible, setAllocationOfIndexDataVisible] =
     useState(false);
+  const pendingNormIdsRef = useRef<Key[]>([]);
+  const pendingDeptIdRef = useRef<number>();
 
   const getDefaultCheckedKeys = (data: IManageDeptAllocationOfIndexItem[]) => {
     let keys: number[] = [];
@@ -76,12 +78,12 @@ export const AllocationOfIndexModal: FC<AllocationOfIndexModalProps> = ({
       setLoading(true);
       setCheckedKeys([]);
       setTreeData([]);
-      const data = await getManageDeptAllocationOfIndexListApi(
+      const data = await getManageDeptAllocationOfIndexListApi({
         assessId,
-        item?.id as number,
+        deptId: item?.id as number,
         type,
-        assign
-      );
+        assign,
+      });
       if (!assign) {
         const keys = getDefaultCheckedKeys(data);
         setTreeData(data);
@@ -108,19 +110,24 @@ export const AllocationOfIndexModal: FC<AllocationOfIndexModalProps> = ({
   };
 
   const handleSubmit = async (values: any) => {
+    const deptId = item?.id as number;
     const data = await checkManageIndexApi({
       assessId,
-      normIds: values.checkedKeys.join(","),
+      deptId,
+      normIds: values.checkedKeys,
     });
 
     if (!data.length) {
       await saveManageDeptAllocationOfIndexApi({
         assessId,
-        deptId: item?.id as number,
+        deptId,
         normIds: values.checkedKeys,
       });
+
       onOk?.();
     } else {
+      pendingNormIdsRef.current = values.checkedKeys;
+      pendingDeptIdRef.current = deptId;
       setIndexList(data);
       setAllocationOfIndexDataVisible(true);
     }
@@ -205,11 +212,22 @@ export const AllocationOfIndexModal: FC<AllocationOfIndexModalProps> = ({
         open={allocationOfIndexDataVisible}
         indexList={indexList}
         deptList={deptList}
-        onOk={() => {
+        onOk={async () => {
+          await saveManageDeptAllocationOfIndexApi({
+            assessId,
+            deptId: pendingDeptIdRef.current as number,
+            normIds: pendingNormIdsRef.current,
+          });
           onOk?.();
           setAllocationOfIndexDataVisible(false);
+          pendingNormIdsRef.current = [];
+          pendingDeptIdRef.current = undefined;
+        }}
+        onCancel={() => {
+          setAllocationOfIndexDataVisible(false);
+          pendingNormIdsRef.current = [];
+          pendingDeptIdRef.current = undefined;
         }}
-        onCancel={() => setAllocationOfIndexDataVisible(false)}
       />
     </>
   );

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

@@ -94,7 +94,7 @@ const ManagementIndexPage = () => {
 
   useEffect(() => {
     getList();
-  }, []);
+  }, [getList]);
 
   return (
     <PageContainer

+ 30 - 29
src/pages/User/RoleEdit.tsx

@@ -9,41 +9,46 @@ import { useNavigate, useParams } from "react-router-dom";
 
 const { TextArea } = Input;
 
+type CheckStatus = "full" | "half" | "none";
+
 const getCheckedIds = (treeData: PermItemType[], checkedIds: number[]) => {
   const fullyCheckedIds: number[] = [];
   const halfCheckedIds: number[] = [];
 
-  const checkNode = (node: PermItemType): boolean => {
-    let allChildrenChecked = true;
-    let someChildrenChecked = false;
+  const checkNode = (node: PermItemType): CheckStatus => {
+    const isNodeExplicitlyChecked = checkedIds.includes(node.id);
+    const children = node.children ?? [];
 
-    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;
+    if (children.length === 0) {
+      if (isNodeExplicitlyChecked) {
+        fullyCheckedIds.push(node.id);
+        return "full";
       }
+      return "none";
     }
 
-    const isNodeExplicitlyChecked = checkedIds.includes(node.id);
+    let allChildrenFull = true;
+    let someChildrenChecked = false;
+
+    for (const child of children) {
+      const status = checkNode(child);
+      if (status !== "full") allChildrenFull = false;
+      if (status !== "none") someChildrenChecked = true;
+    }
 
-    if (
-      isNodeExplicitlyChecked &&
-      (allChildrenChecked || node.children?.length === 0)
-    ) {
+    // 仅当所有子节点都是全选时,父节点才算全选
+    if (allChildrenFull && someChildrenChecked) {
       fullyCheckedIds.push(node.id);
-      return true;
+      return "full";
     }
 
-    if (
-      (someChildrenChecked && !isNodeExplicitlyChecked) ||
-      (isNodeExplicitlyChecked && !allChildrenChecked)
-    ) {
+    // 半选:部分子节点有选中,或自身在权限列表中但子节点未全部选中
+    if (someChildrenChecked || (isNodeExplicitlyChecked && !allChildrenFull)) {
       halfCheckedIds.push(node.id);
-      return true;
+      return "half";
     }
 
-    return isNodeExplicitlyChecked;
+    return "none";
   };
 
   treeData.forEach((node) => checkNode(node));
@@ -62,7 +67,8 @@ const RoleEditPage: FC = () => {
 
   const getTree = async () => {
     const data = await userApi.getPermTree();
-    setPermTree(data);
+    // 过滤掉系统管理菜单
+    setPermTree(data.filter((item) => item.id !== 400));
   };
 
   const getDetail = async () => {
@@ -135,16 +141,11 @@ const RoleEditPage: FC = () => {
               checkable
               // @ts-ignore
               treeData={permTree}
-              checkedKeys={{
-                checked: checkedKeys,
-                halfChecked: halfCheckedKeys,
-              }}
+              checkedKeys={checkedKeys}
               fieldNames={{ title: "name", key: "id" }}
-              onCheck={(keys, halfKeys) => {
+              onCheck={(keys, info) => {
                 setCheckedKeys(keys as number[]);
-                if (halfKeys.halfCheckedKeys) {
-                  setHalfCheckedKeys(halfKeys.halfCheckedKeys as number[]);
-                }
+                setHalfCheckedKeys((info.halfCheckedKeys as number[]) ?? []);
               }}
             />
           </div>

+ 18 - 14
src/router/index.tsx

@@ -26,7 +26,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/assessment/index/create/:type",
             title: "新增指标",
             Component: React.lazy(
-              () => import("../pages/Assessment/Index/CreateOrEdit")
+              () => import("../pages/Assessment/Index/CreateOrEdit"),
             ),
           },
           {
@@ -35,7 +35,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/assessment/index/edit/:type/:id",
             title: "编辑指标",
             Component: React.lazy(
-              () => import("../pages/Assessment/Index/CreateOrEdit")
+              () => import("../pages/Assessment/Index/CreateOrEdit"),
             ),
           },
         ],
@@ -52,7 +52,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/assessment/template/create/:type",
             title: "新增模板",
             Component: React.lazy(
-              () => import("../pages/Assessment/Template/CreateOrEdit")
+              () => import("../pages/Assessment/Template/CreateOrEdit"),
             ),
           },
           {
@@ -61,7 +61,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/assessment/template/edit/:type/:id",
             title: "编辑模板",
             Component: React.lazy(
-              () => import("../pages/Assessment/Template/CreateOrEdit")
+              () => import("../pages/Assessment/Template/CreateOrEdit"),
             ),
           },
         ],
@@ -87,7 +87,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/index/create/:type",
             title: "新增考核",
             Component: React.lazy(
-              () => import("../pages/Management/Index/CreateOrEdit")
+              () => import("../pages/Management/Index/CreateOrEdit"),
             ),
           },
           {
@@ -96,7 +96,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/index/edit/:type/:id",
             title: "编辑考核",
             Component: React.lazy(
-              () => import("../pages/Management/Index/CreateOrEdit")
+              () => import("../pages/Management/Index/CreateOrEdit"),
             ),
           },
           {
@@ -105,7 +105,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/index/setting-index/:type/:id",
             title: "设置指标",
             Component: React.lazy(
-              () => import("../pages/Management/Index/SettingIndex")
+              () => import("../pages/Management/Index/SettingIndex"),
             ),
           },
           {
@@ -114,7 +114,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/index/setting-role/:type/:id/:status",
             title: "设置角色",
             Component: React.lazy(
-              () => import("../pages/Management/Index/SettingRole")
+              () => import("../pages/Management/Index/SettingRole"),
             ),
           },
           {
@@ -133,7 +133,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/index/detail/:id/index",
             title: "考核指标详情",
             Component: React.lazy(
-              () => import("../pages/AssessmentDetail/IndexDetail")
+              () => import("../pages/AssessmentDetail/IndexDetail"),
             ),
           },
         ],
@@ -150,7 +150,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             meta: {
               custom: true,
             },
-            path: "/management/form/detail/:id/:accessId",
+            path: "/management/form/detail/:id/:assessId",
             title: "考核详情",
             Component: React.lazy(() => import("../pages/AssessmentDetail")),
           },
@@ -160,7 +160,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/form/detail/index",
             title: "考核指标详情",
             Component: React.lazy(
-              () => import("../pages/AssessmentDetail/IndexDetail")
+              () => import("../pages/AssessmentDetail/IndexDetail"),
             ),
           },
         ],
@@ -187,7 +187,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/management/evaluation/detail/index",
             title: "考核指标详情",
             Component: React.lazy(
-              () => import("../pages/AssessmentDetail/IndexDetail")
+              () => import("../pages/AssessmentDetail/IndexDetail"),
             ),
           },
         ],
@@ -224,7 +224,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/perfomance/form/create/:id",
             title: "新增报告",
             Component: React.lazy(
-              () => import("../pages/Performance/Form/Edit")
+              () => import("../pages/Performance/Form/Edit"),
             ),
           },
           {
@@ -232,7 +232,7 @@ export const DEFAULT_MENU: DageRouteItem[] = [
             path: "/perfomance/form/edit/:id",
             title: "编辑报告",
             Component: React.lazy(
-              () => import("../pages/Performance/Form/Edit")
+              () => import("../pages/Performance/Form/Edit"),
             ),
           },
         ],
@@ -243,16 +243,19 @@ export const DEFAULT_MENU: DageRouteItem[] = [
 
 export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
   {
+    mapId: 400,
     path: "/setting",
     title: "系统设置",
     icon: <Icon component={SettingIcon} />,
     children: [
       {
+        mapId: 410,
         path: "/setting/user",
         title: "用户管理",
         Component: React.lazy(() => import("../pages/User")),
       },
       {
+        mapId: 420,
         path: "/setting/role",
         title: "角色管理",
         Component: React.lazy(() => import("../pages/User/role")),
@@ -272,6 +275,7 @@ export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
         ],
       },
       {
+        mapId: 430,
         path: "/setting/log",
         title: "操作日志",
         Component: React.lazy(() => import("../pages/Log")),

+ 218 - 194
src/types/management.ts

@@ -1,194 +1,218 @@
-import { PaginationParams } from "@dage/service";
-import {
-  IFileTemplateFormParams,
-  MaterialType,
-  ModuleType,
-  YES_OR_NO,
-} from ".";
-import { ASS_INDEX_TYPE, IAssInspectionItem } from "./assessment";
-
-/**
- * 发布状态
- */
-export enum PUBLISH_ENUM {
-  PENDING = 0,
-  PUBLISHED = 1,
-  ENDED = 2,
-  EVALUATED = 3,
-}
-
-/**
- * 考核进度
- */
-export type PlanType = {
-  name: string;
-  pcsNorm: number;
-  pcsFill: number;
-  status: REVIEW_MATERIAL_TYPE;
-};
-
-export interface IManageIndexDetail {
-  id: number;
-  name: string;
-  remark: string;
-  dateEnd: string;
-  dateStart: string;
-  status: PUBLISH_ENUM;
-  score: null | number;
-  type: ASS_INDEX_TYPE;
-  updateTime: string;
-  creatorName: string;
-  createTime: string;
-  jsonAdd: string;
-  jsonSub: string;
-  materials: MaterialType[] | null;
-  plan: PlanType[];
-  opinion: string;
-}
-
-export interface IManageAssessmentIndex {
-  id: number;
-  score: number;
-  level: number;
-  isPoint: YES_OR_NO;
-  fill: string;
-  name: string;
-}
-
-/**
- * 自评状态
- */
-export enum DEPT_STATUS_ENUM {
-  /** 待填报 */
-  PENDING_SUBMIT = 0,
-  /** 待审核 */
-  PENDING_EXAMINE = 1,
-  /** 审核未通过 */
-  EXAMINE_REJECT = 2,
-  /** 审核通过(待评定) */
-  EXAMINE_SUCCESS = 3,
-  RETURN = 4,
-  SUCCESS = 5,
-}
-
-export interface IManageFormListParams extends PaginationParams {
-  deptStatus?: DEPT_STATUS_ENUM;
-  searchKey?: string;
-  status?: PUBLISH_ENUM;
-  type?: ASS_INDEX_TYPE;
-}
-
-export interface IManageFormItem {
-  id: number;
-  accessId: number;
-  name: string;
-  type: ASS_INDEX_TYPE;
-  remark: string;
-  dateStart: string;
-  dateEnd: string;
-  publishStatus: PUBLISH_ENUM;
-  deptName: string;
-  deptStatus: DEPT_STATUS_ENUM;
-}
-
-export interface IManageFormDetail extends Omit<IManageIndexDetail, "status"> {
-  deptStatus: DEPT_STATUS_ENUM;
-  publishStatus: PUBLISH_ENUM;
-  /** 是否开启自评 */
-  isSelf: YES_OR_NO;
-  deptName: string;
-  accessId: number;
-  /** 是否为部门主管 */
-  leaderUserId: YES_OR_NO;
-}
-
-export interface IManageDeptItem {
-  id: number;
-  name: string;
-  levelMuseum: number;
-  leaderName: string;
-  remark: string;
-  crewUserIds: string;
-  isSelf: YES_OR_NO;
-}
-
-export interface IManageUserItem {
-  id: number;
-  isAdmin: YES_OR_NO;
-  realName: string;
-}
-
-export interface IManageDeptMaterialItem
-  extends Required<IFileTemplateFormParams> {
-  id: number;
-  deptId: null | number;
-}
-
-export interface IManageDeptAllocationOfIndexItem {
-  perm: boolean;
-  id: number;
-  name: string;
-  parentId: number;
-  level: number;
-  disabled?: boolean;
-  children: IManageDeptAllocationOfIndexItem[];
-}
-
-export interface IManageRoleGroupItem {
-  id: number;
-  userName: string;
-  userIds: string;
-}
-
-export interface IManageAssOperationResponse {
-  gist: {
-    one: number;
-    two: number;
-    three: number;
-  };
-  list: ({
-    level: number;
-    weight: number;
-  } & IAssInspectionItem)[];
-}
-
-export interface ISaveManageFileParams {
-  fileName: string;
-  filePath: string;
-  level: number;
-  module: ModuleType;
-  moduleId?: number;
-  name: string;
-  suffix: string;
-  parentId?: number;
-  deptId?: number;
-  assessId?: number;
-}
-
-export interface IManageNormItem {
-  id: number;
-  normId: number;
-  name: string;
-  remark: string;
-  score: number | null;
-  selfScore: number | null;
-  isPoint: YES_OR_NO;
-  deptName: string;
-  opinionScore: string;
-}
-
-export enum WARNING_TYPE {
-  PUBLISH = 0,
-  STOP = 1,
-}
-
-export enum REVIEW_MATERIAL_TYPE {
-  PENDING = 0,
-  PASS = 1,
-  FAIL = 2,
-}
-
-export enum ARCHIVE_TYPE {
-  ARCHIVED = 1,
-  UNARCHIVED = 0,
-}
+import { PaginationParams } from "@dage/service";
+import {
+  IFileTemplateFormParams,
+  MaterialType,
+  ModuleType,
+  YES_OR_NO,
+} from ".";
+import { ASS_INDEX_TYPE, IAssInspectionItem } from "./assessment";
+
+/**
+ * 发布状态
+ */
+export enum PUBLISH_ENUM {
+  PENDING = 0,
+  PUBLISHED = 1,
+  ENDED = 2,
+  EVALUATED = 3,
+}
+
+/**
+ * 考核进度
+ */
+export type PlanType = {
+  name: string;
+  pcsNorm: number;
+  pcsFill: number;
+  status: REVIEW_MATERIAL_TYPE;
+};
+
+export interface IManageIndexDetail {
+  id: number;
+  name: string;
+  remark: string;
+  dateEnd: string;
+  dateStart: string;
+  status: PUBLISH_ENUM;
+  score: null | number;
+  type: ASS_INDEX_TYPE;
+  updateTime: string;
+  creatorName: string;
+  createTime: string;
+  jsonAdd: string;
+  jsonSub: string;
+  materials: MaterialType[] | null;
+  plan: PlanType[];
+  opinion: string;
+}
+
+export interface IManageAssessmentIndex {
+  id: number;
+  normId?: number;
+  score: number;
+  level: number;
+  isPoint: YES_OR_NO;
+  fill: string;
+  name: string;
+  weight?: number;
+  one?: number;
+  two?: number;
+  three?: number;
+  children?: IManageAssessmentIndex[];
+}
+
+/**
+ * 自评状态
+ */
+export enum DEPT_STATUS_ENUM {
+  /** 待填报 */
+  PENDING_SUBMIT = 0,
+  /** 待审核 */
+  PENDING_EXAMINE = 1,
+  /** 审核未通过 */
+  EXAMINE_REJECT = 2,
+  /** 审核通过(待评定) */
+  EXAMINE_SUCCESS = 3,
+  RETURN = 4,
+  SUCCESS = 5,
+}
+
+export interface IManageFormListParams extends PaginationParams {
+  deptStatus?: DEPT_STATUS_ENUM;
+  searchKey?: string;
+  status?: PUBLISH_ENUM;
+  type?: ASS_INDEX_TYPE;
+}
+
+export interface IManageFormItem {
+  id: number;
+  assessId: number;
+  name: string;
+  type: ASS_INDEX_TYPE;
+  remark: string;
+  dateStart: string;
+  dateEnd: string;
+  publishStatus: PUBLISH_ENUM;
+  deptName: string;
+  deptStatus: DEPT_STATUS_ENUM;
+}
+
+export interface IManageFormDetail extends Omit<IManageIndexDetail, "status"> {
+  deptStatus: DEPT_STATUS_ENUM;
+  publishStatus: PUBLISH_ENUM;
+  /** 是否开启自评 */
+  isSelf: YES_OR_NO;
+  deptName: string;
+  assessId: number;
+  /** 是否为部门主管 */
+  leaderUserId: YES_OR_NO;
+}
+
+export interface IManageDeptItem {
+  id: number;
+  name: string;
+  levelMuseum: number;
+  leaderName: string;
+  remark: string;
+  crewUserIds: string;
+  isSelf: YES_OR_NO;
+}
+
+export interface IManageUserItem {
+  id: number;
+  isAdmin: YES_OR_NO;
+  realName: string;
+}
+
+export interface IManageDeptMaterialItem extends Required<IFileTemplateFormParams> {
+  id: number;
+  deptId: null | number;
+}
+
+export interface IManageDeptAllocationOfIndexItem {
+  perm: boolean;
+  id: number;
+  name: string;
+  parentId: number;
+  level: number;
+  disabled?: boolean;
+  children: IManageDeptAllocationOfIndexItem[];
+}
+
+export interface IManageRoleGroupItem {
+  id: number;
+  userName: string;
+  userIds: string;
+}
+
+export interface IManageAssOperationResponse {
+  gist: {
+    one: number;
+    two: number;
+    three: number;
+  };
+  list: ({
+    level: number;
+    weight: number;
+  } & IAssInspectionItem)[];
+}
+
+export interface ISaveManageFileParams {
+  fileName: string;
+  filePath: string;
+  level: number;
+  module: ModuleType;
+  moduleId?: number;
+  name: string;
+  suffix: string;
+  parentId?: number;
+  deptId?: number;
+  assessId?: number;
+}
+
+export interface IManageNormItem {
+  id: number;
+  name: string;
+  remark: string;
+  jsonPoint: string;
+  score: number | null;
+  selfScore: number | null;
+  selfUserName: string | null;
+  isPoint: YES_OR_NO;
+  deptName: string;
+  deptId?: number;
+  fillId: number;
+  opinionScore: string;
+  type: ASS_INDEX_TYPE;
+  materialIds?: string;
+  uploadStatus?: number;
+  selfName?: string;
+  opinionName?: string;
+  children?: IManageNormItem[];
+}
+
+export enum WARNING_TYPE {
+  PUBLISH = 0,
+  STOP = 1,
+}
+
+export enum REVIEW_MATERIAL_TYPE {
+  PENDING = 0,
+  PASS = 1,
+  FAIL = 2,
+  RATED = 3,
+}
+
+export enum DEPT_REVIEW_STATUS_ENUM {
+  EMPTY = 0,
+  PENDING = 1,
+  FAIL = 2,
+  PASS = 3,
+  REJECT = 4,
+  RATED = 5,
+}
+
+export enum ARCHIVE_TYPE {
+  ARCHIVED = 1,
+  UNARCHIVED = 0,
+}

+ 221 - 101
src/utils/index.ts

@@ -1,101 +1,221 @@
-import { removeTokenInfo } from "@dage/pc-components";
-import { logoutApi } from "@/api";
-import { Key } from "react";
-import { AssIndexTreeItemType, PermItemType } from "@/types";
-import { RcFile } from "antd/es/upload";
-import { message } from "antd";
-
-export const logout = async () => {
-  await logoutApi();
-
-  removeTokenInfo();
-  globalThis.location.href = "#/login";
-};
-
-export const getSelectedNodes = (
-  selectedKeys: Key[],
-  treeData: AssIndexTreeItemType[],
-  // 任意节点
-  any = false
-) => {
-  let selectedNodes: AssIndexTreeItemType[] = [];
-
-  const findNodes = (keys: Key[], data: AssIndexTreeItemType[]) => {
-    data.forEach((node) => {
-      if (keys.includes(node.id) && (any ? true : !node.children?.length)) {
-        selectedNodes.push(node);
-      }
-      if (node.children) {
-        findNodes(keys, node.children);
-      }
-    });
-  };
-
-  findNodes(selectedKeys, treeData);
-  return selectedNodes;
-};
-
-export const getImgFullPath = (path: string) =>
-  `${process.env.REACT_APP_API_URL}${process.env.REACT_APP_IMG_PUBLIC}${path}`;
-
-export const downloadFile = async (url: string, name: string) => {
-  try {
-    const response = await fetch(url);
-    const blob = await response.blob();
-    const newUrl = window.URL.createObjectURL(blob);
-    const link = document.createElement("a");
-    link.href = newUrl;
-    link.download = name;
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
-    window.URL.revokeObjectURL(url);
-  } catch (error) {
-    console.error("下载失败:", error);
-  }
-};
-
-export const beforeUpload = (suffix: string, file: RcFile) => {
-  const arr = suffix.split(",");
-
-  if (!arr.length) return true;
-  const result = arr.findIndex((i) => file.name.indexOf(i) > -1) > -1;
-  if (!result) {
-    message.error("选择的文件类型不正确!");
-  }
-  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[];
-};
+import { removeTokenInfo, DageFileResponseType } from "@dage/pc-components";
+import { logoutApi } from "@/api";
+import { Key } from "react";
+import { AssIndexTreeItemType, PermItemType } from "@/types";
+import { message } from "antd";
+import { RcFile } from "antd/es/upload";
+import Upload from "antd/es/upload";
+
+export const logout = async () => {
+  await logoutApi();
+
+  removeTokenInfo();
+  globalThis.location.href = "#/login";
+};
+
+export const getSelectedNodes = (
+  selectedKeys: Key[],
+  treeData: AssIndexTreeItemType[],
+  // 任意节点
+  any = false,
+) => {
+  let selectedNodes: AssIndexTreeItemType[] = [];
+
+  const findNodes = (keys: Key[], data: AssIndexTreeItemType[]) => {
+    data.forEach((node) => {
+      if (keys.includes(node.id) && (any ? true : !node.children?.length)) {
+        selectedNodes.push(node);
+      }
+      if (node.children) {
+        findNodes(keys, node.children);
+      }
+    });
+  };
+
+  findNodes(selectedKeys, treeData);
+  return selectedNodes;
+};
+
+export const getImgFullPath = (path: string) =>
+  `${process.env.REACT_APP_API_URL}${process.env.REACT_APP_IMG_PUBLIC}${path}`;
+
+export const downloadFile = async (url: string, name: string) => {
+  try {
+    const response = await fetch(url);
+    const blob = await response.blob();
+    const newUrl = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = newUrl;
+    link.download = name;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+  } catch (error) {
+    console.error("下载失败:", error);
+  }
+};
+
+export const beforeUpload = (suffix: string, file: RcFile) => {
+  const arr = suffix.split(",");
+
+  if (!arr.length) return true;
+  const result = arr.findIndex((i) => file.name.indexOf(i) > -1) > -1;
+  if (!result) {
+    message.error("选择的文件类型不正确!");
+  }
+  return result;
+};
+
+const MAX_UPLOAD_FILE_SIZE = 5 * 1024 * 1024;
+
+export const beforeUploadFileSize = (
+  file: RcFile,
+  maxSize = MAX_UPLOAD_FILE_SIZE,
+) => {
+  // if (file.size > maxSize) {
+  //   message.error("文件大小不能超过5MB");
+  //   return Upload.LIST_IGNORE;
+  // }
+  return true;
+};
+
+export const getUploadFileSaveKey = (
+  scopeId: number | string,
+  file: DageFileResponseType,
+) => {
+  if (file.response?.filePath) {
+    return `${scopeId}-${file.response.filePath}`;
+  }
+  return `${scopeId}-${file.uid}`;
+};
+
+/** 同步认领待保存文件,避免并发 onChange 重复提交 */
+export const claimPendingUploadFiles = (
+  scopeId: number | string,
+  list: DageFileResponseType[],
+  savedKeys: Set<string>,
+) => {
+  const pendingFiles: DageFileResponseType[] = [];
+
+  list.forEach((file) => {
+    if (file.status !== "done" || !file.response) return;
+
+    const key = getUploadFileSaveKey(scopeId, file);
+    if (savedKeys.has(key)) return;
+
+    savedKeys.add(key);
+    pendingFiles.push(file);
+  });
+
+  return pendingFiles;
+};
+
+export const hasUploadingFiles = (list: DageFileResponseType[]) =>
+  list.some((file) => file.status === "uploading");
+
+export interface AssessmentIndexScoreNode {
+  level?: number;
+  score?: number;
+  weight?: number;
+  one?: number;
+  two?: number;
+  three?: number;
+  children?: AssessmentIndexScoreNode[];
+  [key: string]: unknown;
+}
+
+const getAssessmentIndexWeight = (node: AssessmentIndexScoreNode) =>
+  node.weight && node.weight > 0 ? node.weight / 100 : 1;
+
+/** 将平铺列表按 level 还原为树结构 */
+export const buildAssessmentIndexTree = <T extends AssessmentIndexScoreNode>(
+  list: T[],
+): T[] => {
+  if (!list?.length) return [];
+  if (list.some((item) => item.children?.length)) return list;
+
+  const roots: T[] = [];
+  const parentStack: T[] = [];
+
+  list.forEach((item) => {
+    const node = { ...item, children: [] as T[] };
+    const level = item.level ?? 1;
+
+    while (
+      parentStack.length &&
+      (parentStack[parentStack.length - 1].level ?? 1) >= level
+    ) {
+      parentStack.pop();
+    }
+
+    if (!parentStack.length) {
+      roots.push(node);
+    } else {
+      const parent = parentStack[parentStack.length - 1];
+      parent.children = parent.children || [];
+      parent.children.push(node);
+    }
+
+    parentStack.push(node);
+  });
+
+  return roots;
+};
+
+/** 平级字段求和后,依次乘以父级 weight(百分比) */
+export const calculateAssessmentIndexScore = (
+  list: AssessmentIndexScoreNode[],
+  dataIndex: string,
+): number => {
+  const calculateBranch = (node: AssessmentIndexScoreNode): number => {
+    if (node.children?.length) {
+      const peerSum = node.children.reduce(
+        (sum, child) => sum + calculateBranch(child),
+        0,
+      );
+      return peerSum * getAssessmentIndexWeight(node);
+    }
+
+    return Number(node[dataIndex]) || 0;
+  };
+
+  return buildAssessmentIndexTree(list).reduce(
+    (sum, node) => sum + calculateBranch(node),
+    0,
+  );
+};
+
+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[];
+};