فهرست منبع

feat: questionaire

chenlei 1 سال پیش
والد
کامیت
67cff28646

+ 1 - 2
.gitignore

@@ -9,8 +9,7 @@ lerna-debug.log*
 
 node_modules
 .DS_Store
-dist
-dist-ssr
+build
 coverage
 *.local
 

+ 2 - 1
package.json

@@ -72,6 +72,7 @@
     "tailwindcss": "^3.0.2",
     "terser-webpack-plugin": "^5.2.5",
     "typescript": "^4.9.5",
+    "uuid": "^9.0.1",
     "web-vitals": "^2.1.4",
     "webpack": "^5.64.4",
     "webpack-dev-server": "^4.6.0",
@@ -80,7 +81,7 @@
   },
   "scripts": {
     "start": "cross-env REACT_APP_API_URL=http://192.168.20.61:8059 REACT_APP_IMG_PUBLIC=/api node scripts/start.js",
-    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-shgybwg.4dage.com REACT_APP_IMG_PUBLIC= node scripts/build.js"
+    "build": "cross-env PUBLIC_URL=./ REACT_APP_API_URL=https://sit-qiushoubwg.4dage.com REACT_APP_IMG_PUBLIC= node scripts/build.js"
   },
   "eslintConfig": {
     "extends": [

+ 8 - 0
pnpm-lock.yaml

@@ -212,6 +212,9 @@ dependencies:
   typescript:
     specifier: ^4.9.5
     version: 4.9.5
+  uuid:
+    specifier: ^9.0.1
+    version: 9.0.1
   web-vitals:
     specifier: ^2.1.4
     version: 2.1.4
@@ -11031,6 +11034,11 @@ packages:
     hasBin: true
     dev: false
 
+  /uuid@9.0.1:
+    resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+    hasBin: true
+    dev: false
+
   /v8-compile-cache@2.4.0:
     resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==}
     dev: false

+ 1 - 0
src/api/index.ts

@@ -20,3 +20,4 @@ export * from "./information";
 export * from "./exhibition";
 export * from "./collection";
 export * from "./message";
+export * from "./questionaire";

+ 28 - 0
src/api/questionaire.ts

@@ -0,0 +1,28 @@
+import { requestByGet, requestByPost } from "@dage/service";
+
+export const questionaireApi = {
+  getList(data: any) {
+    return requestByPost("/api/cms/questionnaire/pageList", data);
+  },
+  delete(id: number) {
+    return requestByGet(`/api/cms/questionnaire/remove/${id}`);
+  },
+  getDetail(id: string) {
+    return requestByGet(`/api/cms/questionnaire/detail/${id}`);
+  },
+  save(data: any) {
+    return requestByPost("/api/cms/questionnaire/save", data);
+  },
+  getQuestions(id: string) {
+    return requestByGet(`/api/cms/question/getList/${id}`);
+  },
+  deleteQuestion(id: number) {
+    return requestByGet(`/api/cms/question/remove/${id}`);
+  },
+  saveQuestion(data: any) {
+    return requestByPost("/api/cms/question/save", data);
+  },
+  saveQuestions(data: any[]) {
+    return requestByPost("/api/cms/question/saveBatch", data);
+  },
+};

+ 119 - 37
src/pages/Questionnaire/components/topicDrawer.tsx

@@ -7,30 +7,64 @@ import {
   Radio,
   Space,
   Switch,
+  message,
 } from "antd";
-import { FC, useRef, useState } from "react";
+// @ts-ignore
+import { v4 as uuidv4 } from "uuid";
+import { FC, useEffect, useRef, useState } from "react";
+
+export type QuestionType = {
+  id: number;
+  answer: string;
+  hasDiy: boolean;
+  question: string;
+  type: number;
+  _isNew?: boolean;
+  _isEdit?: boolean;
+};
 
 export interface TopicDrawerProps {
   open: boolean;
+  item: QuestionType | null;
   close: () => void;
+  add: (params: QuestionType) => void;
+  edit: (params: QuestionType) => void;
 }
 
-let id = 0;
-
-export const TopicDrawer: FC<TopicDrawerProps> = ({ open, close }) => {
+export const TopicDrawer: FC<TopicDrawerProps> = ({
+  item,
+  open,
+  add,
+  close,
+  edit,
+}) => {
+  const [loading, setLoading] = useState(false);
   const optionsFormRef = useRef<FormInstance>(null);
+  const basicFormRef = useRef<FormInstance>(null);
 
   const [options, setOptions] = useState<
     {
-      id: number;
+      val: string;
+      name?: string;
     }[]
   >([]);
 
+  useEffect(() => {
+    if (item) {
+      const { answer, ...rest } = item;
+      basicFormRef.current?.setFieldsValue(rest);
+
+      setOptions(JSON.parse(answer).answer);
+    } else {
+      handleReset();
+    }
+  }, [item]);
+
   const handleAddOption = () => {
     setOptions((prev) => [
       ...prev,
       {
-        id: id++,
+        val: uuidv4(),
       },
     ]);
   };
@@ -42,16 +76,52 @@ export const TopicDrawer: FC<TopicDrawerProps> = ({ open, close }) => {
     });
   };
 
+  const handleReset = () => {
+    setOptions([]);
+    basicFormRef.current?.resetFields();
+  };
+
   const handleSubmit = async () => {
-    if (!(await optionsFormRef.current?.validateFields())) return;
+    if (!(await basicFormRef.current?.validateFields())) return;
+    if (!options.length || !(await optionsFormRef.current?.validateFields())) {
+      message.error("选项不能为空");
+      return;
+    }
 
-    const options = optionsFormRef.current?.getFieldsValue();
-    console.log(options);
+    const vals = optionsFormRef.current?.getFieldsValue();
+    const params: QuestionType = {
+      ...basicFormRef.current?.getFieldsValue(),
+      answer: JSON.stringify({
+        answer: Object.keys(vals).map((val) => ({
+          val,
+          name: vals[val],
+        })),
+      }),
+    };
+
+    if (!item) {
+      add({ ...params, _isNew: true, id: new Date().getTime() });
+    } else {
+      try {
+        setLoading(true);
+        edit({
+          ...params,
+          id: item.id,
+          _isEdit: !item._isNew,
+        });
+      } finally {
+        setLoading(false);
+      }
+    }
+
+    close();
+
+    handleReset();
   };
 
   return (
     <Drawer
-      title="新增题目"
+      title={item ? "编辑题目" : "新增题目"}
       placement="right"
       width={500}
       open={open}
@@ -59,16 +129,20 @@ export const TopicDrawer: FC<TopicDrawerProps> = ({ open, close }) => {
       extra={
         <Space>
           <Button onClick={close}>取消</Button>
-          <Button type="primary" onClick={handleSubmit}>
+          <Button loading={loading} type="primary" onClick={handleSubmit}>
             保存
           </Button>
         </Space>
       }
     >
-      <Form layout="vertical">
+      <Form
+        ref={basicFormRef}
+        layout="vertical"
+        initialValues={{ type: 1, hasDiy: false }}
+      >
         <Form.Item
           label="题目描述"
-          name="title"
+          name="question"
           rules={[{ required: true, message: "请输入内容" }]}
         >
           <Input.TextArea
@@ -81,12 +155,12 @@ export const TopicDrawer: FC<TopicDrawerProps> = ({ open, close }) => {
 
         <Form.Item label="类型" name="type">
           <Radio.Group buttonStyle="solid">
-            <Radio.Button value="1">单选</Radio.Button>
-            <Radio.Button value="2">多选</Radio.Button>
+            <Radio.Button value={1}>单选</Radio.Button>
+            <Radio.Button value={2}>多选</Radio.Button>
           </Radio.Group>
         </Form.Item>
 
-        <Form.Item label="允许自定义答案" name="cus">
+        <Form.Item label="允许自定义答案" name="hasDiy" valuePropName="checked">
           <Switch />
         </Form.Item>
       </Form>
@@ -102,29 +176,37 @@ export const TopicDrawer: FC<TopicDrawerProps> = ({ open, close }) => {
         </Button>
 
         {options.map((i, idx) => (
-          <Form.Item
-            key={i.id}
-            label={`选项${idx + 1}`}
-            name={`option_${i.id}`}
-            rules={[{ required: true, message: "请输入" }]}
+          <div
+            key={i.val}
+            style={{
+              display: "flex",
+              alignItems: "center",
+            }}
           >
-            <div
-              style={{
-                display: "flex",
-                alignItems: "center",
-              }}
+            <Form.Item
+              label={`选项${idx + 1}`}
+              name={i.val}
+              initialValue={i.name}
+              rules={[{ required: true, message: "请输入" }]}
+              style={{ flex: 1 }}
+            >
+              <Input
+                placeholder="请输入"
+                autoComplete="off"
+                style={{ marginRight: "10px" }}
+              />
+            </Form.Item>
+
+            <Button
+              type="text"
+              danger
+              size="small"
+              style={{ marginBottom: 24 }}
+              onClick={handleRemoveOption.bind(undefined, idx)}
             >
-              <Input placeholder="请输入" style={{ marginRight: "10px" }} />
-              <Button
-                type="text"
-                danger
-                size="small"
-                onClick={handleRemoveOption.bind(undefined, idx)}
-              >
-                删除
-              </Button>
-            </div>
-          </Form.Item>
+              删除
+            </Button>
+          </div>
         ))}
       </Form>
     </Drawer>

+ 196 - 32
src/pages/Questionnaire/create-or-edit/index.tsx

@@ -1,15 +1,63 @@
 import { FormPageFooter, MemoSpinLoding } from "@/components";
-import { Button, DatePicker, Form, FormInstance, Input, Select } from "antd";
-import { useCallback, useRef, useState } from "react";
+import {
+  App,
+  Button,
+  DatePicker,
+  Form,
+  FormInstance,
+  Input,
+  Switch,
+  Table,
+} from "antd";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import { useNavigate, useParams } from "react-router-dom";
-import { TopicDrawer } from "../components/topicDrawer";
+import { QuestionType, TopicDrawer } from "../components/topicDrawer";
+import { dayjs, formatDate } from "@dage/utils";
+import { questionaireApi } from "@/api";
+import { DageTableActions } from "@dage/pc-components";
 
 export default function QuestionnaireCreateOrEditPage() {
   const formRef = useRef<FormInstance>(null);
   const navigate = useNavigate();
   const params = useParams();
+  const { message } = App.useApp();
   const [loading, setLoading] = useState(false);
+  const [optsLoading, setOptsLoading] = useState(false);
   const [drawerVisible, setDrawerVisible] = useState(false);
+  const [list, setList] = useState<QuestionType[]>([]);
+  const [editQuestion, setEditQuestion] = useState<QuestionType | null>(null);
+
+  const getList = useCallback(async () => {
+    try {
+      setOptsLoading(true);
+      const data = await questionaireApi.getQuestions(params.id as string);
+      setList(data);
+    } finally {
+      setOptsLoading(false);
+    }
+  }, [params.id]);
+
+  const getDetail = useCallback(async () => {
+    setLoading(true);
+    try {
+      const { publishDate, ...rest } = await questionaireApi.getDetail(
+        params.id as string
+      );
+
+      formRef.current?.setFieldsValue({
+        publishDate: dayjs(publishDate),
+        ...rest,
+      });
+
+      await getList();
+    } finally {
+      setLoading(false);
+    }
+  }, [params.id, getList]);
+
+  useEffect(() => {
+    !!params.id && getDetail();
+  }, [getDetail, params.id]);
 
   const handleCancel = useCallback(() => {
     navigate(-1);
@@ -18,16 +66,51 @@ export default function QuestionnaireCreateOrEditPage() {
   const handleSubmit = useCallback(async () => {
     if (!(await formRef.current?.validateFields())) return;
 
-    const { ...rest } = formRef.current?.getFieldsValue();
+    if (!list.length) {
+      message.error("题目不能为空");
+      return Promise.reject(false);
+    }
+
+    const { publishDate, display, ...rest } = formRef.current?.getFieldsValue();
 
     if (params.id) {
       rest.id = params.id;
     }
 
+    const data = await questionaireApi.save({
+      ...rest,
+      display: display ? 1 : 0,
+      publishDate: formatDate(publishDate),
+    });
+
+    const newList = list
+      .filter((i) => i._isNew)
+      .map((i) => ({
+        ...i,
+        id: undefined,
+        _isNew: undefined,
+        _isEdit: undefined,
+        questionnaireId: data.id,
+      }));
+
+    const editList = list
+      .filter((i) => i._isEdit)
+      .map((i) => ({
+        ...i,
+        _isNew: undefined,
+        _isEdit: undefined,
+      }));
+
+    if (editList.length || newList.length) {
+      // @ts-ignore
+      await questionaireApi.saveQuestions(editList.concat(newList));
+    }
+
     handleCancel();
-  }, [handleCancel, params]);
+  }, [handleCancel, params, list, message]);
 
   const handleAddTopic = () => {
+    setEditQuestion(null);
     setDrawerVisible(true);
   };
 
@@ -35,14 +118,89 @@ export default function QuestionnaireCreateOrEditPage() {
     setDrawerVisible(false);
   };
 
+  const handleDelete = useCallback(
+    async (item: QuestionType, idx: number) => {
+      const temp = [...list];
+
+      setList((list) => {
+        const newList = [...list];
+        newList.splice(idx, 1);
+        return newList;
+      });
+
+      if (!item._isNew) {
+        try {
+          await questionaireApi.deleteQuestion(item.id);
+        } catch (err) {
+          setList(temp);
+        }
+      }
+    },
+    [list]
+  );
+
+  const handleEditQues = useCallback((item: QuestionType) => {
+    setEditQuestion(item);
+    setDrawerVisible(true);
+  }, []);
+
+  const COLUMNS = useMemo(() => {
+    return [
+      {
+        title: "题目序号",
+        render: (text: any, record: any, index: number) => {
+          return <span>{index + 1}</span>;
+        },
+      },
+      {
+        title: "题目描述",
+        dataIndex: "question",
+      },
+      {
+        title: "选项",
+        dataIndex: "answer",
+      },
+      {
+        title: "类型",
+        render: (item: QuestionType) => {
+          return <span>{item.type === 1 ? "单选" : "多选"}</span>;
+        },
+      },
+      {
+        title: "允许自定义答案",
+        render: (item: QuestionType) => {
+          return <span>{item.hasDiy ? "是" : "否"}</span>;
+        },
+      },
+      {
+        title: "操作",
+        render: (idx: number, item: QuestionType) => {
+          return (
+            <DageTableActions
+              onEdit={handleEditQues.bind(undefined, item)}
+              onDelete={handleDelete.bind(undefined, item, idx)}
+            />
+          );
+        },
+      },
+    ];
+  }, [handleDelete, handleEditQues]);
+
   return (
-    <div style={{ position: "relative" }}>
+    <App style={{ position: "relative" }}>
       {loading && <MemoSpinLoding />}
 
-      <Form ref={formRef} labelCol={{ span: 2 }}>
+      <Form
+        ref={formRef}
+        labelCol={{ span: 2 }}
+        initialValues={{
+          display: 1,
+          publishDate: dayjs(),
+        }}
+      >
         <Form.Item
           label="标题"
-          name="title"
+          name="name"
           rules={[{ required: true, message: "请输入内容" }]}
         >
           <Input
@@ -53,11 +211,7 @@ export default function QuestionnaireCreateOrEditPage() {
           />
         </Form.Item>
 
-        <Form.Item
-          label="摘要"
-          name="content"
-          rules={[{ required: true, message: "请输入内容" }]}
-        >
+        <Form.Item label="摘要" name="description">
           <Input.TextArea
             className="w450"
             placeholder="请输入内容,最多200字"
@@ -67,35 +221,30 @@ export default function QuestionnaireCreateOrEditPage() {
           />
         </Form.Item>
 
-        <Form.Item
-          label="题目"
-          name="content"
-          rules={[{ required: true, message: "请新增题目" }]}
-        >
+        <Form.Item label="题目" required>
           <Button type="primary" onClick={handleAddTopic}>
             新增
           </Button>
+
+          <Table
+            loading={optsLoading}
+            className="page-table"
+            dataSource={list}
+            columns={COLUMNS}
+            rowKey="id"
+          />
         </Form.Item>
 
         <Form.Item
           label="发布日期"
-          name="date"
+          name="publishDate"
           rules={[{ required: true, message: "请选择日期" }]}
         >
           <DatePicker className="w220" />
         </Form.Item>
 
-        <Form.Item label="展示状态" name="type">
-          <Select
-            defaultValue="lucy"
-            style={{ width: 220 }}
-            options={[
-              { value: "jack", label: "Jack" },
-              { value: "lucy", label: "Lucy" },
-              { value: "Yiminghe", label: "yiminghe" },
-              { value: "disabled", label: "Disabled", disabled: true },
-            ]}
-          />
+        <Form.Item label="展示状态" name="display" valuePropName="checked">
+          <Switch />
         </Form.Item>
       </Form>
 
@@ -103,7 +252,22 @@ export default function QuestionnaireCreateOrEditPage() {
         <FormPageFooter onSubmit={handleSubmit} onCancel={handleCancel} />
       )}
 
-      <TopicDrawer open={drawerVisible} close={handleCloseDrawer} />
-    </div>
+      <TopicDrawer
+        item={editQuestion}
+        open={drawerVisible}
+        close={handleCloseDrawer}
+        add={(val) => setList((list) => [...list, val])}
+        edit={(val) => {
+          const index = list.findIndex((i) => i.id === val.id);
+          if (index > -1) {
+            setList((list) => {
+              const newList = [...list];
+              newList.splice(index, 1, val);
+              return newList;
+            });
+          }
+        }}
+      />
+    </App>
   );
 }

+ 62 - 14
src/pages/Questionnaire/index.tsx

@@ -1,62 +1,105 @@
-import { Button, Form, FormInstance, Input, Radio, Select, Table } from "antd";
-import { useCallback, useMemo, useRef, useState } from "react";
+import { Button, Form, FormInstance, Input, Table } from "antd";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import { debounce } from "lodash";
 import { DageTableActions } from "@dage/pc-components";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { questionaireApi } from "@/api";
 
 const DEFAULT_PARAMS = {
   pageNum: 1,
   pageSize: 20,
+  searchKey: "",
 };
 
 export default function InformationPage() {
   const navigate = useNavigate();
+  const [searchParams, setSearchParams] = useSearchParams();
   const formRef = useRef<FormInstance>(null);
   const [loading, setLoading] = useState(false);
   const [list, setList] = useState<[]>([]);
   const [params, setParams] = useState({
     ...DEFAULT_PARAMS,
+    searchKey: searchParams.get("searchKey") || "",
   });
   const [total, setTotal] = useState(0);
 
+  useEffect(() => {
+    setSearchParams({
+      searchKey: params.searchKey,
+    });
+  }, [params.searchKey, setSearchParams]);
+
+  const getList = useCallback(async () => {
+    setLoading(true);
+    try {
+      const data = await questionaireApi.getList(params);
+      setList(data.records);
+      setTotal(data.total);
+    } finally {
+      setLoading(false);
+    }
+  }, [params]);
+
+  useEffect(() => {
+    getList();
+  }, [getList]);
+
+  const handleDelete = useCallback(
+    async (id: number) => {
+      await questionaireApi.delete(id);
+      getList();
+    },
+    [getList]
+  );
+
   const COLUMNS = useMemo(() => {
     return [
       {
         title: "标题",
-        dataIndex: "title",
+        dataIndex: "name",
       },
       {
         title: "问题数量",
-        dataIndex: "nickName",
+        dataIndex: "pcsQuestion",
       },
       {
         title: "已收集份数",
-        dataIndex: "date",
+        dataIndex: "pcsGather",
       },
       {
         title: "发布日期",
-        dataIndex: "date",
+        dataIndex: "publishDate",
       },
       {
         title: "展示状态",
-        dataIndex: "date",
+        render: (item: any) => {
+          return item.display ? "展示" : "不展示";
+        },
       },
       {
         title: "操作",
         render: (item: any) => {
-          return <DageTableActions onEdit={() => {}} onDelete={() => {}} />;
+          return (
+            <DageTableActions
+              onEdit={() => navigate(`/questionnaire/edit/${item.id}`)}
+              onDelete={handleDelete.bind(undefined, item.id)}
+            />
+          );
         },
       },
     ];
-  }, []);
+  }, [navigate, handleDelete]);
 
   const handleReset = useCallback(() => {
     formRef.current?.resetFields();
   }, [formRef]);
 
   const debounceSearch = useMemo(
-    () => debounce((changedVal: unknown, vals: any) => {}, 500),
-    []
+    () =>
+      debounce((changedVal: unknown, vals: any) => {
+        setParams({ ...params, ...vals });
+      }, 500),
+    [params]
   );
 
   const paginationChange = useCallback(
@@ -68,8 +111,13 @@ export default function InformationPage() {
 
   return (
     <div className="information">
-      <Form ref={formRef} layout="inline" onValuesChange={debounceSearch}>
-        <Form.Item label="搜索项" name="stage">
+      <Form
+        ref={formRef}
+        layout="inline"
+        initialValues={params}
+        onValuesChange={debounceSearch}
+      >
+        <Form.Item label="搜索项" name="searchKey">
           <Input
             placeholder="请输入标题,最多10字"
             maxLength={10}

+ 8 - 0
src/router/index.tsx

@@ -105,6 +105,14 @@ export const DEFAULT_ADMIN_MENU: DageRouteItem[] = [
           () => import("../pages/Questionnaire/create-or-edit")
         ),
       },
+      {
+        path: "/questionnaire/edit/:id",
+        title: "编辑",
+        hide: true,
+        Component: React.lazy(
+          () => import("../pages/Questionnaire/create-or-edit")
+        ),
+      },
     ],
   },
   {