Browse Source

feat: 增加物料管理

xyh 3 years ago
parent
commit
798725b6de

+ 38 - 1
packages/app/src/apis/dictionary.ts

@@ -1,4 +1,11 @@
-import {BaseResult, DictionaryData, DictionaryParamsType} from '@models';
+import {
+  BaseListResult,
+  BaseResult,
+  DictionaryData,
+  DictionaryParamsType,
+  EditDictionaryParams,
+  GetDictionaryListParams,
+} from '@models';
 import {request} from './request';
 
 const BASE_URL = '/dictionary';
@@ -11,3 +18,33 @@ export function getDictionaryOptions(type: DictionaryParamsType): BaseResult<Dic
     url: `${BASE_URL}/getDictionary`,
   });
 }
+
+/** 获取字典分页列表 */
+export function getDictionaryList(data: GetDictionaryListParams): BaseListResult<DictionaryData> {
+  return request({
+    method: 'GET',
+    data,
+    url: `${BASE_URL}/getDictionaryPage`,
+  });
+}
+
+/** 获取字典详情内容 */
+export function getDictionaryInfo(
+  type: DictionaryParamsType,
+  id: string,
+): BaseListResult<DictionaryData> {
+  return request({
+    method: 'GET',
+    data: {page: '1', limit: '1', type, tldId: id},
+    url: `${BASE_URL}/getDictionaryPage`,
+  });
+}
+
+/** 修改物料字典内容 */
+export function editDictionary(data: EditDictionaryParams): BaseResult {
+  return request({
+    method: 'PUT',
+    data,
+    url: '/materialClass/updateMaterial',
+  });
+}

+ 0 - 1
packages/app/src/apis/index.ts

@@ -3,7 +3,6 @@ export * from './department';
 export * from './menu';
 export * from './role';
 export * from './storage';
-export * from './goods';
 export * from './container';
 export * from './dictionary';
 export * from './material';

+ 1 - 1
packages/app/src/hooks/useOptions/index.ts

@@ -73,7 +73,7 @@ export function useDictionaryOptions(type: DictionaryParamsType, addAll = false)
 
       return [];
     },
-    {initialData: []},
+    {initialData: [], cacheTime: 1000 * 60 * 10},
   );
 
   return data;

+ 26 - 0
packages/app/src/models/request/dictionary.ts

@@ -1,2 +1,28 @@
+import {ListParams} from '.';
+
 export type DictionaryParamsType = '公司' | '库存组织' | '仓库' | '部门字典'
 | '物料字典' | '计量单位' | 'WBS字典' | '供应商/客户' | '物料类型';
+
+/** 获取物料列表 */
+export type GetDictionaryListParams = {
+  type: DictionaryParamsType,
+  name: string,
+  code: string,
+} & ListParams;
+
+/** 修改物料字典内容 */
+export type EditDictionaryParams = {
+  id: string;
+  /**
+   * 是否混合
+   */
+  isNotDisable: string;
+  /**
+   * 物料类型
+   */
+  materialType: string;
+  /**
+   * 物料大小
+   */
+  size: string;
+};

+ 0 - 1
packages/app/src/models/request/index.ts

@@ -11,7 +11,6 @@ export * from './menu';
 export * from './role';
 export * from './storage';
 export * from './user';
-export * from './goods';
 export * from './container';
 export * from './dictionary';
 export * from './material';

+ 14 - 20
packages/app/src/models/response/dictionary.ts

@@ -1,26 +1,20 @@
 export type DictionaryData = {
-  /**
-     * 编号
-     */
+  /** 编号 */
   code: string;
-  /**
-        * 主键
-        */
   id: string;
-  /**
-        * 名称
-        */
+  /** 是否混合存储 */
+  isNotDisable: string | null;
+  /** 物料类型id */
+  materialId: string | null;
+  wllbClass: string | null;
+  /** 物料类型 */
+  materialType: string | null;
+  /** 名称 */
   name: string;
-  /**
-        * 规格型号
-        */
-  specificationAndModel: string | null;
-  /**
-        * GS id
-        */
+  /** 数量 */
+  num: string | null;
+  /** 存储数量 */
+  size: string | null;
   tldId: string;
-  /**
-        * 计量单位
-        */
-  unitOfMeasurement: string | null;
+  type: string | null;
 };

