Lind 4 лет назад
Родитель
Сommit
cadacd2a90

BIN
public/images/login1.png


+ 0 - 202
src/components/Upload/Upload.tsx

@@ -1,202 +0,0 @@
-import React, { useEffect } from 'react';
-import type { Field } from '@formily/core';
-import { connect, mapProps, useField } from '@formily/react';
-import { Upload as AntdUpload, Button } from 'antd';
-import type {
-  UploadChangeParam,
-  UploadProps as AntdUploadProps,
-  DraggerProps as AntdDraggerProps,
-} from 'antd/lib/upload';
-import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
-import { reaction } from '@formily/reactive';
-import type { UploadFile } from 'antd/lib/upload/interface';
-import { isArr, toArr } from '@formily/shared';
-import { UPLOAD_PLACEHOLDER } from './placeholder';
-import { usePrefixCls } from '@formily/antd/lib/__builtins__';
-
-type UploadProps = Omit<AntdUploadProps, 'onChange'> & {
-  textContent?: React.ReactNode;
-  onChange?: (fileList: UploadFile[]) => void;
-  serviceErrorMessage?: string;
-};
-
-type DraggerProps = Omit<AntdDraggerProps, 'onChange'> & {
-  textContent?: React.ReactNode;
-  onChange?: (fileList: UploadFile[]) => void;
-  serviceErrorMessage?: string;
-};
-
-type ComposedUpload = React.FC<UploadProps> & {
-  Dragger?: React.FC<DraggerProps>;
-};
-
-type IUploadProps = {
-  serviceErrorMessage?: string;
-  onChange?: (...args: any) => void;
-};
-
-const testOpts = (ext: RegExp, options: { exclude?: string[]; include?: string[] }) => {
-  if (options && isArr(options.include)) {
-    return options.include.some((url) => ext.test(url));
-  }
-
-  if (options && isArr(options.exclude)) {
-    return !options.exclude.some((url) => ext.test(url));
-  }
-
-  return true;
-};
-
-const getImageByUrl = (url: string, options: any) => {
-  for (let i = 0; i < UPLOAD_PLACEHOLDER.length; i += 1) {
-    if (UPLOAD_PLACEHOLDER[i].ext.test(url) && testOpts(UPLOAD_PLACEHOLDER[i].ext, options)) {
-      return UPLOAD_PLACEHOLDER[i].icon || url;
-    }
-  }
-
-  return url;
-};
-
-const getURL = (target: any) => {
-  return target?.['result'] || target?.['url'] || target?.['downloadURL'] || target?.['imgURL'];
-};
-const getThumbURL = (target: any) => {
-  return (
-    target?.['result'] ||
-    target?.['thumbUrl'] ||
-    target?.['url'] ||
-    target?.['downloadURL'] ||
-    target?.['imgURL']
-  );
-};
-
-const getErrorMessage = (target: any) => {
-  return target?.errorMessage ||
-    target?.errMsg ||
-    target?.errorMsg ||
-    target?.message ||
-    typeof target?.error === 'string'
-    ? target.error
-    : '';
-};
-
-const getState = (target: any) => {
-  if (target?.success === false) return 'error';
-  if (target?.failed === true) return 'error';
-  if (target?.error) return 'error';
-  return target?.state || target?.status;
-};
-
-const normalizeFileList = (fileList: UploadFile[]) => {
-  if (fileList && fileList.length) {
-    return fileList.map((file, index) => {
-      return {
-        ...file,
-        uid: file.uid || `${index}`,
-        status: getState(file.response) || getState(file),
-        url: getURL(file) || getURL(file?.response),
-        thumbUrl: getImageByUrl(getThumbURL(file) || getThumbURL(file?.response), {
-          exclude: ['.png', '.jpg', '.jpeg', '.gif'],
-        }),
-      };
-    });
-  }
-  return [];
-};
-
-const useValidator = (validator: (value: any) => string) => {
-  const field = useField<Field>();
-  useEffect(() => {
-    const dispose = reaction(
-      () => field.value,
-      (value) => {
-        const message = validator(value);
-        field.setFeedback({
-          type: 'error',
-          code: 'UploadError',
-          messages: message ? [message] : [],
-        });
-      },
-    );
-    return () => {
-      dispose();
-    };
-  }, []);
-};
-
-const useUploadValidator = (serviceErrorMessage = 'Upload Service Error') => {
-  // eslint-disable-next-line consistent-return
-  useValidator((value) => {
-    const list = toArr(value);
-    for (let i = 0; i < list.length; i += 1) {
-      if (list[i]?.status === 'error') {
-        return (
-          getErrorMessage(list[i]?.response) || getErrorMessage(list[i]) || serviceErrorMessage
-        );
-      }
-    }
-  });
-};
-
-function useUploadProps<T extends IUploadProps = UploadProps>({
-  serviceErrorMessage,
-  ...props
-}: T) {
-  useUploadValidator(serviceErrorMessage);
-  const onChange = (param: UploadChangeParam<UploadFile>) => {
-    props.onChange?.(normalizeFileList([...param.fileList]));
-  };
-  return {
-    ...props,
-    onChange,
-  };
-}
-
-const getPlaceholder = (props: UploadProps) => {
-  if (props.listType !== 'picture-card') {
-    return (
-      <Button>
-        <UploadOutlined />
-        {props.textContent}
-      </Button>
-    );
-  }
-  return <UploadOutlined style={{ fontSize: 20 }} />;
-};
-
-export const Upload: ComposedUpload = connect(
-  (props: React.PropsWithChildren<UploadProps>) => {
-    return (
-      <AntdUpload {...useUploadProps(props)}>{props.children || getPlaceholder(props)}</AntdUpload>
-    );
-  },
-  mapProps({
-    value: 'fileList',
-  }),
-);
-
-const Dragger = connect(
-  (props: React.PropsWithChildren<DraggerProps>) => {
-    return (
-      <div className={usePrefixCls('upload-dragger')}>
-        <AntdUpload.Dragger {...useUploadProps(props)}>
-          {props.children || (
-            <React.Fragment>
-              <p className="ant-upload-drag-icon">
-                <InboxOutlined />
-              </p>
-              {props.textContent && <p className="ant-upload-text">{props.textContent}</p>}
-            </React.Fragment>
-          )}
-        </AntdUpload.Dragger>
-      </div>
-    );
-  },
-  mapProps({
-    value: 'fileList',
-  }),
-);
-
-Upload.Dragger = Dragger;
-
-export default Upload;

