Преглед изворни кода

fix(product): fix product bug

Next xyh
Lind пре 3 година
родитељ
комит
42848c0a9c

+ 52 - 0
src/components/ProTableCard/CardItems/product.tsx

@@ -0,0 +1,52 @@
+import { Avatar, Card } from 'antd';
+import React from 'react';
+import type { ProductItem } from '@/pages/device/Product/typings';
+import { BadgeStatus } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import '@/style/common.less';
+import { useIntl } from '@@/plugin-locale/localeExports';
+
+export interface ProductCardProps extends ProductItem {
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+export default (props: ProductCardProps) => {
+  const intl = useIntl();
+
+  return (
+    <Card style={{ width: 340 }} cover={null} actions={props.actions}>
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <Avatar size={props.avatarSize || 64} src={props.photoUrl} />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+            <BadgeStatus
+              status={props.state}
+              text={intl.formatMessage({
+                id: `pages.system.tenant.assetInformation.${
+                  props.state ? 'published' : 'unpublished'
+                }`,
+                defaultMessage: '已发布',
+              })}
+              statusNames={{
+                0: StatusColorEnum.error,
+                1: StatusColorEnum.processing,
+              }}
+            />
+          </div>
+          <div className={'card-item-content'}>
+            <label>设备类型:</label>
+            <span className={'ellipsis'}>{props.deviceType ? props.deviceType.text : '--'}</span>
+          </div>
+          <div className={'card-item-content'}>
+            <label>接入方式:</label>
+            <span className={'ellipsis'}>{props.transportProtocol || '--'}</span>
+          </div>
+        </div>
+      </div>
+    </Card>
+  );
+};

+ 11 - 5
src/components/ProTableCard/index.tsx

@@ -41,13 +41,19 @@ const ProTableCard = <
    */
   const handleCard = (dataSource: readonly T[] | undefined): JSX.Element => {
     return (
-      <div className={'pro-table-card-items'}>
-        {dataSource ? (
-          dataSource.map((item) => (cardRender && isFunction(cardRender) ? cardRender(item) : null))
+      <>
+        {dataSource && dataSource.length ? (
+          <div className={'pro-table-card-items'}>
+            {dataSource.map((item) =>
+              cardRender && isFunction(cardRender) ? cardRender(item) : null,
+            )}
+          </div>
         ) : (
-          <Empty />
+          <div style={{ display: 'flex', justifyContent: 'center' }}>
+            <Empty />
+          </div>
         )}
-      </div>
+      </>
     );
   };
 

+ 4 - 2
src/locales/zh-CN/pages.ts

@@ -190,9 +190,9 @@ export default {
   'pages.device.productDetail.id': '产品ID',
   'pages.device.productDetail.classifiedName': '所属品类',
   'pages.device.productDetail.protocolName': '消息协议',
-  'pages.device.productDetail.transportProtocol': '链接协议',
+  'pages.device.productDetail.transportProtocol': '接入方式',
   'pages.device.productDetail.createTime': '创建时间',
-  'pages.device.productDetail.updateTime': '创建时间',
+  'pages.device.productDetail.updateTime': '更新时间',
   'pages.device.productDetail.base': '配置信息',
   'pages.device.productDetail.base.save': '保存',
   'pages.device.productDetail.metadata': '物模型',
@@ -231,6 +231,7 @@ export default {
   'pages.device.productDetail.setting': '应用配置',
   'pages.device.productDetail.disable': '停用',
   'pages.device.productDetail.enabled': '启用',
+  'pages.device.productDetail.deleteTip': '已发布的产品不能进行删除操作',
 
   // 设备管理-设备分类
   'pages.device.type.device': '直连设备',
@@ -250,6 +251,7 @@ export default {
   'pages.device.instance.status.notActive': '未启用',
   'pages.device.instance.status.offLine': '离线',
   'pages.device.instance.status.onLine': '在线',
+  'pages.device.instance.deleteTip': '已启用的设备无法删除',
   'pages.device.instanceDetail.deviceType': '设备类型',
   'pages.device.instanceDetail.transportProtocol': '链接协议',
   'pages.device.instanceDetail.protocolName': '消息协议',

+ 1 - 0
src/pages/device/Instance/Save/index.tsx

@@ -76,6 +76,7 @@ const Save = (props: Props) => {
           props.reload();
         }
         props.close(resp.result);
+        form.resetFields();
       }
     }
   };

+ 52 - 39
src/pages/device/Instance/index.tsx

@@ -83,42 +83,46 @@ const Instance = () => {
         <EyeOutlined />
       </Tooltip>
     </Link>,
-    <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
-      <Popconfirm
+    <Popconfirm
+      title={intl.formatMessage({
+        id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}.tips`,
+        defaultMessage: '确认禁用?',
+      })}
+      onConfirm={async () => {
+        if (record.state.value !== 'notActive') {
+          await service.undeployDevice(record.id);
+        } else {
+          await service.deployDevice(record.id);
+        }
+        message.success(
+          intl.formatMessage({
+            id: 'pages.data.option.success',
+            defaultMessage: '操作成功!',
+          }),
+        );
+        actionRef.current?.reload();
+      }}
+    >
+      <Tooltip
         title={intl.formatMessage({
-          id: 'pages.data.option.disabled.tips',
-          defaultMessage: '确认禁用?',
+          id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
+          defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
         })}
-        onConfirm={async () => {
-          if (record.state.value !== 'notActive') {
-            await service.undeployDevice(record.id);
-          } else {
-            await service.deployDevice(record.id);
-          }
-          message.success(
-            intl.formatMessage({
-              id: 'pages.data.option.success',
-              defaultMessage: '操作成功!',
-            }),
-          );
-          actionRef.current?.reload();
-        }}
       >
-        <Tooltip
-          title={intl.formatMessage({
-            id: `pages.data.option.${record.state.value !== 'notActive' ? 'disabled' : 'enabled'}`,
-            defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
-          })}
-        >
+        <Button type={'link'}>
           {record.state.value !== 'notActive' ? <StopOutlined /> : <CheckCircleOutlined />}
-        </Tooltip>
-      </Popconfirm>
-    </a>,
-
-    <a key={'delete'}>
-      <Popconfirm
-        title="确认删除"
-        onConfirm={async () => {
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
+    <Popconfirm
+      title={intl.formatMessage({
+        id:
+          record.state.value === 'notActive'
+            ? 'pages.data.option.remove.tips'
+            : 'pages.device.instance.deleteTip',
+      })}
+      onConfirm={async () => {
+        if (record.state.value === 'notActive') {
           await service.remove(record.id);
           message.success(
             intl.formatMessage({
@@ -127,13 +131,22 @@ const Instance = () => {
             }),
           );
           actionRef.current?.reload();
-        }}
+        } else {
+          message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+        }
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.remove',
+          defaultMessage: '删除',
+        })}
       >
-        <Tooltip title={'删除'}>
+        <Button type={'link'}>
           <DeleteOutlined />
-        </Tooltip>
-      </Popconfirm>
-    </a>,
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
   ];
 
   const columns: ProColumns<DeviceInstance>[] = [
@@ -349,8 +362,8 @@ const Instance = () => {
             ...params,
             sorts: [
               {
-                name: 'id',
-                order: 'ascend',
+                name: 'createTime',
+                order: 'desc',
               },
             ],
           })

+ 11 - 10
src/pages/device/Product/Detail/BaseInfo/index.tsx

@@ -162,35 +162,36 @@ const BaseInfo = () => {
         </Descriptions.Item>
         <Descriptions.Item
           label={intl.formatMessage({
-            id: 'pages.device.productDetail.protocolName',
-            defaultMessage: '消息协议',
+            id: 'pages.device.instanceDetail.deviceType',
+            defaultMessage: '设备类型',
           })}
         >
-          {productModel.current?.protocolName}
+          {productModel.current?.deviceType ? productModel.current?.deviceType.text : '-'}
         </Descriptions.Item>
         <Descriptions.Item
           label={intl.formatMessage({
             id: 'pages.device.productDetail.transportProtocol',
-            defaultMessage: '链接协议',
+            defaultMessage: '接入方式',
           })}
         >
           {productModel.current?.transportProtocol}
         </Descriptions.Item>
+
         <Descriptions.Item
           label={intl.formatMessage({
-            id: 'pages.device.productDetail.updateTime',
-            defaultMessage: '更新时间',
+            id: 'pages.device.productDetail.createTime',
+            defaultMessage: '创建时间',
           })}
         >
-          {getDateFormat(productModel.current?.updateTime)}
+          {getDateFormat(productModel.current?.createTime)}
         </Descriptions.Item>
         <Descriptions.Item
           label={intl.formatMessage({
-            id: 'pages.device.productDetail.createTime',
-            defaultMessage: '创建时间',
+            id: 'pages.device.productDetail.updateTime',
+            defaultMessage: '更新时间',
           })}
         >
-          {getDateFormat(productModel.current?.createTime)}
+          {getDateFormat(productModel.current?.updateTime)}
         </Descriptions.Item>
         <Descriptions.Item
           span={3}

+ 12 - 22
src/pages/device/Product/Detail/index.tsx

@@ -1,17 +1,6 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { history, Link, useParams } from 'umi';
-import {
-  Badge,
-  Button,
-  Card,
-  Descriptions,
-  message,
-  Space,
-  Spin,
-  Switch,
-  Tabs,
-  Tooltip,
-} from 'antd';
+import { Badge, Card, Descriptions, message, Space, Spin, Switch, Tabs, Tooltip } from 'antd';
 import BaseInfo from '@/pages/device/Product/Detail/BaseInfo';
 import { observer } from '@formily/react';
 import { productModel, service } from '@/pages/device/Product';
@@ -152,21 +141,22 @@ const ProductDetail = observer(() => {
       subTitle={
         <Switch
           key={2}
-          checkedChildren="启用"
-          unCheckedChildren="停用"
+          checked={productModel.current?.state === 1}
+          checkedChildren="已发布"
+          unCheckedChildren="未发布"
           onChange={() => {
             changeDeploy(statusMap[productModel.current?.state || 0].action);
           }}
         />
       }
-      extra={[
-        <Button key="1" type="primary" onClick={() => changeDeploy('deploy')}>
-          {intl.formatMessage({
-            id: 'pages.device.productDetail.setting',
-            defaultMessage: '应用配置',
-          })}
-        </Button>,
-      ]}
+      // extra={[
+      //   <Button key="1" type="primary" onClick={() => changeDeploy('deploy')}>
+      //     {intl.formatMessage({
+      //       id: 'pages.device.productDetail.setting',
+      //       defaultMessage: '应用配置',
+      //     })}
+      //   </Button>,
+      // ]}
     >
       <Card>
         <Tabs defaultActiveKey="base">

+ 6 - 3
src/pages/device/Product/Save/index.tsx

@@ -30,7 +30,7 @@ const Save = (props: Props) => {
     // 特殊处理deviceType字段
     if (data) {
       if (typeof data.deviceType !== 'string') {
-        data.deviceType = data.deviceType?.value;
+        data.deviceTypeId = data.deviceType?.value;
       }
     }
     return data;
@@ -71,13 +71,16 @@ const Save = (props: Props) => {
   const handleSave = async () => {
     const formData = await form.validateFields();
     if (formData) {
-      const res = await service.update(formData);
+      const { deviceTypeId, ...extraFormData } = formData;
+      extraFormData.deviceType = formData.deviceTypeId;
+      const res = await service.update(extraFormData);
       if (res.status === 200) {
         message.success('保存成功');
         if (props.reload) {
           props.reload();
         }
         props.close();
+        form.resetFields();
       }
     }
   };
@@ -215,7 +218,7 @@ const Save = (props: Props) => {
           <Col span={24}>
             <Form.Item
               label={intlFormat('pages.device.instanceDetail.deviceType', '设备类型')}
-              name={'deviceType'}
+              name={'deviceTypeId'}
               rules={[
                 {
                   required: true,

+ 120 - 103
src/pages/device/Product/index.tsx

@@ -16,12 +16,12 @@ import { model } from '@formily/reactive';
 import { Link, useHistory } from 'umi';
 import { useIntl } from '@@/plugin-locale/localeExports';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import ProTable from '@jetlinks/pro-table';
 import { useEffect, useRef, useState } from 'react';
-import encodeQuery from '@/utils/encodeQuery';
 import Save from '@/pages/device/Product/Save';
 import SearchComponent from '@/components/SearchComponent';
 import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { ProTableCard } from '@/components';
+import ProductCard from '@/components/ProTableCard/CardItems/product';
 
 export const service = new Service('device-product');
 export const statusMap = {
@@ -105,6 +105,109 @@ const Product = observer(() => {
     });
   };
 
+  const tools = (record: ProductItem) => [
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.detail',
+        defaultMessage: '查看',
+      })}
+      key={'detail'}
+    >
+      <Link
+        onClick={() => {
+          productModel.current = record;
+        }}
+        to={`${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], record.id)}`}
+        key="link"
+      >
+        <EyeOutlined />
+      </Link>
+    </Tooltip>,
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.edit',
+        defaultMessage: '编辑',
+      })}
+      key={'edit'}
+    >
+      <Button
+        key="warning"
+        onClick={() => {
+          setCurrent(record);
+          setVisible(true);
+        }}
+        type={'link'}
+      >
+        <EditOutlined />
+      </Button>
+    </Tooltip>,
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.download',
+        defaultMessage: '下载',
+      })}
+      key={'download'}
+    >
+      <Button type={'link'}>
+        <DownloadOutlined
+          onClick={async () => {
+            await message.success(
+              `${intl.formatMessage({
+                id: 'pages.data.option.download',
+                defaultMessage: '下载',
+              })}`,
+            );
+          }}
+        />
+      </Button>
+    </Tooltip>,
+    <Popconfirm
+      key={'state'}
+      title={intl.formatMessage({
+        id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
+        defaultMessage: '是否启用?',
+      })}
+      onConfirm={() => {
+        changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
+          defaultMessage: record.state ? '禁用' : '启用',
+        })}
+      >
+        <Button type={'link'}>{record.state ? <StopOutlined /> : <PlayCircleOutlined />}</Button>
+      </Tooltip>
+    </Popconfirm>,
+    <Popconfirm
+      key="unBindUser"
+      title={intl.formatMessage({
+        id: record.state === 1 ? 'pages.device.productDetail.deleteTip' : 'page.table.isDelete',
+        defaultMessage: '是否删除?',
+      })}
+      onConfirm={async () => {
+        if (record.state === 0) {
+          await deleteItem(record.id);
+        } else {
+          message.error('已发布的产品不能进行删除操作');
+        }
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.remove',
+          defaultMessage: '删除',
+        })}
+        key={'remove'}
+      >
+        <a key="delete">
+          <DeleteOutlined />
+        </a>
+      </Tooltip>
+    </Popconfirm>,
+  ];
+
   const columns: ProColumns<ProductItem>[] = [
     {
       title: 'ID',
@@ -116,7 +219,8 @@ const Product = observer(() => {
     },
     {
       title: '设备类型',
-      dataIndex: 'classifiedName',
+      dataIndex: 'deviceType',
+      render: (_, row) => <>{row.deviceType ? row.deviceType.text : undefined}</>,
     },
     {
       title: '状态',
@@ -130,110 +234,14 @@ const Product = observer(() => {
       valueType: 'option',
       align: 'center',
       width: 200,
-      render: (_, record) => [
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.detail',
-            defaultMessage: '查看',
-          })}
-          key={'detail'}
-        >
-          <Link
-            onClick={() => {
-              productModel.current = record;
-            }}
-            to={`${getMenuPathByParams(MENUS_CODE['device/Product/Detail'], record.id)}`}
-            key="link"
-          >
-            <EyeOutlined />
-          </Link>
-        </Tooltip>,
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.edit',
-            defaultMessage: '编辑',
-          })}
-          key={'edit'}
-        >
-          <a
-            key="warning"
-            onClick={() => {
-              setCurrent(record);
-              setVisible(true);
-            }}
-          >
-            <EditOutlined />
-          </a>
-        </Tooltip>,
-        <Tooltip
-          title={intl.formatMessage({
-            id: 'pages.data.option.download',
-            defaultMessage: '下载',
-          })}
-          key={'download'}
-        >
-          <a key="download">
-            <DownloadOutlined
-              onClick={async () => {
-                await message.success(
-                  `${intl.formatMessage({
-                    id: 'pages.data.option.download',
-                    defaultMessage: '下载',
-                  })}`,
-                );
-              }}
-            />
-          </a>
-        </Tooltip>,
-        <Popconfirm
-          key={'state'}
-          title={intl.formatMessage({
-            id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}.tips`,
-            defaultMessage: '是否删除?',
-          })}
-          onConfirm={() => {
-            changeDeploy(record.id, record.state ? 'undeploy' : 'deploy');
-          }}
-        >
-          <Tooltip
-            title={intl.formatMessage({
-              id: `pages.data.option.${record.state ? 'disabled' : 'enabled'}`,
-              defaultMessage: record.state ? '禁用' : '启用',
-            })}
-          >
-            <a key="state">{record.state ? <StopOutlined /> : <PlayCircleOutlined />}</a>
-          </Tooltip>
-        </Popconfirm>,
-        <Popconfirm
-          key="unBindUser"
-          title={intl.formatMessage({
-            id: 'page.system.menu.table.delete',
-            defaultMessage: '是否删除?',
-          })}
-          onConfirm={async () => {
-            await deleteItem(record.id);
-          }}
-        >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove.tips',
-              defaultMessage: '删除',
-            })}
-            key={'remove'}
-          >
-            <a key="delete">
-              <DeleteOutlined />
-            </a>
-          </Tooltip>
-        </Popconfirm>,
-      ],
+      render: (_, record) => tools(record),
     },
   ];
 
   return (
     <PageContainer>
       <SearchComponent field={columns} onSearch={searchFn} />
-      <ProTable<ProductItem>
+      <ProTableCard<ProductItem>
         columns={columns}
         actionRef={actionRef}
         options={{ fullScreen: true }}
@@ -244,12 +252,20 @@ const Product = observer(() => {
         // }}
         params={queryParam}
         request={(params = {}) =>
-          service.query(encodeQuery({ ...params, sorts: { createTime: 'ascend' } }))
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
         }
         rowKey="id"
         search={false}
         pagination={{ pageSize: 10 }}
-        toolBarRender={() => [
+        headerTitle={[
           <Button
             onClick={() => {
               setCurrent(undefined);
@@ -265,6 +281,7 @@ const Product = observer(() => {
             })}
           </Button>,
         ]}
+        cardRender={(record) => <ProductCard {...record} actions={tools(record)} />}
       />
       <Save
         model={!current ? 'add' : 'edit'}

+ 3 - 1
src/pages/device/Product/typings.d.ts

@@ -14,7 +14,8 @@ export type ProductItem = {
   createTime: number;
   updateTime: number;
   creatorId: string;
-  deviceType: string | DeviceType;
+  deviceType: DeviceType;
+  deviceTypeId?: string;
   count?: number;
   messageProtocol: string;
   metadata: string;
@@ -25,6 +26,7 @@ export type ProductItem = {
   describe?: string;
   accessId?: string;
   accessName?: string;
+  photoUrl?: string;
 };
 
 export type ConfigProperty = {

+ 15 - 4
src/pages/system/Menu/Detail/edit.tsx

@@ -102,7 +102,12 @@ export default (props: EditProps) => {
           <Title title={'基本信息'} />
           <Row>
             <Col span={3}>
-              <Form.Item name={'icon'} label={'菜单图标'} required={true}>
+              <Form.Item
+                name={'icon'}
+                label={'菜单图标'}
+                required={true}
+                rules={[{ required: true, message: '请上传图标' }]}
+              >
                 <UploadImage disabled={disabled} style={{ width: 140, height: 130 }} />
               </Form.Item>
             </Col>
@@ -116,7 +121,7 @@ export default (props: EditProps) => {
                       defaultMessage: '名称',
                     })}
                     required={true}
-                    rules={[{ required: true, message: '该字段是必填字段' }]}
+                    rules={[{ required: true, message: '请输入名称' }]}
                   >
                     <Input disabled={disabled} />
                   </Form.Item>
@@ -129,7 +134,7 @@ export default (props: EditProps) => {
                       defaultMessage: '编码',
                     })}
                     required={true}
-                    rules={[{ required: true, message: '该字段是必填字段' }]}
+                    rules={[{ required: true, message: '请输入编码' }]}
                   >
                     <Input disabled={disabled} />
                   </Form.Item>
@@ -145,7 +150,7 @@ export default (props: EditProps) => {
                     })}
                     required={true}
                     rules={[
-                      { required: true, message: '该字段是必填字段' },
+                      { required: true, message: '请输入页面地址' },
                       { max: 120, message: '最多可输入120字符' },
                     ]}
                   >
@@ -252,6 +257,12 @@ export default (props: EditProps) => {
                   />
                   {/*</Form.Item>*/}
                 </Form.Item>
+                <Form.Item hidden name={'id'}>
+                  <Input />
+                </Form.Item>
+                <Form.Item hidden name={'parentId'}>
+                  <Input />
+                </Form.Item>
               </Col>
             </Row>
           )}