+ 0 - 1
packages/app/src/models/response/index.ts

@@ -30,7 +30,6 @@ export * from './department';
 export * from './menu';
 export * from './role';
 export * from './storage';
-export * from './goods';
 export * from './container';
 export * from './dictionary';
 export * from './material';

+ 40 - 0
packages/app/src/pages/goods/context.ts

@@ -0,0 +1,40 @@
+import {createPageContext, createSearchContext} from '@hooks';
+import {useReducer} from 'react';
+import {createContext} from 'use-context-selector';
+
+export const pageContext = createPageContext();
+export const searchContext = createSearchContext();
+
+type State = {
+  name: string,
+  code: string,
+};
+
+type Action = {
+  type: 'SEARCH',
+  payload: State,
+};
+
+function initState(): State {
+  return {name: '', code: ''};
+}
+
+function reducer(state: State, action: Action): State {
+  const {payload, type} = action;
+
+  switch (type) {
+    case 'SEARCH':
+      return payload;
+    default:
+      return state;
+  }
+}
+
+export function useContextReducer() {
+  return useReducer(reducer, initState());
+}
+
+export const context = createContext<ReturnType<typeof useContextReducer>>([
+  initState(),
+  () => null,
+]);

+ 13 - 0
packages/app/src/pages/goods/filter/hooks.ts

@@ -0,0 +1,13 @@
+import {useContextSection, useTableSearch} from '@hooks';
+import {context, searchContext} from '../context';
+
+export function useSearch(name: string, code: string) {
+  const [isSearching] = useTableSearch(searchContext);
+  const dispatch = useContextSection(context, state => state[1]);
+
+  function onSearch() {
+    dispatch({type: 'SEARCH', payload: {name, code}});
+  }
+
+  return [isSearching, onSearch] as const;
+}

+ 27 - 0
packages/app/src/pages/goods/filter/index.tsx

@@ -0,0 +1,27 @@
+import {FilterField, FilterButtonGroup} from '@components';
+import {useFilterField} from '@hooks';
+import {Card, Row} from 'antd';
+import {FC} from 'react';
+import {useSearch} from './hooks';
+
+const Filter: FC = function() {
+  const [{name, code}, onChange] = useFilterField({name: '', code: ''});
+  const [isSearching, onSearch] = useSearch(name, code);
+
+  return (
+    <Card>
+      <Row>
+        <FilterField name='goodsName' label='物料名称' value={name} onChange={onChange('name')} />
+        <FilterField name='goodsCode' label='物料编号' value={code} onChange={onChange('code')} />
+
+        <FilterButtonGroup
+          offset={6}
+          onSearch={onSearch}
+          isSearching={isSearching}
+        />
+      </Row>
+    </Card>
+  );
+};
+
+export default Filter;

+ 30 - 0
packages/app/src/pages/goods/index.tsx

@@ -0,0 +1,30 @@
+import {ChildrenFC} from '@utils';
+import {FC} from 'react';
+import {context, pageContext, searchContext, useContextReducer} from './context';
+import {PageProvider, SearchProvider} from '@components';
+import Filter from './filter';
+import TableList from './table';
+
+const GoodsProvider: ChildrenFC = function({children}) {
+  const {Provider} = context;
+  const state = useContextReducer();
+
+  return <Provider value={state}>{children}</Provider>;
+};
+
+const Goods: FC = function() {
+  return (
+    <GoodsProvider>
+      <PageProvider context={pageContext}>
+        <SearchProvider context={searchContext}>
+          <section className='content-main'>
+            <Filter />
+            <TableList />
+          </section>
+        </SearchProvider>
+      </PageProvider>
+    </GoodsProvider>
+  );
+};
+
+export default Goods;

+ 78 - 0
packages/app/src/pages/goods/table/hooks.tsx

