Browse Source

feat: 其他出库 其他入库

xyh 2 years ago
parent
commit
25905fcb26

+ 6 - 4
packages/app/src/index.tsx

@@ -5,7 +5,7 @@ import {createRoot} from 'react-dom/client';
 import {BrowserRouter} from 'react-router-dom';
 import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
 import {ReactQueryDevtools} from '@tanstack/react-query-devtools';
-import {StrictMode} from 'react';
+import {StrictMode, Suspense} from 'react';
 import {ConfigProvider} from 'antd';
 import {ThemeConfig} from 'antd/es/config-provider/context';
 import 'dayjs/locale/zh-cn';
@@ -40,9 +40,11 @@ root.render(
   <StrictMode>
     <QueryClientProvider client={queryClient}>
       <ConfigProvider theme={themeConfig} locale={zhCN}>
-        <BrowserRouter>
-          <RootRoutes />
-        </BrowserRouter>
+        <Suspense>
+          <BrowserRouter>
+            <RootRoutes />
+          </BrowserRouter>
+        </Suspense>
       </ConfigProvider>
       <ReactQueryDevtools initialIsOpen={false} position='bottom-right' />
     </QueryClientProvider>

+ 70 - 0
packages/app/src/pages/stock-operation/field/index.tsx

@@ -0,0 +1,70 @@
+import css from '../index.module.css';
+import {Row, Col, Input} from 'antd';
+import classNames from 'classnames';
+import {FC} from 'react';
+import {Controller, UseControllerProps} from 'react-hook-form';
+
+type Props = {
+  label: string;
+  placeholder?: string;
+  width?: string;
+  type?: string;
+  required?: boolean;
+} & UseControllerProps<any, any>;
+
+const Field: FC<Props> = function({
+  control,
+  name,
+  label,
+  placeholder,
+  type,
+  required,
+}) {
+  return (
+    <Row>
+      <Col>
+        <label
+          className={
+            classNames([
+              css.textRight,
+              {[css.fieldRequired]: required ?? true},
+            ])
+          }
+          htmlFor={`field_${name}`}
+        >
+          {label}
+        </label>
+      </Col>
+      <Col flex={1}>
+        <Controller
+          name={name}
+          control={control}
+          render={function({field: {value, onChange, ref}, formState: {errors}}) {
+            const errorTips = errors[name]?.message?.toString() ?? '';
+            return (
+              <>
+                <Input
+                  className={classNames([css.modalField, css.filedWidth])}
+                  name={name}
+                  placeholder={placeholder ?? '请输入'}
+                  id={`field_${name}`}
+                  data-testid={`field_${name}`}
+                  value={value}
+                  onChange={onChange}
+                  ref={ref}
+                  type={type}
+                />
+                <p className={classNames([css.errorTips, {[css.errorTipsHidden]: !errorTips}])}>
+                  {errorTips}
+                </p>
+              </>
+            );
+          }}
+        />
+
+      </Col>
+    </Row>
+  );
+};
+
+export default Field;

+ 34 - 0
packages/app/src/pages/stock-operation/hooks.ts

@@ -0,0 +1,34 @@
+import {yupResolver} from '@hookform/resolvers/yup';
+import {useForm} from 'react-hook-form';
+import {number, object, string} from 'yup';
+
+type FormState = {
+  /** 物料code */
+  materialCode: string;
+  /** 出入库数量 */
+  operationNum: number;
+  /** wbs编号 */
+  wbsCode: string;
+};
+
+const validate = object({
+  materialCode: string().required('请选择物料'),
+  operationNum: number().typeError('请输入数字类型').min(1, '数量不能小于1个'),
+});
+
+export function useFormState() {
+  const {control, clearErrors, reset, handleSubmit} = useForm<FormState>({
+    defaultValues: {
+      materialCode: '',
+      operationNum: 0,
+      wbsCode: '',
+    },
+    resolver: yupResolver(validate),
+  });
+
+  const onSubmit = handleSubmit(function({materialCode, operationNum, wbsCode}) {
+    console.log(materialCode, operationNum, wbsCode);
+  });
+
+  return [{control}, {onSubmit}] as const;
+}

+ 56 - 0
packages/app/src/pages/stock-operation/index.module.css

@@ -0,0 +1,56 @@
+.error-tips {
+  height: 14px;
+  margin: 4px 0 8px;
+  font-size: 14px;
+  color: var(--error-color);
+}
+
+.error-tips-hidden {
+  visibility: hidden;
+}
+
+.text-right {
+  display: block;
+  width: 80px;
+  height: 32px;
+  margin-right: 12px;
+  line-height: 2.2;
+  color: #666;
+  text-align: right;
+}
+
+.field-required {
+  &::before {
+    padding-right: 4px;
+    color: #f20c00;
+    content: '*';
+  }
+}
+
+.filed-width {
+  width: 260px;
+}
+
+.area-filed {
+  width: 100%;
+  resize: none !important;
+}
+
+.modal-field {
+  padding-right: 0;
+  padding-left: 0;
+  border: none;
+  border-bottom: 1px solid #e4e4e4;
+  border-radius: unset;
+  box-shadow: unset !important;
+}
+
+.modal-select {
+  & :global(.ant-select-selector) {
+    padding: 0 !important;
+    border: unset !important;
+    border-bottom: 1px solid #e4e4e4 !important;
+    border-radius: unset;
+    box-shadow: unset !important;
+  }
+}

+ 46 - 0
packages/app/src/pages/stock-operation/index.tsx

@@ -0,0 +1,46 @@
+import {Card, Space} from 'antd';
+import {FC} from 'react';
+import {useParams} from 'react-router-dom';
+import {useFormState} from './hooks';
+import Field from './field';
+import {ModalBtnGroup} from '@components';
+import Select from './select';
+import {useDictionaryOptions} from '@hooks';
+
+const StockOperation: FC = function() {
+  const {type} = useParams<{type: 'in' | 'out'}>();
+  const [{control}, {onSubmit}] = useFormState();
+  const options = useDictionaryOptions('物料字典');
+
+  return (
+    <section className='content-main'>
+      <Card title={type === 'in' ? '其他入库' : '其他出库'}>
+        <form onSubmit={onSubmit}>
+          <Space direction='vertical'>
+            <Select
+              control={control}
+              label='物料'
+              name='materialCode'
+              data={options}
+            />
+            <Field
+              control={control}
+              label={type === 'in' ? '入库数量' : '出库数量'}
+              name='operationNum'
+              type='number'
+            />
+            <Field
+              control={control}
+              label='wbs编号'
+              name='wbsCode'
+              required={false}
+            />
+            <ModalBtnGroup />
+          </Space>
+        </form>
+      </Card>
+    </section>
+  );
+};
+
+export default StockOperation;

+ 75 - 0
packages/app/src/pages/stock-operation/select/index.tsx

@@ -0,0 +1,75 @@
+import css from '../index.module.css';
+import {Row, Col, Select as AntdSelect} from 'antd';
+import classNames from 'classnames';
+import {FC} from 'react';
+import {Controller, UseControllerProps} from 'react-hook-form';
+
+type Props = {
+  label: string;
+  placeholder?: string;
+  width?: string;
+  data: {value: string, label: string}[];
+  required?: boolean;
+} & UseControllerProps<any, any>;
+
+const Select: FC<Props> = function({
+  control,
+  name,
+  label,
+  placeholder,
+  data,
+  required,
+}) {
+  return (
+    <Row>
+      <Col>
+        <label
+          className={
+            classNames([
+              css.textRight,
+              {[css.fieldRequired]: required ?? true},
+            ])
+          }
+          htmlFor={`operation_${name}`}
+        >
+          {label}
+        </label>
+      </Col>
+      <Col flex={1}>
+        <Controller
+          name={name}
+          control={control}
+          render={function({field: {value, onChange, ref}, formState: {errors}}) {
+            const errorTips = errors[name]?.message?.toString() ?? '';
+
+            return (
+              <>
+                <AntdSelect
+                  rootClassName={classNames([css.modalSelect, css.filedWidth])}
+                  data-testid={`select_${name}`}
+                  defaultActiveFirstOption={false}
+                  filterOption={false}
+                  placeholder={placeholder ?? '请选择'}
+                  id={`select_${name}`}
+                  // 处理placeholder不显示问题
+                  value={value.length ? value : void 0}
+                  onChange={onChange}
+                  ref={ref}
+                  options={data}
+                  className='width-full'
+                  getPopupContainer={(node: HTMLElement) => node.parentNode as HTMLElement}
+                />
+                <p className={classNames([css.errorTips, {[css.errorTipsHidden]: !errorTips}])}>
+                  {errorTips}
+                </p>
+              </>
+            );
+          }}
+        />
+
+      </Col>
+    </Row>
+  );
+};
+
+export default Select;

+ 0 - 2
packages/app/src/routes/index.ts

@@ -1,2 +0,0 @@
-export * from './routes';
-export * from './name';

+ 94 - 0
packages/app/src/routes/index.tsx

@@ -0,0 +1,94 @@
+import {FC} from 'react';
+import {Home, Main, Login} from '@pages';
+import {RouteObject, useRoutes} from 'react-router-dom';
+import {
+  HOME_PATH,
+  MAIN_PATH,
+  DEPARTMENT_PATH,
+  MENU_PATH,
+  ROLE_PATH,
+  USER_PATH,
+  STORAGE_PATH,
+  GOODS_PATH,
+  PDA_MENU_PATH,
+  CONTAINER_PATH,
+  MATTER_PATH,
+  RECEIVE_PATH,
+  RECEIVE_TIMEOUT_PATH,
+  RAW_IN_STREAM_PATH,
+  RAW_OUT_STREAM_PATH,
+  SEMI_REPORT_PATH,
+  SEMI_DRAW_PATH,
+  SEMI_IN_STREAM_PATH,
+  SEMI_OUT_STREAM_PATH,
+  LOGIN_PATH,
+  NO_PERMISSION_PATH,
+  FINISH_PRODUCT_IN_STREAM_PATH,
+  FINISH_PRODUCT_OUT_STREAM_PATH,
+  STOCK_OPERATION_PATH,
+} from './name';
+import {
+  Container,
+  Department,
+  Goods,
+  Matter,
+  Menu,
+  NoPermision,
+  NotFound,
+  Pda,
+  RawInStream,
+  RawOutStream,
+  Receive,
+  ReceiveTimeout,
+  Role,
+  SemiDraw,
+  SemiInStream,
+  SemiOutStream,
+  SemiReport,
+  User,
+  Storage,
+  ProductInStream,
+  ProductOutStream,
+  StockOperation,
+} from './routes';
+
+export const routes: RouteObject[] = [
+  {
+    path: HOME_PATH,
+    element: <Home />,
+    children: [
+      {path: MAIN_PATH, element: <Main />},
+      {path: DEPARTMENT_PATH, element: <Department />},
+      {path: MENU_PATH, element: <Menu />},
+      {path: ROLE_PATH, element: <Role />},
+      {path: USER_PATH, element: <User />},
+      {path: STORAGE_PATH, element: <Storage />},
+      {path: GOODS_PATH, element: <Goods />},
+      {path: PDA_MENU_PATH, element: <Pda />},
+      {path: CONTAINER_PATH, element: <Container />},
+      {path: MATTER_PATH, element: <Matter />},
+      {path: RECEIVE_PATH, element: <Receive />},
+      {path: RECEIVE_TIMEOUT_PATH, element: <ReceiveTimeout />},
+      {path: RAW_IN_STREAM_PATH, element: <RawInStream />},
+      {path: RAW_OUT_STREAM_PATH, element: <RawOutStream />},
+      {path: SEMI_REPORT_PATH, element: <SemiReport />},
+      {path: SEMI_DRAW_PATH, element: <SemiDraw />},
+      {path: SEMI_IN_STREAM_PATH, element: <SemiInStream />},
+      {path: SEMI_OUT_STREAM_PATH, element: <SemiOutStream />},
+      {path: FINISH_PRODUCT_IN_STREAM_PATH, element: <ProductInStream />},
+      {path: FINISH_PRODUCT_OUT_STREAM_PATH, element: <ProductOutStream />},
+      {path: STOCK_OPERATION_PATH, element: <StockOperation />},
+    ],
+  },
+  {path: LOGIN_PATH, element: <Login />},
+  {path: NO_PERMISSION_PATH, element: <NoPermision />},
+  {path: '*', element: <NotFound />},
+];
+
+export const RootRoutes: FC = function() {
+  const Routes = useRoutes(routes);
+
+  return Routes;
+};
+
+export * from './name';

+ 12 - 2
packages/app/src/routes/name.ts

@@ -38,6 +38,16 @@ export const SEMI_REPORT_PATH = '/semi/report';
 /** 半成品领料单 */
 export const SEMI_DRAW_PATH = '/semi/draw';
 /** 半成品入库流水 */
-export const SEMI_IN_STREAM = '/stream/semiin';
+export const SEMI_IN_STREAM_PATH = '/stream/semiin';
 /** 半成品出库流水 */
-export const SEMI_OUT_STREAM = '/stream/semiout';
+export const SEMI_OUT_STREAM_PATH = '/stream/semiout';
+/** 产成品入库流水 */
+export const FINISH_PRODUCT_IN_STREAM_PATH = '/stream/productin';
+/** 产成品出库流水 */
+export const FINISH_PRODUCT_OUT_STREAM_PATH = '/stream/productout';
+/**
+ * 其他出库和其他入库
+ * type = in 入库
+ * type = out 出库
+ *  */
+export const STOCK_OPERATION_PATH = '/stock/:type';

+ 56 - 81
packages/app/src/routes/routes.tsx

@@ -1,111 +1,86 @@
-import {FC, lazy} from 'react';
-import {Home, Main, Login} from '@pages';
-import {
-  CONTAINER_PATH,
-  DEPARTMENT_PATH,
-  GOODS_PATH,
-  HOME_PATH,
-  LOGIN_PATH,
-  MAIN_PATH,
-  MATTER_PATH,
-  MENU_PATH,
-  NO_PERMISSION_PATH,
-  PDA_MENU_PATH,
-  RAW_IN_STREAM_PATH,
-  RAW_OUT_STREAM_PATH,
-  RECEIVE_PATH,
-  RECEIVE_TIMEOUT_PATH,
-  ROLE_PATH,
-  SEMI_DRAW_PATH,
-  SEMI_IN_STREAM,
-  SEMI_OUT_STREAM,
-  SEMI_REPORT_PATH,
-  STORAGE_PATH,
-  USER_PATH,
-} from './name';
-import {RouteObject, useRoutes} from 'react-router-dom';
+import {lazy} from 'react';
 
-const NotFound = lazy(() => import(
+export const NotFound = lazy(() => import(
   /* webpackChunkName: "notFoundPage" */
   /* webpackPrefetch: true */
   '@pages/not-found'
 ));
-const Department = lazy(() => import(
+export const Department = lazy(() => import(
   /* webpackChunkName: "departmentPage" */
   '@pages/department'
 ));
-const Menu = lazy(() => import(/* webpackChunkName: "menuPage" */'@pages/menu'));
-const Role = lazy(() => import(/* webpackChunkName: "rolePage" */'@pages/role'));
-const NoPermision = lazy(() => import(
+export const Menu = lazy(() => import(/* webpackChunkName: "menuPage" */'@pages/menu'));
+export const Role = lazy(() => import(/* webpackChunkName: "rolePage" */'@pages/role'));
+export const NoPermision = lazy(() => import(
   /* webpackChunkName: "noPermissionPage" */
   /* webpackPrefetch: true */
   '@pages/no-permission'
 ));
-const User = lazy(() => import(/* webpackChunkName: "userPage" */'@pages/user'));
-const Storage = lazy(() => import(/* webpackChunkName: "storagePage" */'@pages/storage'));
-const Goods = lazy(() => import(/* webpackChunkName: "goods" */'@pages/goods'));
-const Pda = lazy(() => import(/* webpackChunkName: "pdaMenu" */'@pages/pda-menu'));
-const Container = lazy(() => import(/* webpackChunkName: "container" */'@pages/container'));
-const Matter = lazy(() => import(/* webpackChunkName: "matter" */'@pages/matter'));
-const Receive = lazy(() => import(/* webpackChunkName: "receive" */'@pages/receive'));
-const ReceiveTimeout = lazy(() => import(
+export const User = lazy(() => import(
+  /* webpackChunkName: "userPage" */
+  '@pages/user'
+));
+export const Storage = lazy(() => import(
+  /* webpackChunkName: "storagePage" */
+  '@pages/storage'
+));
+export const Goods = lazy(() => import(
+  /* webpackChunkName: "goods" */
+  '@pages/goods'
+));
+export const Pda = lazy(() => import(
+  /* webpackChunkName: "pdaMenu" */
+  '@pages/pda-menu'
+));
+export const Container = lazy(() => import(
+  /* webpackChunkName: "container" */
+  '@pages/container'
+));
+export const Matter = lazy(() => import(
+  /* webpackChunkName: "matter" */
+  '@pages/matter'
+));
+export const Receive = lazy(() => import(
+  /* webpackChunkName: "receive" */
+  '@pages/receive'
+));
+export const ReceiveTimeout = lazy(() => import(
   /* webpackChunkName: "receiveTimeout" */
   '@pages/receive-timeout'
 ));
-const RawInStream = lazy(() => import(
+export const RawInStream = lazy(() => import(
   /* webpackChunkName: "rawInStream" */
   '@pages/raw-in-stream'
 ));
-const RawOutStream = lazy(() => import(
+export const RawOutStream = lazy(() => import(
   /* webpackChunkName: "rawOutStream" */
   '@pages/raw-out-stream'
 ));
-const SemiReport = lazy(() => import(
+export const SemiReport = lazy(() => import(
   /* webpackChunkName: "semiReposrt" */
   '@pages/semi-report'
 ));
-const SemiInStream = lazy(() => import(
+export const SemiInStream = lazy(() => import(
   /* webpackChunkName: "semiInStream" */
   '@pages/semi-in-stream'
 ));
-const SemiDraw = lazy(() => import(/* webpackChunkName: "semiDraw" */'@pages/semi-draw'));
-const SemiOutStream = lazy(() => import(
+export const SemiDraw = lazy(() => import(
+  /* webpackChunkName: "semiDraw" */
+  '@pages/semi-draw'
+));
+export const SemiOutStream = lazy(() => import(
   /* webpackChunkName: "semiOutStream" */
   '@pages/semi-out-stream'
 ));
-
-const routes: RouteObject[] = [
-  {
-    path: HOME_PATH,
-    element: <Home />,
-    children: [
-      {path: MAIN_PATH, element: <Main />},
-      {path: DEPARTMENT_PATH, element: <Department />},
-      {path: MENU_PATH, element: <Menu />},
-      {path: ROLE_PATH, element: <Role />},
-      {path: USER_PATH, element: <User />},
-      {path: STORAGE_PATH, element: <Storage />},
-      {path: GOODS_PATH, element: <Goods />},
-      {path: PDA_MENU_PATH, element: <Pda />},
-      {path: CONTAINER_PATH, element: <Container />},
-      {path: MATTER_PATH, element: <Matter />},
-      {path: RECEIVE_PATH, element: <Receive />},
-      {path: RECEIVE_TIMEOUT_PATH, element: <ReceiveTimeout />},
-      {path: RAW_IN_STREAM_PATH, element: <RawInStream />},
-      {path: RAW_OUT_STREAM_PATH, element: <RawOutStream />},
-      {path: SEMI_REPORT_PATH, element: <SemiReport />},
-      {path: SEMI_DRAW_PATH, element: <SemiDraw />},
-      {path: SEMI_IN_STREAM, element: <SemiInStream />},
-      {path: SEMI_OUT_STREAM, element: <SemiOutStream />},
-    ],
-  },
-  {path: LOGIN_PATH, element: <Login />},
-  {path: NO_PERMISSION_PATH, element: <NoPermision />},
-  {path: '*', element: <NotFound />},
-];
-
-export const RootRoutes: FC = function() {
-  const Routes = useRoutes(routes);
-
-  return Routes;
-};
+export const ProductInStream = lazy(() => import(
+  /* webpackChunkName: "product-in-stream" */
+  '@pages/product-in-stream'
+));
+export const ProductOutStream = lazy(() => import(
+  /* webpackChunkName: "product-in-stream" */
+  '@pages/product-out-stream'
+));
+export const StockOperation = lazy(() => import(
+  /* webpackChunkName: "stockOperation" */
+  '@pages/stock-operation'
+));