+ 38 - 8
src/components/Upload/index.tsx

@@ -1,20 +1,50 @@
-import { Button } from 'antd';
-import { UploadOutlined } from '@ant-design/icons';
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
 import SystemConst from '@/utils/const';
 import Token from '@/utils/token';
-import Upload from '@/components/Upload/Upload';
+import { useState } from 'react';
+import { connect } from '@formily/react';
+import { Upload } from 'antd';
+import type { UploadChangeParam } from 'antd/lib/upload/interface';
 
-const FUpload = (props: any) => {
+interface Props {
+  value: string;
+  onChange: (value: string) => void;
+}
+
+const FUploadImage = connect((props: Props) => {
+  console.log(props, 'pro');
+  const [url, setUrl] = useState<string>(props?.value);
+  const [loading, setLoading] = useState<boolean>(false);
+  const uploadButton = (
+    <div>
+      {loading ? <LoadingOutlined /> : <PlusOutlined />}
+      <div style={{ marginTop: 8 }}>选择图片</div>
+    </div>
+  );
+  const handleChange = (info: UploadChangeParam) => {
+    console.log(info);
+    if (info.file.status === 'uploading') {
+      setLoading(false);
+    }
+    if (info.file.status === 'done') {
+      info.file.url = info.file.response?.result;
+      setLoading(false);
+      setUrl(info.file.response?.result);
+      props.onChange(info.file.response?.result);
+    }
+  };
   return (
     <Upload
-      {...props}
+      listType="picture-card"
       action={`/${SystemConst.API_BASE}/file/static`}
       headers={{
         'X-Access-Token': Token.get(),
       }}
+      onChange={handleChange}
+      showUploadList={false}
     >
-      <Button icon={<UploadOutlined />}>{props.title}</Button>
+      {url ? <img src={url} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
     </Upload>
   );
-};
-export default FUpload;
+});
+export default FUploadImage;

+ 0 - 62
src/components/Upload/placeholder.ts

@@ -1,62 +0,0 @@
-export const UPLOAD_PLACEHOLDER = [
-  {
-    ext: /\.docx?$/i,
-    icon: '//img.alicdn.com/tfs/TB1n8jfr1uSBuNjy1XcXXcYjFXa-200-200.png',
-  },
-  {
-    ext: /\.pptx?$/i,
-    icon: '//img.alicdn.com/tfs/TB1ItgWr_tYBeNjy1XdXXXXyVXa-200-200.png',
-  },
-  {
-    ext: /\.jpe?g$/i,
-    icon: '//img.alicdn.com/tfs/TB1wrT5r9BYBeNjy0FeXXbnmFXa-200-200.png',
-  },
-  {
-    ext: /\.pdf$/i,
-    icon: '//img.alicdn.com/tfs/TB1GwD8r9BYBeNjy0FeXXbnmFXa-200-200.png',
-  },
-  {
-    ext: /\.png$/i,
-    icon: '//img.alicdn.com/tfs/TB1BHT5r9BYBeNjy0FeXXbnmFXa-200-200.png',
-  },
-  {
-    ext: /\.eps$/i,
-    icon: '//img.alicdn.com/tfs/TB1G_iGrVOWBuNjy0FiXXXFxVXa-200-200.png',
-  },
-  {
-    ext: /\.ai$/i,
-    icon: '//img.alicdn.com/tfs/TB1B2cVr_tYBeNjy1XdXXXXyVXa-200-200.png',
-  },
-  {
-    ext: /\.gif$/i,
-    icon: '//img.alicdn.com/tfs/TB1DTiGrVOWBuNjy0FiXXXFxVXa-200-200.png',
-  },
-  {
-    ext: /\.svg$/i,
-    icon: '//img.alicdn.com/tfs/TB1uUm9rY9YBuNjy0FgXXcxcXXa-200-200.png',
-  },
-  {
-    ext: /\.xlsx?$/i,
-    icon: '//img.alicdn.com/tfs/TB1any1r1OSBuNjy0FdXXbDnVXa-200-200.png',
-  },
-  {
-    ext: /\.psd?$/i,
-    icon: '//img.alicdn.com/tfs/TB1_nu1r1OSBuNjy0FdXXbDnVXa-200-200.png',
-  },
-  {
-    ext: /\.(wav|aif|aiff|au|mp1|mp2|mp3|ra|rm|ram|mid|rmi)$/i,
-    icon: '//img.alicdn.com/tfs/TB1jPvwr49YBuNjy0FfXXXIsVXa-200-200.png',
-  },
-  {
-    ext: /\.(avi|wmv|mpg|mpeg|vob|dat|3gp|mp4|mkv|rm|rmvb|mov|flv)$/i,
-    icon: '//img.alicdn.com/tfs/TB1FrT5r9BYBeNjy0FeXXbnmFXa-200-200.png',
-  },
-  {
-    ext: /\.(zip|rar|arj|z|gz|iso|jar|ace|tar|uue|dmg|pkg|lzh|cab)$/i,
-    icon: '//img.alicdn.com/tfs/TB10jmfr29TBuNjy0FcXXbeiFXa-200-200.png',
-  },
-  {
-    ext: /\.[^.]+$/i,
-    icon: '//img.alicdn.com/tfs/TB10.R4r3mTBuNjy1XbXXaMrVXa-200-200.png',
-  },
-];

+ 0 - 1
src/components/Upload/style.ts

@@ -1 +0,0 @@
-import 'antd/lib/upload/style/index';

+ 223 - 0
src/pages/device/Product/Save/index.tsx

@@ -0,0 +1,223 @@
+import { Button, Drawer, message } from 'antd';
+import type { Field } from '@formily/core';
+import { createForm, onFieldValueChange } from '@formily/core';
+import { TreeSelect, Form, FormItem, FormLayout, Input, Radio, Select } from '@formily/antd';
+import { createSchemaField } from '@formily/react';
+import type { ISchema } from '@formily/json-schema';
+import FUploadImage from '@/components/Upload';
+import { service } from '@/pages/device/Product';
+import { action } from '@formily/reactive';
+import 'antd/lib/tree-select/style/index.less';
+import type { ProductItem } from '@/pages/device/Product/typings';
+
+interface Props {
+  visible: boolean;
+  close: () => void;
+  data?: ProductItem;
+}
+
+/**
+ * 处理品类数据
+ * @param tree
+ */
+const treeToArray = (tree: any) => {
+  const arr: any[] = [];
+  const expanded = (datas: any[]) => {
+    if (datas && datas.length > 0) {
+      datas.forEach((e) => {
+        arr.push(e);
+        expanded(e.children);
+      });
+    }
+  };
+  expanded(tree);
+  return arr;
+};
+
+const Save = (props: Props) => {
+  const { visible, close, data } = props;
+  const handleData = () => {
+    // 特殊处理deviceType字段
+    console.log(data, '处理钱');
+    if (data) {
+      if (typeof data.deviceType !== 'string') {
+        data.deviceType = data.deviceType?.value;
+      }
+    }
+
+    console.log(data, '初始化数据');
+    return data;
+  };
+  const form = createForm({
+    initialValues: handleData(),
+    effects() {
+      onFieldValueChange('messageProtocol', (field, f) => {
+        const protocol = (field as Field).value;
+        f.setFieldState('transportProtocol', async (state) => {
+          state.loading = true;
+          const resp = await service.getTransport(protocol);
+          state.dataSource = resp.result.map((item: { name: string; id: string }) => ({
+            label: item.name,
+            value: item.id,
+          }));
+          state.loading = false;
+        });
+      });
+    },
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Select,
+      Radio,
+      FUploadImage,
+      FormLayout,
+      TreeSelect,
+    },
+  });
+  const serviceMap = new Map<string, Promise<any>>();
+  serviceMap.set('classifiedId', service.category());
+  serviceMap.set('protocol', service.getProtocol());
+  serviceMap.set('storePolicy', service.getStorage());
+  serviceMap.set('org', service.getOrg());
+
+  let classifiedList: any[] = [];
+  const useAsyncDataSource = (type: string) => (field: Field) => {
+    field.loading = true;
+    serviceMap.get(type)!.then(
+      action.bound!((resp) => {
+        if (type === 'classifiedId') {
+          // 处理key错误
+          field.dataSource = resp.result.map((item: Record<string, unknown>) => ({
+            ...item,
+            key: item.id,
+          }));
+          // 考虑冗余分类字段
+          classifiedList = resp.result;
+        } else {
+          field.dataSource = resp.result.map((item: { name: any; id: any }) => ({
+            label: item.name,
+            value: item.id,
+          }));
+        }
+        field.loading = false;
+      }),
+    );
+  };
+
+  const handleSave = async () => {
+    const values = (await form.submit()) as any;
+    // 冗余classifiedName 字段;
+    // 如果只存储string。 可考虑字段解构方式处理
+    // 可能存在数据反显问题,此处考虑与后台协商处理
+    const classifiedId = values.classifiedId;
+    if (classifiedId) {
+      const tempClassifiedList = treeToArray(classifiedList);
+      const classified = tempClassifiedList.find((i) => i.id === classifiedId);
+      // values.classifiedId = classifiedId[classifiedId.length - 1];
+      values.classfiedName = classified.name;
+    }
+    const resp = (await service.update(values)) as any;
+    if (resp.status === 200) {
+      message.success('保存成功');
+      props.close();
+    }
+  };
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-component': 'FormLayout',
+        'x-component-props': {
+          labelCol: 4,
+          wrapperCol: 18,
+        },
+        properties: {
+          photoUrl: {
+            title: '图标',
+            'x-component': 'FUploadImage',
+            'x-decorator': 'FormItem',
+          },
+          id: {
+            title: 'ID',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+          },
+          name: {
+            title: '名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+          },
+          classifiedId: {
+            title: '所属品类',
+            'x-component': 'TreeSelect',
+            'x-decorator': 'FormItem',
+            'x-component-props': {
+              fieldNames: { label: 'name', value: 'id', children: 'children', key: 'id' },
+            },
+            'x-reactions': ['{{useAsyncDataSource("classifiedId")}}'],
+          },
+          orgId: {
+            title: '所属机构',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-reactions': ['{{useAsyncDataSource("org")}}'],
+          },
+          messageProtocol: {
+            title: '消息协议',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-reactions': ['{{useAsyncDataSource("protocol")}}'],
+          },
+          transportProtocol: {
+            title: '传输协议',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+          },
+          storePolicy: {
+            title: '存储策略',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            'x-reactions': ['{{useAsyncDataSource("storePolicy")}}'],
+          },
+          deviceType: {
+            title: '设备类型',
+            'x-component': 'Radio.Group',
+            'x-decorator': 'FormItem',
+            enum: [
+              { label: '直连设备', value: 'device' },
+              { label: '网关子设备', value: 'childrenDevice' },
+              { label: '网关设备', value: 'gateway' },
+            ],
+          },
+          describe: {
+            title: '描述',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+          },
+        },
+      },
+    },
+  };
+  return (
+    <Drawer
+      visible={visible}
+      onClose={() => close()}
+      width="25vw"
+      title="新增产品"
+      extra={
+        <Button type="primary" onClick={handleSave}>
+          保存数据
+        </Button>
+      }
+    >
+      <Form form={form} size="small">
+        <SchemaField schema={schema} scope={{ useAsyncDataSource }} />
+      </Form>
+    </Drawer>
+  );
+};
+export default Save;