@@ -0,0 +1,78 @@
+import {useContextSection, useQueryTableList} from '@hooks';
+import {context, pageContext, searchContext} from '../context';
+
+import {getDictionaryList} from '@apis';
+import {DictionaryData, DictionaryParamsType} from '@models';
+import {Button} from 'antd';
+import {ColumnsType} from 'antd/es/table';
+import {useState} from 'react';
+import {useBoolean} from 'ahooks';
+
+export function useList() {
+  const params = useContextSection(
+    context,
+    function([{name, code}]) {
+      return {name, code, type: ('物料字典' as DictionaryParamsType)};
+    },
+  );
+
+  return useQueryTableList({
+    queryFn: getDictionaryList,
+    params,
+    pageContext,
+    searchContext,
+  });
+}
+
+export function useEdit() {
+  const [editId, setEditId] = useState('');
+  const [visible, {setTrue, setFalse}] = useBoolean();
+
+  function onEdit(id: string) {
+    return function() {
+      setEditId(id);
+      setTrue();
+    };
+  }
+
+  return [{visible, editId}, {onClose: setFalse, onEdit}] as const;
+}
+
+export function useHandle() {
+  const [{visible, editId}, {onClose, onEdit}] = useEdit();
+
+  const columns: ColumnsType<DictionaryData> = [
+    {title: '物料名称', dataIndex: 'name', key: 'name'},
+    {title: '物料编号', dataIndex: 'code', key: 'code'},
+    {title: '物料类型', dataIndex: 'materialType', key: 'materialType'},
+    {title: '存储容量', dataIndex: 'size', key: 'size'},
+    {
+      title: '是否混合存储',
+      dataIndex: 'isNotDisable',
+      key: 'isNotDisable',
+      render(_, {isNotDisable}) {
+        return isNotDisable === '1' ? '是' : '否';
+      },
+    },
+    {
+      title: '操作',
+      dataIndex: 'tldId',
+      key: 'tldId',
+      width: 330,
+      render(_, {tldId}) {
+        return (
+          <>
+            <Button
+              type='link'
+              onClick={onEdit(tldId)}
+            >
+              修改
+            </Button>
+          </>
+        );
+      },
+    },
+  ];
+
+  return [{columns, visible, editId}, {onClose}] as const;
+}

+ 31 - 0
packages/app/src/pages/goods/table/index.tsx

@@ -0,0 +1,31 @@
+import {Table} from '@components';
+import {Card} from 'antd';
+import {FC} from 'react';
+import {useHandle, useList} from './hooks';
+import {pageContext, searchContext} from '../context';
+import EditModal from './modal';
+
+const TableList: FC = function() {
+  const [{data, count}, {refetch}] = useList();
+  const [{columns, editId, visible}, {onClose}] = useHandle();
+
+  return (
+    <>
+      <Card className='table-wrapper'>
+
+        <Table
+          data-testid='goods_table'
+          columns={columns}
+          data={data}
+          pageContext={pageContext}
+          searchContext={searchContext}
+          count={count}
+        />
+      </Card>
+
+      <EditModal id={editId} visible={visible} onClose={onClose} onFetch={refetch} />
+    </>
+  );
+};
+
+export default TableList;

+ 42 - 0
packages/app/src/pages/goods/table/modal/Info.tsx

@@ -0,0 +1,42 @@
+import {ModalField, ModalSelect} from '@components';
+import {FC} from 'react';
+import {useControl, useWatchId} from './hooks';
+import {useDictionaryOptions} from '@hooks';
+
+type Props = {id: string};
+
+const options = [
+  {label: '是', value: '1'},
+  {label: '否', value: '0'},
+];
+
+const Info: FC<Props> = function({id}) {
+  const control = useControl();
+  const typeOptions = useDictionaryOptions('物料类型');
+  useWatchId(id);
+
+  return (
+    <>
+      <ModalSelect
+        label='物料类型'
+        name='goodsType'
+        control={control}
+        data={typeOptions}
+      />
+      <ModalField
+        label='物料存放容量'
+        control={control}
+        name='goodsSize'
+        type='number'
+      />
+      <ModalSelect
+        label='混合放置'
+        name='goodsMixin'
+        control={control}
+        data={options}
+      />
+    </>
+  );
+};
+
+export default Info;

