Procházet zdrojové kódy

feat: 用户修改密码

xyh před 2 roky
rodič
revize
1d5ae705e9

+ 10 - 0
packages/app/src/apis/finishProduct.ts

@@ -3,6 +3,7 @@ import {
   FinishProductListStreamInListData,
   FinishProductListStreamOutListData,
   GetFinishProductListStreamParams,
+  OtherGoodsOutParams,
 } from '@models';
 import {request} from './request';
 
@@ -54,3 +55,12 @@ export function getFinishProductInStreamExport(
     skipError: true,
   });
 }
+
+/** 其他出库 */
+export function otherGoodsOut(data: OtherGoodsOutParams): BaseListResult {
+  return request({
+    method: 'GET',
+    data,
+    url: `${BASE_URL}/otherAskGoods`,
+  });
+}

+ 19 - 0
packages/app/src/apis/user.ts

@@ -3,6 +3,7 @@ import {
   BaseListResult,
   BaseResult,
   EditUserParams,
+  EditUserPasswordParams,
   GetUserListParams,
   UserListData,
   UserLoginData,
@@ -74,3 +75,21 @@ export function deleteUser(id: string): BaseResult {
     url: `${BASE_URL}/delUser`,
   });
 }
+
+/** 重置密码 */
+export function resetUserPassword(id: string): BaseResult {
+  return request({
+    method: 'POST',
+    data: {id},
+    url: `${BASE_URL}/resetPassword`,
+  });
+}
+
+/** 修改密码 */
+export function editUserPassword(data: EditUserPasswordParams): BaseResult {
+  return request({
+    method: 'POST',
+    data,
+    url: `${BASE_URL}/updatePassword`,
+  });
+}

+ 7 - 3
packages/app/src/hooks/use-options/index.ts

@@ -1,5 +1,5 @@
 import {getAllRoleList, getAllStorage, getDictionaryOptions} from '@apis';
-import {BaseResult, DictionaryParamsType, StorageListData} from '@models';
+import {BaseResult, DictionaryData, DictionaryParamsType, StorageListData} from '@models';
 import {useQuery} from '@tanstack/react-query';
 import {useLatest} from 'ahooks';
 
@@ -59,7 +59,11 @@ export function useRoleOptions(addAll = false) {
   });
 }
 