+ 25 - 5
src/pages/device/Product/index.tsx

@@ -16,11 +16,11 @@ import { model } from '@formily/reactive';
 import { Link } from 'umi';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import ProTable from '@jetlinks/pro-table';
 import { lastValueFrom } from 'rxjs';
 import encodeQuery from '@/utils/encodeQuery';
-import { CurdModel } from '@/components/BaseCrud/model';
+import Save from '@/pages/device/Product/Save';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -34,6 +34,8 @@ export const productModel = model<{
 });
 
 const Product = observer(() => {
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<ProductItem>();
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
   const status = {
@@ -115,7 +117,13 @@ const Product = observer(() => {
           })}
           key={'edit'}
         >
-          <a key="warning">
+          <a
+            key="warning"
+            onClick={() => {
+              setCurrent(record);
+              setVisible(true);
+            }}
+          >
             <EditOutlined />
           </a>
         </Tooltip>,
@@ -163,7 +171,6 @@ const Product = observer(() => {
     },
   ];
 
-  // const schema = {};
   return (
     <PageContainer>
       <ProTable<ProductItem>
@@ -178,7 +185,12 @@ const Product = observer(() => {
         rowKey="id"
         pagination={{ pageSize: 10 }}
         toolBarRender={() => [
-          <Button onClick={CurdModel.add} key="button" icon={<PlusOutlined />} type="primary">
+          <Button
+            onClick={() => setVisible(true)}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
             {intl.formatMessage({
               id: 'pages.data.option.add',
               defaultMessage: '新增',
@@ -186,6 +198,14 @@ const Product = observer(() => {
           </Button>,
         ]}
       />
+      <Save
+        data={current}
+        close={() => {
+          setVisible(false);
+          actionRef.current?.reload();
+        }}
+        visible={visible}
+      />
     </PageContainer>
   );
 });

+ 39 - 2
src/pages/device/Product/service.ts

@@ -5,7 +5,7 @@ import SystemConst from '@/utils/const';
 import { concatMap, defer, from, toArray } from 'rxjs';
 import { filter, map } from 'rxjs/operators';
 import encodeQuery from '@/utils/encodeQuery';
-import type { Response } from '@/utils/typings';
+import type { PageResult, Response } from '@/utils/typings';
 import _ from 'lodash';
 
 class Service extends BaseService<ProductItem> {
@@ -15,7 +15,7 @@ class Service extends BaseService<ProductItem> {
   public queryZipCount = (params: any) =>
     from(this.query(params)).pipe(
       concatMap((i: Response<ProductItem>) =>
-        from(i.result.data as ProductItem[]).pipe(
+        from((i.result as PageResult)?.data).pipe(
           concatMap((t: ProductItem) =>
             from(this.instanceCount(encodeQuery({ terms: { productId: t.id } }))).pipe(
               map((count) => ({ ...t, count: count.result })),
@@ -124,6 +124,43 @@ class Service extends BaseService<ProductItem> {
         params,
       }),
   };
+
+  public deviceDetail = (id: string) =>
+    request(`/${SystemConst.API_BASE}/device/instance/${id}/detail`, {
+      method: 'GET',
+    });
+
+  public saveAlarm = (id: string, data: Record<string, unknown>) =>
+    request(`/${SystemConst.API_BASE}/device/alarm/product/${id}`, {
+      method: 'PATCH',
+      data,
+    });
+
+  public category = () =>
+    request(`/${SystemConst.API_BASE}/device/category/_tree?paging=false`, {
+      method: 'GET',
+      params: encodeQuery({ sorts: { id: 'desc' } }),
+    });
+
+  public getOrg = () =>
+    request(`/${SystemConst.API_BASE}/organization/_all`, {
+      method: 'GET',
+    });
+
+  public getProtocol = () =>
+    request(`/${SystemConst.API_BASE}/protocol/supports`, {
+      method: 'GET',
+    });
+
+  public getStorage = () =>
+    request(`/${SystemConst.API_BASE}/device/product/storage/policies`, {
+      method: 'GET',
+    });
+
+  public getTransport = (protocol: string) =>
+    request(`/${SystemConst.API_BASE}/protocol/${protocol}/transports`, {
+      method: 'GET',
+    });
 }
 
 export default Service;

+ 7 - 5
src/pages/device/Product/typings.d.ts

@@ -3,15 +3,17 @@ import type { BaseItem, State } from '@/utils/typings';
 export type ProductItem = {
   id: string;
   name: string;
-  classifiedId: string;
+  classifiedId: string | string[];
   classifiedName: string;
   configuration: Record<string, any>;
   createTime: number;
   creatorId: string;
-  deviceType: {
-    text: string;
-    value: string;
-  };
+  deviceType:
+    | {
+        text: string;
+        value: string;
+      }
+    | string;
   count?: number;
   messageProtocol: string;
   metadata: string;

+ 7 - 8
src/utils/typings.d.ts

@@ -1,16 +1,15 @@
 export type Response<T> = {
   message: 'success' | 'error';
-  result:
-    | {
-        pageIndex: number;
-        pageSize: number;
-        total: number;
-        data: T[];
-      }
-    | Record<string, any>;
+  result: PageResult | Record<string, any> | T | T[];
   status: number;
   timestamp: number;
 };
+type PageResult = {
+  pageIndex: number;
+  pageSize: number;
+  total: number;
+  data: T[];
+};
 
 type BaseItem = {
   id: string;