Browse Source

feat: add dict update and delete

RegMs If 3 years ago
parent
commit
38b66fea87

+ 1 - 4
src/apis/AuthProvider.tsx

@@ -27,10 +27,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
       {initialized ? (
         children
       ) : (
-        <Result
-          icon={<Loading3QuartersOutlined spin />}
-          subTitle="Loading..."
-        />
+        <Result icon={<Loading3QuartersOutlined spin />} subTitle="加载中..." />
       )}
     </AuthContext.Provider>
   );

+ 37 - 6
src/apis/dicts.ts

@@ -8,9 +8,9 @@ import type { WordResult } from './words';
 export interface DictResult {
   id: number;
   name: string;
-  value: string;
-  meaning: string;
-  extra: string;
+  valueTitle: string;
+  meaningTitle: string;
+  extraTitle: string;
   wordCount: number;
 }
 
@@ -55,9 +55,9 @@ export const useGetDict = (request: GetDictRequest) =>
 
 interface CreateDictRequest {
   name: string;
-  value: string;
-  meaning: string;
-  extra: string;
+  valueTitle: string;
+  meaningTitle: string;
+  extraTitle: string;
 }
 
 export const useCreateDict = () => async (request: CreateDictRequest) =>
@@ -69,3 +69,34 @@ export const useCreateDict = () => async (request: CreateDictRequest) =>
       )
     ).data,
   );
+
+interface UpdateDictRequest {
+  id: number;
+  name: string;
+  valueTitle: string;
+  meaningTitle: string;
+  extraTitle: string;
+}
+
+export const useUpdateDict = () => async (request: UpdateDictRequest) =>
+  unwrap(
+    (
+      await instance.put<Response<DictResult>>(
+        '/dict/update',
+        qs.stringify(request),
+      )
+    ).data,
+  );
+
+interface DeleteDictRequest {
+  id: number;
+}
+
+export const useDeleteDict = () => async (request: DeleteDictRequest) =>
+  unwrap(
+    (
+      await instance.delete<Response<null>>(
+        `/dict/delete?${qs.stringify(request)}`,
+      )
+    ).data,
+  );

+ 36 - 14
src/components/DictDrawer.tsx

@@ -1,38 +1,60 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { Drawer, Form, Input, Button, message } from 'antd';
 import type { DrawerProps } from 'antd';
-import { useCreateDict } from '../apis/dicts';
+import type { DictResult } from '../apis/dicts';
+import { useCreateDict, useUpdateDict } from '../apis/dicts';
 
 interface DictDrawerProps extends DrawerProps {
+  initialDict?: DictResult;
   refresh: () => void;
   onClose: () => void;
 }
 