-export function useDictionaryOptions(type: DictionaryParamsType, addAll = false) {
+export function useDictionaryOptions(
+  type: DictionaryParamsType,
+  addAll = false,
+  findValue: (state: DictionaryData) => string = state => state.tldId,
+) {
   const {data} = useQuery(
     [getDictionaryOptions.name, type, addAll],
     async function() {
@@ -67,7 +71,7 @@ export function useDictionaryOptions(type: DictionaryParamsType, addAll = false)
 
       if (data.msg === '200') {
         const list = data.data.map(function(value) {
-          return {label: value.name, value: String(value.tldId)};
+          return {label: value.name, value: findValue(value)};
         });
 
         if (addAll) list.unshift({label: '全部', value: ''});

+ 16 - 0
packages/app/src/models/request/finishProduct.ts

@@ -9,3 +9,19 @@ export type GetFinishProductListStreamParams = {
   /** 结束时间 */
   endTime: string;
 } & ListParams;
+
+/** 其他出库 */
+export type OtherGoodsOutParams = {
+  /** 物料code */
+  wllbCode: string;
+  /** 库位code */
+  storageLocationCode: string;
+  /** 出库数量 */
+  num: string;
+  /** 用户id */
+  userId: string;
+  /** 部门id */
+  department: string;
+  /** wbs编号 */
+  wbs: string;
+};

+ 10 - 0
packages/app/src/models/request/user.ts

@@ -20,3 +20,13 @@ export type EditUserParams = Omit<
 UserListData,
 'departmentId' | 'roleId' | 'createTime' | 'code' | 'password'
 >;
+
+/** 修改密码 */
+export type EditUserPasswordParams = {
+  /** 用户id */
+  id: string;
+  /** 新密码 */
+  newPassword: string;
+  /** 旧密码*/
+  password: string;
+};

+ 2 - 0
packages/app/src/models/response/user.ts

@@ -9,6 +9,8 @@ export type UserLoginData = {
   userName: string;
   /** 菜单 */
   menu: string | null;
+  /** 部门id */
+  department: string;
 };
 
 /** 用户列表 */

+ 24 - 13
packages/app/src/pages/home/user/hook.tsx

@@ -1,31 +1,42 @@
 import {MenuProps, Modal} from 'antd';
-import {LogoutOutlined} from '@ant-design/icons';
+import {LogoutOutlined, EditFilled} from '@ant-design/icons';
 import {useStore} from 'zustand';
 import {userStore} from '@stores';
 import {useNavigate} from 'react-router-dom';
 import {LOGIN_PATH} from '@routes';
+import {useBoolean} from 'ahooks';
 
 export const items: MenuProps['items'] = [
   {key: 'logout', icon: <LogoutOutlined />, label: '退出登录'},
+  {key: 'editPassword', icon: <EditFilled />, label: '修改密码'},
 ];
 
 export function useLogout() {
   const logout = useStore(userStore, state => state.logout);
   const navigate = useNavigate();
+  const [visible, {setTrue, setFalse}] = useBoolean();
 
   function onClick({key}: {key: string}) {
-    if (key === 'logout')
-      Modal.confirm({
-        title: '退出登录',
-        content: '你确定要退出登录吗?',
-        okText: '确定',
-        cancelText: '取消',
-        onOk() {
-          logout();
-          navigate(LOGIN_PATH);
-        },
-      });
+    switch (key) {
+      case 'logout': {
+        Modal.confirm({
+          title: '退出登录',
+          content: '你确定要退出登录吗?',
+          okText: '确定',
+          cancelText: '取消',
+          onOk() {
+            logout();
+            navigate(LOGIN_PATH);
+          },
+        });
+        break;
+      }
+      case 'editPassword': {
+        setTrue();
+        break;
+      }
+    }
   }
 
-  return onClick;
+  return [{visible}, {onClick, onClose: setFalse}] as const;
 }

+ 12 - 7
packages/app/src/pages/home/user/index.tsx

@@ -5,18 +5,23 @@ import {items, useLogout} from './hook';
 import {useStore} from 'zustand';
 import {userStore} from '@stores';
 import face from '@assets/images/face.svg';
+import PassModal from './modal';
 
 const User: FC = function() {
-  const onClick = useLogout();
+  const [{visible}, {onClick, onClose}] = useLogout();
   const name = useStore(userStore, state => state.realName);
 
   return (
-    <Dropdown menu={{items, onClick}}>
-      <div className={css.user}>
-        <Avatar src={face} alt='face' />
-        <span data-testid='user_name'>{name}</span>
-      </div>
-    </Dropdown>
+    <>
+      <Dropdown menu={{items, onClick}}>
+        <div className={css.user}>
+          <Avatar src={face} alt='face' />
+          <span data-testid='user_name'>{name}</span>
+        </div>
+      </Dropdown>
+
+      <PassModal visible={visible} onClose={onClose} />
+    </>
   );
 };
 

+ 77 - 0
packages/app/src/pages/home/user/modal/hooks.ts

@@ -0,0 +1,77 @@
+import {editUserPassword} from '@apis';
+import {yupResolver} from '@hookform/resolvers/yup';
+import {LOGIN_PATH} from '@routes';
+import {userStore} from '@stores';
+import {useMutation} from '@tanstack/react-query';
+import {message} from 'antd';
+import {useEffect} from 'react';
+import {useForm} from 'react-hook-form';
+import {useNavigate} from 'react-router-dom';
+import {object, string} from 'yup';
+import {useStore} from 'zustand';
+
+type FormState = {
+  oldPassword: string;
+  newPassword: string;
+  repeatPassword: string;
+};
+
+const validate = object({
+  oldPassword: string().required('请输入原密码'),
+  newPassword: string().required('请输入新密码'),
+});
+
+export function useEditPassword(onClose: () => void) {
+  const logout = useStore(userStore, state => state.logout);
+  const navigate = useNavigate();
+
+  return useMutation({
+    mutationFn: editUserPassword,
+    onSuccess({msg}) {
+      if (msg === '200') {
+        onClose();
+        message.success('修改成功,请重新登录');
+        logout();
+        navigate(LOGIN_PATH);
+      }
+    },
+  });
+}
+
+export function useFormState(visible: boolean, onClose: () => void) {
+  const {control, clearErrors, reset, setError, handleSubmit} = useForm<FormState>({
+    defaultValues: {
+      oldPassword: '',
+      newPassword: '',
+      repeatPassword: '',
+    },
+    resolver: yupResolver(validate),
+  });
+
+  useEffect(function() {
+    if (visible) {
+      clearErrors();
+      reset();
+    }
+  }, [clearErrors, reset, visible]);
+
+  const {isLoading, mutate} = useEditPassword(onClose);
+  const userId = useStore(userStore, state => state.id);
+
+  const onSubmit = handleSubmit(function({
+    oldPassword,
+    newPassword,
+    repeatPassword,
+  }) {
+    if (newPassword !== repeatPassword)
+      return setError('repeatPassword', {type: 'custom', message: '两次输入密码不一致'});
+
+    mutate({
+      id: String(userId),
+      password: oldPassword,
+      newPassword,
+    });
+  });
+
+  return [{control, isLoading}, {onSubmit}] as const;
+}

+ 45 - 0
packages/app/src/pages/home/user/modal/index.tsx

@@ -0,0 +1,45 @@
+import {Modal, ModalField} from '@components';
+import {FC} from 'react';
+import {useFormState} from './hooks';
+
+type Props = {
+  visible: boolean,
+  onClose: () => void,
+};
+
+const PassModal: FC<Props> = function({visible, onClose}) {
+  const [{control, isLoading}, {onSubmit}] = useFormState(visible, onClose);
+
+  return (
+    <Modal
+      visible={visible}
+      title='修改密码'
+      onSubmit={onSubmit}
+      onClose={onClose}
+      testId='edit_password_modal'
+      height='500px'
+      isLoading={isLoading}
+    >
+      <ModalField
+        control={control}
+        name='oldPassword'
+        label='旧密码'
+        type='password'
+      />
+      <ModalField
+        control={control}
+        name='newPassword'
+        label='新密码'
+        type='password'
+      />
+      <ModalField
+        control={control}
+        name='repeatPassword'
+        label='重复新密码'
+        type='password'
+      />
+    </Modal>
+  );
+};
+
+export default PassModal;

+ 7 - 0
packages/app/src/pages/login/info/hooks.ts

@@ -14,11 +14,13 @@ import {USER_TOKEN_STORAGE} from '@utils';
 type FormState = {
   name: string;
   password: string;
+  type: string;
 };
 
 const validate = object({
   name: string().required('请输入账号'),
   password: string().required('请输入密码'),
+  type: string().required('请选择系统'),
 });
 
 function useLogin() {
@@ -50,6 +52,11 @@ function useLogin() {
 export function useFormState() {
   const {handleSubmit, formState: {errors}, control} = useForm<FormState>({
     resolver: yupResolver(validate),
+    defaultValues: {
+      type: '1',
+      name: '',
+      password: '',
+    },
   });
   const {isLoading, mutate, isDisable} = useLogin();
 

+ 3 - 2
packages/app/src/pages/login/info/index.module.css

@@ -15,8 +15,9 @@
   flex-direction: column;
   margin-top: 20px;
 
-  & input:nth-of-type(2) {
-    margin-top: 16px;
+  & input:nth-of-type(2),
+  & :global(.ant-select) {
+    margin-top: 12px;
   }
 }
 

+ 23 - 4
packages/app/src/pages/login/info/index.tsx

@@ -1,4 +1,4 @@
-import {Button, Input} from 'antd';
+import {Button, Input, Select} from 'antd';
 import css from './index.module.css';
 import {FC} from 'react';
 import {useFormState} from './hooks';
@@ -51,6 +51,7 @@ const LoginInfo: FC = function() {
             );
           }}
         />
+
         <p
           className={cla([
             css.errorTips,
@@ -60,10 +61,28 @@ const LoginInfo: FC = function() {
           {errors.password?.message ?? ''}
         </p>
 
-        <ul className={css.loginRootList}>
-          <li>仓库管理系统</li>
-        </ul>
+        <Controller
+          control={control}
+          name='type'
+          render={function({field: {onChange, value}}) {
+            return (
+              <Select
+                value={value}
+                onChange={onChange}
+                options={[{label: '仓储物流系统', value: '1'}]}
+              />
+            );
+          }}
+        />
 
+        <p
+          className={cla([
+            css.errorTips,
+            {[css.loginFieldHidden]: !errors.type?.message},
+          ])}
+        >
+          {errors.type?.message ?? ''}
+        </p>
         <Button
           htmlType='submit'
           type='primary'

+ 2 - 1
packages/app/src/pages/semi-draw/table/modal/hooks.ts

@@ -15,7 +15,8 @@ type FormState = {
 };
 
 const validate = object({
-  semiDrawNum: number().typeError('请输入数字').min(1, '不能小于1个'),
+  semiDrawNum: number().required('请输入出库数量')
+    .typeError('请输入数字').min(1, '不能小于1个'),
 });
 
 function useInfoData(id: string) {

+ 2 - 1
packages/app/src/pages/semi-report/table/modal/hooks.ts

@@ -15,7 +15,8 @@ type FormState = {
 };
 
 const validate = object({
-  putInStoragenum: number().typeError('请输入数字').min(1, '不能少于1个'),
+  putInStoragenum: number().required('请输入入库数量')
+    .typeError('请输入数字').min(1, '不能少于1个'),
 });
 
 function useQuery(onClose: () => void, onFetch: () => void) {

+ 31 - 6
packages/app/src/pages/stock-operation/hooks.ts

@@ -1,6 +1,11 @@
+import {otherGoodsOut} from '@apis';
 import {yupResolver} from '@hookform/resolvers/yup';
+import {userStore} from '@stores';
+import {useMutation} from '@tanstack/react-query';
+import {message} from 'antd';
 import {useForm} from 'react-hook-form';
 import {number, object, string} from 'yup';
+import {useStore} from 'zustand';
 
 type FormState = {
   /** 物料code */
@@ -15,23 +20,43 @@ type FormState = {
 
 const validate = object({
   materialCode: string().required('请选择物料'),
-  operationNum: number().typeError('请输入数字类型').min(1, '数量不能小于1个'),
+  locationCode: string().required('请选择库位'),
+  operationNum: number().required('请输入出库数量')
+    .typeError('请输入数字类型').min(1, '数量不能小于1个'),
 });
 
 export function useFormState() {
-  const {control, handleSubmit} = useForm<FormState>({
+  const {control, handleSubmit, reset, clearErrors} = useForm<FormState>({
     defaultValues: {
       materialCode: '',
-      operationNum: 0,
       wbsCode: '',
       locationCode: '',
     },
     resolver: yupResolver(validate),
   });
 
-  const onSubmit = handleSubmit(function({materialCode, operationNum, wbsCode}) {
-    console.log(materialCode, operationNum, wbsCode);
+  const {mutate, isLoading} = useMutation({
+    mutationFn: otherGoodsOut,
+    onSuccess({msg}) {
+      if (msg === '200') {
+        message.success('出库成功');
+        reset();
+        clearErrors();
+      }
+    },
+  });
+  const {id, department} = useStore(userStore);
+
+  const onSubmit = handleSubmit(function({materialCode, operationNum, wbsCode, locationCode}) {
+    mutate({
+      userId: String(id),
+      department,
+      wllbCode: materialCode,
+      num: String(operationNum),
+      wbs: wbsCode,
+      storageLocationCode: locationCode,
+    });
   });
 
-  return [{control}, {onSubmit}] as const;
+  return [{control, isLoading}, {onSubmit}] as const;
 }

+ 9 - 4
packages/app/src/pages/stock-operation/index.module.css

@@ -11,22 +11,27 @@
 
 .text-right {
   display: block;
-  width: 80px;
+  width: 100px;
   height: 32px;
   margin-right: 12px;
   line-height: 2.2;
   color: #666;
-  text-align: right;
-}
+  text-align: left;
 
-.field-required {
   &::before {
     padding-right: 4px;
     color: #f20c00;
+    visibility: hidden;
     content: '*';
   }
 }
 
+.field-required {
+  &::before {
+    visibility: visible;
+  }
+}
+
 .filed-width {
   width: 260px;
 }

+ 12 - 5
packages/app/src/pages/stock-operation/index.tsx

@@ -9,9 +9,16 @@ import {useDictionaryOptions, useStorageOptions} from '@hooks';
 
 const StockOperation: FC = function() {
   const {type} = useParams<{type: 'in' | 'out'}>();
-  const [{control}, {onSubmit}] = useFormState();
-  const options = useDictionaryOptions('物料字典');
-  const locationOptions = useStorageOptions();
+  const [{control, isLoading}, {onSubmit}] = useFormState();
+  const options = useDictionaryOptions(
+    '物料字典',
+    false,
+    state => state.code,
+  );
+  const locationOptions = useStorageOptions(
+    false,
+    state => state.storageLocationCode,
+  );
 
   return (
     <section className='content-main'>
@@ -26,7 +33,7 @@ const StockOperation: FC = function() {
             />
             <Select
               control={control}
-              label='所在库'
+              label='所在库'
               name='locationCode'
               data={locationOptions}
             />
@@ -42,7 +49,7 @@ const StockOperation: FC = function() {
               name='wbsCode'
               required={false}
             />
-            <ModalBtnGroup />
+            <ModalBtnGroup isLoading={isLoading} />
           </Space>
         </form>
       </Card>

+ 39 - 10
packages/app/src/pages/user/table/hooks.tsx

@@ -1,12 +1,13 @@
 import {useContextSection, useQueryTableList} from '@hooks';
 import {useMutation} from '@tanstack/react-query';
 import {context, pageContext, searchContext} from '../context';
-import {deleteUser, getUserList} from '@apis';
+import {deleteUser, getUserList, resetUserPassword} from '@apis';
 import {UserListData} from '@models';
 import {ColumnsType} from 'antd/es/table';
 import {useState} from 'react';
 import {Button, Modal, message} from 'antd';
 import {useBoolean} from 'ahooks';
+import {deleteConfirm} from '@utils';
 
 export function useList() {
   const params = useContextSection(
@@ -26,6 +27,29 @@ export function useList() {
   return [{data, isFetching, count}, {refetch}] as const;
 }
 
+export function useResetPassword() {
+  const {isLoading, mutate} = useMutation({
+    mutationFn: resetUserPassword,
+    onSuccess({msg}) {
+      msg === '200' && message.success('密码重置成功');
+    },
+  });
+
+  function onClick(name: string, id: string) {
+    return function() {
+      Modal.confirm({
+        title: '重置密码',
+        content: `你确定要重置${name}的密码吗?`,
+        onOk() {
+          mutate(id);
+        },
+      });
+    };
+  }
+
+  return [isLoading, onClick] as const;
+}
+
 export function useHandle(onFetch: () => void) {
   const [visible, {setTrue, setFalse}] = useBoolean();
 
@@ -45,13 +69,9 @@ export function useHandle(onFetch: () => void) {
   );
   function onDelte(id: string) {
     return function() {
-      Modal.confirm({
-        title: '删除用户',
-        content: '你确定要删除当前用户吗?',
-        onOk() {
-          setPendingId(id);
-          mutate(id);
-        },
+      deleteConfirm('用户', function() {
+        setPendingId(id);
+        mutate(id);
       });
     };
   }
@@ -70,6 +90,7 @@ export function useHandle(onFetch: () => void) {
     setEditId('');
   }
 
+  const [isReseting, onResetClick] = useResetPassword();
   const columns: ColumnsType<UserListData> = [
     {title: '用户编号', dataIndex: 'code', key: 'code', width: 150},
     {title: '用户名称', dataIndex: 'userName', key: 'userName', width: 250},
@@ -84,9 +105,9 @@ export function useHandle(onFetch: () => void) {
       title: '操作',
       dataIndex: 'id',
       key: 'id',
-      width: 200,
+      width: 260,
       fixed: 'right',
-      render(_, {id}) {
+      render(_, {id, realName}) {
         return (
           <>
             <Button
@@ -104,6 +125,14 @@ export function useHandle(onFetch: () => void) {
             >
               删除
             </Button>
+            <Button
+              type='text'
+              disabled={String(id) === pendingId}
+              loading={isReseting}
+              onClick={onResetClick(realName, String(id))}
+            >
+              重置密码
+            </Button>
           </>
         );
       },

+ 1 - 1
packages/app/src/pages/user/table/index.tsx

@@ -21,7 +21,7 @@ const TableList: FC = function() {
           pageContext={pageContext}
           searchContext={searchContext}
           count={count}
-          scrollX={2050}
+          scrollX={2100}
         />
       </Card>
 

+ 1 - 0
packages/app/src/stores/user.ts

@@ -19,6 +19,7 @@ function defaultValue(): UserStoreState {
     token: '',
     userName: '',
     menu: null,
+    department: '',
   };
 }