+ 80 - 0
packages/app/src/pages/goods/table/modal/hooks.ts

@@ -0,0 +1,80 @@
+import {editDictionary, getDictionaryInfo} from '@apis';
+import {yupResolver} from '@hookform/resolvers/yup';
+import {useQueryDataInfo} from '@hooks';
+import {useMutation} from '@tanstack/react-query';
+import {message} from 'antd';
+import {useEffect} from 'react';
+import {useForm, useFormContext} from 'react-hook-form';
+import {number, object, string} from 'yup';
+
+type FormState = {
+  goodsType: string,
+  goodsSize: number,
+  goodsMixin: string,
+};
+
+const validate = object({
+  goodsType: string().required('请选择物料类型'),
+  goodsSize: number().typeError('请输入数字')
+    .min(1, '不能小于1个').required('请输入物料存储容量'),
+  goodsMixin: string().required('请选择是否混合存储'),
+});
+
+export function useFormState(
+  {visible, id, onClose, onFetch}:
+  {onClose: () => void, onFetch: () => void, visible: boolean, id: string},
+) {
+  const formInstance = useForm<FormState>({
+    defaultValues: {goodsType: '', goodsSize: 1, goodsMixin: ''},
+    resolver: yupResolver(validate),
+  });
+
+  const {clearErrors, handleSubmit} = formInstance;
+
+  useEffect(function() {
+    visible && clearErrors();
+  }, [visible, clearErrors]);
+
+  const {isLoading, mutate} = useMutation({
+    mutationFn: editDictionary,
+    onSuccess({msg}) {
+      if (msg === '200') {
+        onClose();
+        onFetch();
+        message.success('修改成功');
+      }
+    },
+  });
+
+  const onSubmit = handleSubmit(function({goodsMixin, goodsSize, goodsType}) {
+    mutate({
+      isNotDisable: goodsMixin,
+      size: String(goodsSize),
+      id,
+      materialType: goodsType,
+    });
+  });
+
+  return [{formInstance, isLoading}, {onSubmit}] as const;
+}
+
+export function useControl() {
+  const {control} = useFormContext<FormState>();
+
+  return control;
+}
+
+export function useWatchId(id: string) {
+  const {setValue} = useFormContext<FormState>();
+  const data = useQueryDataInfo({
+    queryFn: getDictionaryInfo,
+    params: ['物料字典', id],
+    enabled: Boolean(id),
+  });
+
+  useEffect(function() {
+    setValue('goodsType', data?.wllbClass ?? '');
+    setValue('goodsSize', Number(data?.num ?? '1'));
+    setValue('goodsMixin', data?.isNotDisable ?? '');
+  }, [data, setValue]);
+}

+ 42 - 0
packages/app/src/pages/goods/table/modal/index.tsx

@@ -0,0 +1,42 @@
+import {Loading, Modal, ErrorBoundary, ReactModalStyle, ModalBtnGroup} from '@components';
+import {FC, Suspense} from 'react';
+import {useFormState} from './hooks';
+import {FormProvider} from 'react-hook-form';
+import Info from './Info';
+
+const style: ReactModalStyle = {
+  content: {width: '540px', height: '500px'},
+};
+
+type Props = {
+  onFetch: () => void,
+  onClose: () => void,
+  visible: boolean,
+  id: string
+};
+
+const EditModal: FC<Props> = function({id, visible, onClose, onFetch}) {
+  const [{formInstance, isLoading}, {onSubmit}] = useFormState({id, visible, onClose, onFetch});
+
+  return (
+    <Modal
+      visible={visible}
+      title='修改物料信息'
+      onSubmit={onSubmit}
+      onClose={onClose}
+      style={style}
+      hideBtnGroup
+    >
+      <FormProvider {...formInstance}>
+        <ErrorBoundary>
+          <Suspense fallback={<Loading tip='正在获取' />}>
+            <Info id={id} />
+            <ModalBtnGroup isLoading={isLoading} onClose={onClose} />
+          </Suspense>
+        </ErrorBoundary>
+      </FormProvider>
+    </Modal>
+  );
+};
+
+export default EditModal;