-const DictDrawer: React.FC<DictDrawerProps> = ({ refresh, ...props }) => {
+const DictDrawer: React.FC<DictDrawerProps> = ({
+  initialDict,
+  refresh,
+  ...props
+}) => {
+  const [loading, setLoading] = useState(false);
+
   const createDict = useCreateDict();
+  const updateDict = useUpdateDict();
 
   return (
-    <Drawer title="创建新词库" destroyOnClose {...props}>
+    <Drawer
+      title={initialDict ? '更新词库' : '创建新词库'}
+      destroyOnClose
+      {...props}
+    >
       <Form
         name="dict"
         labelCol={{ span: 6 }}
         wrapperCol={{ span: 18 }}
+        initialValues={initialDict}
         onFinish={(values: {
           name: string;
-          value: string;
-          meaning: string;
-          extra: string;
+          valueTitle: string;
+          meaningTitle: string;
+          extraTitle: string;
         }) =>
           void (async () => {
             try {
-              await createDict(values);
-              void message.success('创建成功');
+              setLoading(true);
+              if (initialDict) {
+                await updateDict({ ...initialDict, ...values });
+                void message.success('更新成功');
+              } else {
+                await createDict(values);
+                void message.success('创建成功');
+              }
               refresh();
               props.onClose();
             } catch (err) {
               if (err instanceof Error) {
                 void message.error(err.message);
               }
+            } finally {
+              setLoading(false);
             }
           })()
         }
@@ -47,7 +69,7 @@ const DictDrawer: React.FC<DictDrawerProps> = ({ refresh, ...props }) => {
         </Form.Item>
         <Form.Item
           label="单词标题"
-          name="value"
+          name="valueTitle"
           rules={[{ required: true, message: '请输入单词标题' }]}
           validateTrigger="onBlur"
         >
@@ -55,18 +77,18 @@ const DictDrawer: React.FC<DictDrawerProps> = ({ refresh, ...props }) => {
         </Form.Item>
         <Form.Item
           label="词义标题"
-          name="meaning"
+          name="meaningTitle"
           rules={[{ required: true, message: '请输入词义标题' }]}
           validateTrigger="onBlur"
         >
           <Input placeholder="请输入词义标题(如:中文)" />
         </Form.Item>
-        <Form.Item label="附加标题" name="extra">
+        <Form.Item label="附加标题" name="extraTitle">
           <Input placeholder="请输入附加标题(如:音标/假名)" />
         </Form.Item>
         <Form.Item label=" " colon={false}>
-          <Button htmlType="submit" type="primary">
-            创建
+          <Button htmlType="submit" type="primary" loading={loading}>
+            {initialDict ? '更新' : '创建'}
           </Button>
         </Form.Item>
       </Form>

+ 17 - 19
src/components/UserDrawer.tsx

@@ -15,6 +15,7 @@ interface UserDrawerProps extends DrawerProps {
 
 const UserDrawer: React.FC<UserDrawerProps> = props => {
   const [mode, setMode] = useState<Mode>(Mode.Login);
+  const [loading, setLoading] = useState(false);
 
   const { refresh } = useAuth();
   const register = useRegister();
@@ -33,6 +34,7 @@ const UserDrawer: React.FC<UserDrawerProps> = props => {
         onFinish={(values: { name: string; password: string }) =>
           void (async () => {
             try {
+              setLoading(true);
               if (mode === Mode.Register) {
                 await register(values);
                 void message.success('注册成功');
@@ -45,6 +47,8 @@ const UserDrawer: React.FC<UserDrawerProps> = props => {
               if (err instanceof Error) {
                 void message.error(err.message);
               }
+            } finally {
+              setLoading(false);
             }
           })()
         }
@@ -73,25 +77,19 @@ const UserDrawer: React.FC<UserDrawerProps> = props => {
           <Input.Password placeholder="请输入密码" />
         </Form.Item>
         <Form.Item label=" " colon={false}>
-          {mode === Mode.Register ? (
-            <>
-              <Button htmlType="submit" type="primary">
-                注册
-              </Button>
-              <Button type="link" onClick={() => setMode(Mode.Login)}>
-                已有账号?返回登录
-              </Button>
-            </>
-          ) : (
-            <>
-              <Button htmlType="submit" type="primary">
-                登录
-              </Button>
-              <Button type="link" onClick={() => setMode(Mode.Register)}>
-                没有账号?前往注册
-              </Button>
-            </>
-          )}
+          <Button htmlType="submit" type="primary" loading={loading}>
+            {mode === Mode.Register ? '注册' : '登录'}
+          </Button>
+          <Button
+            type="link"
+            onClick={() =>
+              setMode(mode === Mode.Register ? Mode.Login : Mode.Register)
+            }
+          >
+            {mode === Mode.Register
+              ? '已有账号?返回登录'
+              : '没有账号?前往注册'}
+          </Button>
         </Form.Item>
       </Form>
     </Drawer>

+ 7 - 2
src/components/WordDrawer.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { Drawer, Form, Input, Button, message } from 'antd';
 import type { DrawerProps } from 'antd';
 import type { WordResult } from '../apis/words';
@@ -17,6 +17,8 @@ const WordDrawer: React.FC<WordDrawerProps> = ({
   refresh,
   ...props
 }) => {
+  const [loading, setLoading] = useState(false);
+
   const createWord = useCreateWord();
   const updateWord = useUpdateWord();
 
@@ -34,6 +36,7 @@ const WordDrawer: React.FC<WordDrawerProps> = ({
         onFinish={(values: { value: string; meaning: string; extra: string }) =>
           void (async () => {
             try {
+              setLoading(true);
               if (initialWord) {
                 await updateWord({ ...initialWord, ...values });
                 void message.success('更新成功');
@@ -47,6 +50,8 @@ const WordDrawer: React.FC<WordDrawerProps> = ({
               if (err instanceof Error) {
                 void message.error(err.message);
               }
+            } finally {
+              setLoading(false);
             }
           })()
         }
@@ -71,7 +76,7 @@ const WordDrawer: React.FC<WordDrawerProps> = ({
           <Input placeholder="请输入附加(如:[ɡʊd])" />
         </Form.Item>
         <Form.Item label=" " colon={false}>
-          <Button htmlType="submit" type="primary">
+          <Button htmlType="submit" type="primary" loading={loading}>
             {initialWord ? '更新' : '创建'}
           </Button>
         </Form.Item>

+ 31 - 25
src/pages/DictPage.tsx

@@ -36,6 +36,7 @@ const DictPage: React.FC = () => {
 
   const {
     data: { dict, words } = {},
+    loading,
     refresh,
     mutate,
   } = useGetDict({
@@ -59,10 +60,6 @@ const DictPage: React.FC = () => {
       },
     });
 
-  if (!dict || !words) {
-    return <></>;
-  }
-
   return (
     <div className={styles.wrapper}>
       <List
@@ -76,34 +73,37 @@ const DictPage: React.FC = () => {
                 onChange={e => setSearch(e.target.value)}
               />
             </Col>
-            <Col>
-              <Button
-                type="link"
-                icon={<PlusOutlined />}
-                onClick={() => {
-                  setInitialWord(undefined);
-                  setOpen(true);
-                }}
-              >
-                创建新单词
-              </Button>
-              <Link to={`/dicts/${dictID}/memo`}>
-                <Button type="link" icon={<AimOutlined />}>
-                  开始复习
+            {dict && (
+              <Col>
+                <Button
+                  type="link"
+                  icon={<PlusOutlined />}
+                  onClick={() => {
+                    setInitialWord(undefined);
+                    setOpen(true);
+                  }}
+                >
+                  创建新单词
                 </Button>
-              </Link>
-            </Col>
+                <Link to={`/dicts/${dictID}/memo`}>
+                  <Button type="link" icon={<AimOutlined />}>
+                    开始复习
+                  </Button>
+                </Link>
+              </Col>
+            )}
           </Row>
         }
+        loading={loading}
         dataSource={
           search
-            ? words.list.filter(
+            ? words?.list.filter(
                 word =>
                   word.value.includes(search) ||
                   word.meaning.includes(search) ||
                   word.extra.includes(search),
               )
-            : words.list
+            : words?.list
         }
         renderItem={(word, index) => (
           <List.Item
@@ -111,6 +111,7 @@ const DictPage: React.FC = () => {
               <Button
                 key="update"
                 type="link"
+                size="small"
                 icon={<EditOutlined />}
                 onClick={() => {
                   setInitialWord(word);
@@ -121,6 +122,7 @@ const DictPage: React.FC = () => {
                 key="star"
                 type="link"
                 danger
+                size="small"
                 icon={word.star ? <StarFilled /> : <StarOutlined />}
                 onClick={() =>
                   void (async () => {
@@ -128,7 +130,6 @@ const DictPage: React.FC = () => {
                       const newWord = { ...word, star: !word.star };
                       mutateWordsList(newWord, index);
                       await updateWord(newWord);
-                      void message.success('更新成功');
                     } catch (err) {
                       if (err instanceof Error) {
                         void message.error(err.message);
@@ -154,7 +155,12 @@ const DictPage: React.FC = () => {
                   })()
                 }
               >
-                <Button type="link" danger icon={<DeleteOutlined />} />
+                <Button
+                  type="link"
+                  danger
+                  size="small"
+                  icon={<DeleteOutlined />}
+                />
               </Popconfirm>,
             ]}
           >
@@ -169,7 +175,7 @@ const DictPage: React.FC = () => {
             />
           </List.Item>
         )}
-      ></List>
+      />
       <WordDrawer
         dictID={parseInt(dictID)}
         initialWord={initialWord}

+ 91 - 29
src/pages/DictsPage.tsx

@@ -1,47 +1,109 @@
 import React, { useState } from 'react';
 import { Link } from 'react-router-dom';
-import { Row, Col, Card, Space, Typography } from 'antd';
-import { PlusOutlined } from '@ant-design/icons';
-import { useListDicts } from '../apis/dicts';
+import {
+  Spin,
+  Row,
+  Col,
+  Card,
+  Button,
+  Popconfirm,
+  Space,
+  Typography,
+  message,
+} from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
+import type { DictResult } from '../apis/dicts';
+import { useListDicts, useDeleteDict } from '../apis/dicts';
 import DictDrawer from '../components/DictDrawer';
 import styles from './DictsPage.module.css';
 
 const { Text } = Typography;
 
 const DictsPage: React.FC = () => {
+  const [initialDict, setInitialDict] = useState<DictResult>();
   const [open, setOpen] = useState(false);
 
-  const { data: dicts, refresh } = useListDicts();
+  const { data: dicts, loading, refresh } = useListDicts();
+  const deleteDict = useDeleteDict();
 
   return (
     <div className={styles.wrapper}>
-      <Row gutter={[16, 16]}>
-        <Col xs={24} sm={12} md={8} lg={6} xl={4} xxl={3}>
-          <Card hoverable onClick={() => setOpen(true)}>
-            <PlusOutlined /> 创建新词库
-          </Card>
-        </Col>
-        {dicts?.list.map(dict => (
-          <Col key={dict.id} xs={24} sm={12} md={8} lg={6} xl={4} xxl={3}>
-            <Link to={`/dicts/${dict.id}`}>
-              <Card hoverable>
-                <Card.Meta
-                  title={
-                    <Space size="middle">
-                      {dict.name}
-                      <Text type="secondary">
-                        {dict.value} ↔ {dict.meaning}
-                      </Text>
-                    </Space>
-                  }
-                  description={`词数:${dict.wordCount}`}
-                />
-              </Card>
-            </Link>
+      <Spin spinning={loading}>
+        <Row gutter={[16, 16]}>
+          <Col xs={24} sm={12} md={8} lg={6} xl={4} xxl={3}>
+            <Card
+              hoverable
+              onClick={() => {
+                setInitialDict(undefined);
+                setOpen(true);
+              }}
+            >
+              <PlusOutlined /> 创建新词库
+            </Card>
           </Col>
-        ))}
-      </Row>
+          {dicts?.list.map(dict => (
+            <Col key={dict.id} xs={24} sm={12} md={8} lg={6} xl={4} xxl={3}>
+              <Card
+                hoverable
+                actions={[
+                  <Button
+                    key="update"
+                    type="link"
+                    block
+                    size="small"
+                    icon={<EditOutlined />}
+                    onClick={() => {
+                      setInitialDict(dict);
+                      setOpen(true);
+                    }}
+                  />,
+                  <Popconfirm
+                    key="delete"
+                    title="确定删除该词库?"
+                    onConfirm={() =>
+                      void (async () => {
+                        try {
+                          await deleteDict({ id: dict.id });
+                          void message.success('删除成功');
+                          refresh();
+                        } catch (err) {
+                          if (err instanceof Error) {
+                            void message.error(err.message);
+                          }
+                        }
+                      })()
+                    }
+                  >
+                    <Button
+                      type="link"
+                      danger
+                      block
+                      size="small"
+                      icon={<DeleteOutlined />}
+                    />
+                  </Popconfirm>,
+                ]}
+              >
+                <Link to={`/dicts/${dict.id}`}>
+                  <Card.Meta
+                    title={
+                      <Space size="middle">
+                        {dict.name}
+                        <Text type="secondary">
+                          {dict.valueTitle} ↔ {dict.meaningTitle}
+                        </Text>
+                      </Space>
+                    }
+                    description={`词数:${dict.wordCount}`}
+                  />
+                </Link>
+              </Card>
+            </Col>
+          ))}
+        </Row>
+      </Spin>
       <DictDrawer
+        initialDict={initialDict}
         refresh={refresh}
         open={open}
         onClose={() => setOpen(false)}

+ 40 - 28
src/pages/MemoPage.tsx

@@ -1,8 +1,18 @@
 import React, { useState, useEffect } from 'react';
 import { useParams } from 'react-router-dom';
 import _ from 'lodash';
-import { Space, Row, Col, Button, Typography, Empty, message } from 'antd';
 import {
+  Result,
+  Space,
+  Row,
+  Col,
+  Button,
+  Typography,
+  Empty,
+  message,
+} from 'antd';
+import {
+  Loading3QuartersOutlined,
   EyeOutlined,
   StarFilled,
   StarOutlined,
@@ -31,7 +41,7 @@ const MemoPage: React.FC = () => {
 
   const { dictID = '' } = useParams();
 
-  const { data: { dict, words } = {} } = useGetDict({
+  const { data: { dict, words } = {}, loading } = useGetDict({
     id: parseInt(dictID),
   });
   const updateWord = useUpdateWord();
@@ -56,33 +66,34 @@ const MemoPage: React.FC = () => {
     setShowExtra(false);
   };
 
-  if (!dict || !shuffledWords) {
-    return <></>;
-  }
-
   return (
     <div className={styles.wrapper}>
       <Space className={styles.space} size="large" direction="vertical">
-        <Row justify="space-around">
-          <Col>
-            <Button
-              type={mode === Mode.ValueToMeaning ? 'primary' : 'default'}
-              onClick={() => setMode(Mode.ValueToMeaning)}
-            >
-              {dict.value} → {dict.meaning}
-            </Button>
-          </Col>
-          <Col>
-            <Button
-              type={mode === Mode.MeaningToValue ? 'primary' : 'default'}
-              onClick={() => setMode(Mode.MeaningToValue)}
-            >
-              {dict.meaning} → {dict.value}
-            </Button>
-          </Col>
-        </Row>
-        {shuffledWords.length ? (
+        {loading ? (
+          <Result
+            icon={<Loading3QuartersOutlined spin />}
+            subTitle="加载中..."
+          />
+        ) : dict && shuffledWords?.length ? (
           <>
+            <Row justify="space-around">
+              <Col>
+                <Button
+                  type={mode === Mode.ValueToMeaning ? 'primary' : 'default'}
+                  onClick={() => setMode(Mode.ValueToMeaning)}
+                >
+                  {dict.valueTitle} → {dict.meaningTitle}
+                </Button>
+              </Col>
+              <Col>
+                <Button
+                  type={mode === Mode.MeaningToValue ? 'primary' : 'default'}
+                  onClick={() => setMode(Mode.MeaningToValue)}
+                >
+                  {dict.meaningTitle} → {dict.valueTitle}
+                </Button>
+              </Col>
+            </Row>
             <Row justify="center">
               <Col className={styles.primaryText}>
                 <Text strong>
@@ -107,7 +118,9 @@ const MemoPage: React.FC = () => {
                     icon={<EyeOutlined />}
                     onClick={() => setShowValueOrMeaning(true)}
                   >
-                    {mode === Mode.MeaningToValue ? dict.value : dict.meaning}
+                    {mode === Mode.MeaningToValue
+                      ? dict.valueTitle
+                      : dict.meaningTitle}
                   </Button>
                 )}
               </Col>
@@ -121,7 +134,7 @@ const MemoPage: React.FC = () => {
                     icon={<EyeOutlined />}
                     onClick={() => setShowExtra(true)}
                   >
-                    {dict.extra}
+                    {dict.extraTitle}
                   </Button>
                 )}
               </Col>
@@ -157,7 +170,6 @@ const MemoPage: React.FC = () => {
                         };
                         mutateWordsList(newWord, index);
                         await updateWord(newWord);
-                        void message.success('更新成功');
                       } catch (err) {
                         if (err instanceof Error) {
                           void message.error(err.message);