2
头图

antd是我们常用的一款react框架(等于没说,哈哈)

什么是ProComponents?

对于一个使用这个组件开发了半年之久的菜鸟来说,什么是ProComponents,
就是antd的加强集成版本,集成度很高,用起来很方便(对于我这个菜鸟来说 容易踩坑),无论是elementUi vant antd...,组件使用情况大致类似,抽个时间记录一下,也增深一下印象,以后再遇到新的组件也好得心应手不是。

ProFormDigit

这段代码,为什么要在ProFormDigit套上form.item呢?
那是因为ProFormDigit有一个bug,
因为如果我直接点提交,就会跳过ProFormDigit对于输入的内容的限制,包括(数字,位数,最大值,最小值)都会没来得及校验,提交上去~~

<Form.Item
        style={{width:'300px'}}
          name="percent"
          rules={[
            {
              required: true,
              message: '请输入调整比例',
            },
            {
              pattern:/^([1-9][0-9]{0,1}|100)$/,
              message:'请输入1到100之间的整数',
            },
          ]}
          >
            <ProFormDigit
              fieldProps={{ precision: 0 }}
              label=""
              width={150}
              placeholder="请输入调整比例"
              min={0}
              max={100}
            />
          </Form.Item>

这个组件是酱紫的~
image.png

时间组件ProFormDateRangePicker

一般使用

import {
  ProFormDateRangePicker,
} from '@ant-design/pro-form'



<ProFormDateRangePicker width="md" name={['contract', 'createTime']} label="合同生效时间" />

在useColumns中使用

const columns = defineProTableColumn<MaintenanceListVo>([
 {
      title: '投诉时间',
      dataIndex: 'createTime',
      key: 'createTime',
      hideInSearch: true,
    },
 {
      title:'',
      dataIndex: 'createTime',
      key: 'createTime',
      valueType: 'dateRange',
      hideInTable: true,
      fieldProps: {
        placeholder: ['投诉时间','投诉时间'],
      },
      search: {
      transform: (value) => {
        return {
          startTime: value[0],
          endTime: value[1],
        };
      },
    },
    },
      ]);


      /** 处理表格列 */
export function useColumns() {
  return { columns };
}

ProFormSelect 选择框

<ProFormSelect
        width={364}
        rules={[
          {
            required: true,
            message: '请选择要转让的员工',
          },
        ]}
        placeholder="请选择人员"
        fieldProps={{
          onChange: (e) => {
            setData(staffList.find((item) => item.employeeId == e));
          },
        }}
        help={currentStore?.storeType === 'community' && '转让后你就不是该小区的负责人,请慎用!'}
        name="employeeId"
        options={staffList.map((item) => ({
          label: item.employeeName,
          value: item.employeeId,
        }))}
        label="转让到"
      />

ProFormDependency 是否展示隐藏表单项

<ProFormSelect
        width="md"
        placeholder="请选择设备类型"
        options={[
          {
            value: 'UNIT',
            label: '单元设备',
          },
          {
            value: 'AREA',
            label: '小区设备',
          },
        ]}
        rules={[
          {
            required: true,
            message: '请选择设备类型',
          },
        ]}
        name="deviceType"
        label="设备类型"
      />
      <ProFormDependency name={['deviceType', 'buildingId']}>
        {({ deviceType, buildingId }) => {
          return deviceType === 'UNIT' ? (
            <div style={{ gap: '0px' }}>
              <ProFormSelect
                width="md"
                required
                placeholder="请选择楼栋"
                options={buildingList}
                rules={[
                  {
                    required: true,
                    message: '请选择楼栋',
                  },
                ]}
                name="buildingId"
                label="绑定位置"
              />
              <ProFormSelect
                width="md"
                required
                name="unitId"
                placeholder="请选择单元"
                options={buildingId ? unitObj[`${buildingId}`] : []}
                rules={[
                  {
                    required: true,
                    message: '请输入单元',
                  },
                ]}
              />
            </div>
          ) : (
            <ProFormSelect
              width="md"
              placeholder="请选择区域"
              options={areaList}
              // fieldNames={{ label: 'areaName', value: 'areaId' }
              rules={[
                {
                  required: true,
                  message: '请选择区域',
                },
              ]}
              name="areaId"
              label="绑定位置"
            />
          );
        }}
      </ProFormDependency>

ProFormTextArea 同textArea

<ProFormTextArea width="md" name="content" label="问题描述" placeholder="请输入您的问题描述" />

ProFormUploadButton 上传图片

 <ProFormUploadButton
        name="images"
        label="上传图片"
        tooltip="仅支持.jpg"
        max={2}
        fieldProps={{
          listType: 'picture',
          accept: 'jpg',
          name: 'file',
          data: { fileType: 'IMAGE' },
          headers: { 'User-Token': localStorage.getItem('User-Token') || '' },
          className: 'upload-list-inline',
        }}
        action="http://67.104.133.180:8092/api/exterior/upload/file"
      />

ProFormText

  <ProFormText
            fieldProps={{
              size: 'large',
              prefix: <CTIcon type="mima" className={styles.prefixIcon} />,
              suffix:
                state.timeCount > -1 ? (
                  `${state.timeCount}s`
                ) : (
                  <a
                    style={{ zIndex: 99 }}
                    onClick={() => sendCheckPicture({ cellphone: formRef?.current?.getFieldsValue().cellphone })}
                  >
                    获取验证码
                  </a>
                ),
            }}
            name="checkCode"
            allowClear={false}
            placeholder={'请输入验证码'}
            rules={[
              {
                required: true,
                message: '请输入短信验证码!',
              },
            ]}
          />



 
 <ProFormText
          name="newPassword"
          fieldProps={{
            size: 'large',
            prefix: <LockOutlined className={styles.prefixIcon} />,
            autoComplete: 'off',
            className: readOnly ? styles.pwd : '',
            allowClear: false,
            suffix: readOnly ? (
              <EyeInvisibleOutlined onClick={() => setReadOnly(false)} />
            ) : (
              <EyeTwoTone onClick={() => setReadOnly(true)} />
            ),
          }}
          placeholder={'设置新密码'}
          rules={[
            {
              required: true,
              message: '请填写新密码!',
            },
            {
              pattern:
                /^[\u4E00-\u9FA5A-Za-z0-9`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]{8,20}$/,
              message: '请输入8-20位字母/数字/标点符号',
            },
          ]}
        />


  <ProFormText
          name="surePassword"
          dependencies={['newPassword']}
          fieldProps={{
            size: 'large',
            prefix: <LockOutlined className={styles.prefixIcon} />,
            autoComplete: 'off',
            className: sureReadOnly ? styles.pwd : '',
            allowClear: false,
            suffix: sureReadOnly ? (
              <EyeInvisibleOutlined onClick={() => setSureReadOnly(false)} />
            ) : (
              <EyeTwoTone onClick={() => setSureReadOnly(true)} />
            ),
          }}
          placeholder={'再次确认新密码'}
          rules={[
            {
              required: true,
              message: '请再次填写新密码',
            },
            ({ getFieldValue }) => ({
              validator(_, value) {
                if (!value || getFieldValue('newPassword') === value) {
                  return Promise.resolve();
                }
                return Promise.reject('与新密码不同,请重新确认新密码');
              },
            }),
          ]}
        />

组件的rules验证写法

   rules={[
          {
            validator: (_, value) => {
              if (!value) {
                return Promise.resolve();
              }
              if (value && value.length < 6) {
                return Promise.resolve();
              }
              if (value.length > 5) {
                return Promise.reject('标签最大数量不能超过5个');
              }
            },
          },
        ]}


 rules={[
              {
                required: true,
                message: '请输入联系方式',
              },
              {
                pattern: /^1\d{10}$/,
                message: '请输入正确的手机号格式',
              },
              {
                pattern: /^[\s\S]*.*[^\s][\s\S]*$/,
                message: '联系方式不可为空',
              },
            ]}

ProFormUploadButton 上传

在oncChange中图片 去除重复
在beforeUpload中限制图片的大小,返回true代表可以上传,如果过大则返回Upload.LIST_IGNORE,代表不可以上传,不会出现在页面上

  import { Upload } from 'antd';


  const handleChange = (pic: UploadChangeParam) => {
    const filterRes = pic.fileList.filter((f) => f.name === pic.file.name && f.size === pic.file.size);
    filterRes.length > 1 && pic.fileList.pop();
  };

  const beforeUpload = (file: UploadFile) => {
    const isLt2M = (file.size || 0) < 1024 * 1024 * 2;
    if (!isLt2M) {
      message.error('图片不能超过2MB!');
    }
    console.log('Upload.LIST_IGNORE',Upload.LIST_IGNORE)
    return isLt2M ? true : Upload.LIST_IGNORE;
  };
<ProFormUploadButton
        name="images"
        label="上传图片"
        tooltip="仅支持jpeg、jpg、png"
        max={6}
        onChange={handleChange}
        fieldProps={{
          listType: 'picture',
          accept: '.jpeg,.jpg,.png',
          name: 'file',
          data: { fileType: 'IMAGE' },
          headers: { 'User-Token': localStorage.getItem('User-Token') || '' },
          className: 'upload-list-inline',
          beforeUpload: beforeUpload,
        }}
        action={`${HOST_BASE}/api/exterior/upload/file`}
      />

显示商品的各个方面图片 并带放大镜

import { CTIcon, CTImage } from '@/components';
import cm from 'classnames';
import { FC, useEffect, useMemo, useState } from 'react';
import ReactImageZoom from 'react-image-zoom';
import { store } from '../../store';
import './index.less';

const MAX_COUNT = 3;

/**
 * 多规格: 零售价显示区间价
 * 多单位: 展示换算关系
 */
const GoodsImageInfo: FC = () => {
  const { unibuyGoodsDeatil, currentObj } = store.useState();

  const imageList = useMemo(() => {
    return currentObj.skuPic || [];
  }, [currentObj]);

  const [index, setIndex] = useState(0);
  const [selectImage, setSelectImage] = useState<string>(imageList[0]);

  const count = imageList.length;
  const isStart = index <= 0;
  const isEnd = index >= imageList.length - MAX_COUNT;

  useEffect(() => {
    setIndex(0);
    setSelectImage(imageList[0]);
  }, [imageList]);

  return (
    <div className="goods-image">
      {selectImage ? (
        <div className="goods-image-detail">
          <ReactImageZoom width={238} height={238} offset={{ vertical: 0, horizontal: 10 }} img={selectImage} />
        </div>
      ) : (
        <CTImage className="goods-image-detail" src={selectImage} width={338} />
      )}

      {count > 0 && (
        <div className="goods-image-btn-groups">
          <div
            className={cm('btn-prev', { disable: isStart })}
            onClick={() => {
              if (isStart) return;
              setIndex((index) => index - 1);
            }}
          >
            <CTIcon type="fanhui" size={16} color="#999" />
          </div>
          <div
            className="goods-image-btn-container"
            style={{
              /* stylelint-disable value-keyword-case */
              maxWidth: 56 * MAX_COUNT,
            }}
          >
            <div
              className="goods-image-btn-container-wrapper"
              style={{
                width: count * 56 * MAX_COUNT,
                transform: `translate3d(-${index * 56}px, 0, 0)`,
              }}
            >
              {imageList.map((item, index) => (
                <CTImage
                  key={index}
                  src={item}
                  className={cm('goods-image-btn', {
                    active: item === selectImage,
                  })}
                  width={48}
                  onClick={() => setSelectImage(item)}
                />
              ))}
            </div>
          </div>
          <div
            className={cm('btn-next', { disable: isEnd })}
            onClick={() => {
              if (isEnd) return;
              setIndex((index) => index + 1);
            }}
          >
            <CTIcon type="xiaji" size={16} color="#999" />
          </div>
        </div>
      )}
    </div>
  );
};

export default GoodsImageInfo;




index.less
.goods-image {
  .goods-image-detail {
    width: 240px;
    height: 240px;
    border: 1px solid #d9d9d9;
    border-radius: 8px;
    img {
      object-fit: contain;
      border-radius: 8px;
    }
  }
  .goods-image-btn-groups {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 16px;

    .btn-prev,
    .btn-next {
      flex: none;
      padding: 4px;
      cursor: pointer;

      &.disable {
        cursor: not-allowed;
      }
      .anticon {
        vertical-align: middle;
      }
    }
    .goods-image-btn-container {
      position: relative;
      flex: 1;
      height: 48px;
      overflow: hidden;
    }

    .goods-image-btn-container-wrapper {
      position: absolute;
      top: 0;
      left: 0;
      transform: translate3d(0, 0, 0);
      transition: transform 0.3s;
    }

    .goods-image-btn {
      width: 48px;
      height: 48px;
      margin-left: 8px;
      border: 1px solid #d9d9d9;
      border-radius: 4px;
      cursor: pointer;
      &.active {
        border-color: #1b9aee;
      }
    }
  }
}

image.png
image.png

ProTable(一)

  • 直接跳过一般进高级,完成的是下面图片的效果分为: 左边分类, 批量操作, 子table展示sku(颜色、规格)*

image.png

 <ProTable<GoodsInfoVo>
        options={false}
        接口数据
        dataSource={list}
        展示隐藏子table组件(GoodsSkuList组件)
        expandedRowRender={(record) => <GoodsSkuList goods={record} btnKeys={btnKeys}/>}
        列表字段
        columns={columns}
        多选框
        rowSelection={rowSelection}
        toolbar={{
          列表左侧控制分类行的展示隐藏
          title: !cateVisible && (
            <div className={styles.cate_title}>
              商品分类
              <MenuUnfoldOutlined className={styles.icon} onClick={() => store.onChangeCateVisible(true)} />
            </div>
          ),
          右侧的批量按钮
          actions: btns,
        }}
        rowKey="goodsId"
        tableRender={(_, dom) => (
          单独渲染  分类组件与原table拼接   dom是原有的结构
          <div style={{ display: 'flex', width: '100%' }}>
            <GoodsCate />
            <div style={{ flex: 1 }}>{dom}</div>
          </div>
        )}
        搜索框的配置
        search={{
          labelWidth: 0,
          collapsed: false,
          collapseRender: false,
        }}
        查询触发的方法
        onSubmit={(params) => {
          const { pageSize } = queryParams;
          loadGoodsList({ ...params, pageSize, pageNum: 1 });
        }}
        重置按钮触发的方法
        onReset={() => {
          loadGoodsList({ pageSize: 10, pageNum: 1 });
        }}
        分页配置
        pagination={{
          showQuickJumper: true,
          size: 'default',
          pageSize: queryParams.pageSize,
          current: queryParams.pageNum,
          total,
        }}
        点击分页触发方法
        onChange={(pagination, _) => {
          const { current = 1, pageSize } = pagination;
          loadGoodsList({ ...queryParams, pageSize: pageSize || 10, pageNum: current });
        }}
        dateFormatter="string"
      />

index.hook.tsx
设置列表项, 搜索框和给按钮设置权限

import { message } from '@/components';
import { defineProTableColumn } from '@/utils/defineProTableColumn';
import { Button, Select, TreeSelect, Typography } from 'antd';
const { Text } = Typography;
import { history } from 'umi';
import { downUsing, putUsing, store, toSetReferPrice } from './store';

const loadColumns = (unibuyBrandList: API.TOption[], categoryList: Array<UnibuyCategoryVo>, btnKeys: string[]) =>
  defineProTableColumn<GoodsInfoVo>([
    {
      title: '',
      dataIndex: 'keyWords',
      key: 'keyWords',
      hideInTable: true,
      fieldProps: {
        placeholder: '商品名称/商品编号/商品货号',
      },
    },
    {
      title: '商品名称/规格',
      dataIndex: 'goodsName',
      key: 'goodsName',
      width: 200,
      renderText: (text, record) =>
        text ? (
          <a onClick={() => history.push(`/merchant/unibuy/goods/detail?goodsId=${record.goodsId}`)}>{text}</a>
        ) : (
          '-'
        ),
      hideInSearch: true,
    },
    {
      title: '商品编号/商品货号',
      dataIndex: 'goodsId',
      key: 'goodsId',
      width: 100,
      hideInSearch: true,
    },
    {
      title: '零售价(元)',
      key: 'storePrice',
      width: 80,
      hideInSearch: true,
      render: (_,record) => {
        return <Text>{record.storePrice || 0}</Text>;
      }
    },
    {
      title: '成本价(元)',
      key: 'skuCostPrice',
      hideInSearch: true,
      width: 80,
      render: (_,record) => {
        return <Text>{record.skuCostPrice || 0}</Text>;
      },
    },
    {
      title: '商品分类',
      dataIndex: 'categoryName',
      key: 'categoryName',
      width: 80,
      hideInSearch: true,
    },
    {
      title: '库存',
      key: 'stock',
      width: 80,
      hideInSearch: true,
      render: (_,record) => {
        return <Text>{record.stock || 0}</Text>;
      },
    },
    {
      title: '销量',
      dataIndex: 'saleCount',
      key: 'saleCount',
      hideInSearch: true,
      render: (_,record) => {
        return <Text>{record.saleCount || 0}</Text>;
      },
      width: 80,
    },
    {
      title: '',
      hideInTable: true,
      dataIndex: 'brandId',
      key: 'brandId',
      renderFormItem: (_, { type, defaultRender }, form) => {
        return <Select options={[{ label: '全部', value: '' }].concat(unibuyBrandList)} placeholder="请选择品牌" />;
      },
    },
    {
      title: (_, type) => (type === 'table' ? '销售类型' : ''),
      dataIndex: 'sellType',
      key: 'sellType',
      width: 80,
      valueEnum: {
        '': { text: '全部' },
        1: { text: '香港现货' },
        2: { text: '期货' },
        3: { text: '欧洲现货' },
        4: { text: '国内现货' },
      },
      fieldProps: {
        placeholder: '请选择销售类型',
      },
    },
    {
      title: '',
      hideInTable: true,
      dataIndex: 'categoryId',
      key: 'categoryId',
      renderFormItem: (_, { type, defaultRender }, form) => {
        return <TreeSelect options={[{ title: '全部', value: '' }].concat(categoryList)} placeholder="全部分类" />;
      },
    },
    {
      title: '',
      dataIndex: 'priceChangeTime',
      key: 'priceChangeTime',
      valueEnum: {
        ONE_DAY: { text: '最近24小时' },
        TWO_DAY: { text: '最近2天' },
        THREE_DAY: { text: '最近3天' },
      },
      fieldProps: {
        placeholder: '最近改价',
      },
      hideInTable: true,
    },

    {
      title: '操作',
      key: 'option',
      width: 150,
      valueType: 'option',
      render: (_, record) => {
        return [
          btnKeys.includes('btn_merchant_unibuy_goods_price') && (
            <a key="setPrice" onClick={() => toSetReferPrice(record)}>
              设价
            </a>
          ),
          record?.skuStatus === 0 && record.goodsId && btnKeys.includes('btn_merchant_unibuy_goods_on') && (
            <a key="on" onClick={() => record.goodsId && putUsing({ goodsIds: [record.goodsId] })}>
              上架
            </a>
          ),
          record?.skuStatus === 1 && btnKeys.includes('btn_merchant_unibuy_goods_off') && (
            <a key="off" onClick={() => record.goodsId && downUsing({ goodsIds: [record.goodsId] })}>
              下架
            </a>
          ),
        ];
      },
    },
  ]);

/** 处理表格列 */
export function useColumns(btnKeys: string[]) {
  const { unibuyBrandList, categoryList } = store.getState();
  return {
    columns: loadColumns(unibuyBrandList, categoryList, btnKeys),
  };
}
/** 处理按钮组 */
export function useToolBtn(btnKeys: string[]) {
  const { selectedRowKeys } = store.getState();
  return [
    btnKeys.includes('btn_merchant_unibuy_goods_batchOn') && (
      <Button
        key="on"
        onClick={() => {
          if (selectedRowKeys.length === 0) {
            message.error('请选择至少一个商品');
            return;
          }
          putUsing({ goodsIds: selectedRowKeys });
        }}
      >
        批量上架
      </Button>
    ),
    btnKeys.includes('btn_merchant_unibuy_goods_batchOff') && (
      <Button
        key="off"
        onClick={() => {
          if (selectedRowKeys.length === 0) {
            message.error('请选择至少一个商品');
            return;
          }
          downUsing({ goodsIds: selectedRowKeys });
        }}
      >
        批量下架
      </Button>
    ),
  ];
}

分类组件

import { Tree } from 'antd';
import cn from 'classnames';
import { FC } from 'react';
import { loadGoodsList, store } from '../../store';
import styles from './index.less';

const GoodsCate: FC = () => {
  const { cateVisible, categoryList, queryParams } = store.useState();
  const onSelect = (selectedKeys: React.Key[], info: any) => {
    loadGoodsList({ ...queryParams, pageNum: 1, categoryId: selectedKeys[0] as string });
  };
  return (
    <div className={cn(styles.goods_cate_box, styles[`${cateVisible ? 'show' : 'hidden'}`])}>
      <div className={styles.title} onClick={() => store.onChangeCateVisible(false)}>
        商品分类
        <MenuFoldOutlined className={styles.icon} />
      </div>
      <Tree selectedKeys={[queryParams.categoryId as React.Key]} onSelect={onSelect} treeData={categoryList} />
    </div>
  );
};
export default GoodsCate;


index.less
.goods_cate_box {
  background: #fff;
  &.show {
    width: 158px;
    margin-right: 10px;
  }
  &.hidden {
    width: 0;
    margin-right: 0;
    .title {
      display: none;
    }
  }
  .title {
    margin-bottom: 16px;
    padding: 0 16px;
    color: #262626;
    font-size: 14px;
    line-height: 56px;
    border-bottom: 1px solid #f0f0f0;
    cursor: pointer;
    .icon {
      margin-left: 10px;
    }
  }
  :global .ant-menu-inline {
    border-right: 0;
  }
}

子Table GoodsSkuList

import ProTable from '@ant-design/pro-table';
import { FC } from 'react';
import { useColumns } from './index.hooks';
import styles from './index.less';
interface IGoodsSkuListProps {
  goods: GoodsInfoVo;
}
const GoodsSkuList: FC<IGoodsSkuListProps> = ({ goods,btnKeys }) => {
  const { columns } = useColumns(goods,btnKeys);
  return (
    <ProTable<GoodsVo>
      className={styles.sku_table}
      showHeader={false}
      columns={columns}
      headerTitle={false}
      rowKey="goodsId"
      search={false}
      options={false}
      dataSource={goods.skusList || []}
      pagination={false}
    />
  );
};

export default GoodsSkuList;

index.less
.sku_table {
  background: #fff;
  :global .ant-card-body {
    padding: 0;
  }
  table tbody tr:last-child {
    td {
      border-bottom: 0;
    }
  }
}

子index.hook
设置表格里层的列表结构,但是由于里层的没有多选批量框,所以要多设置一列空的

import { defineProTableColumn } from '@/utils/defineProTableColumn';
import { Typography } from 'antd';
const { Text } = Typography;
import { history } from 'umi';
import { downUsing, putUsing, toSetReferPrice } from '../../store';
const loadColumns = (goods: GoodsInfoVo,btnKeys:string[]) =>
  defineProTableColumn<GoodsVo>([
    {
      key: 'index',
      width: 80,
    },
    {
      key: 'attr',
      width: 200,
      renderText: (text, record) => (
        <a onClick={() => history.push(`/merchant/unibuy/goods/detail?goodsId=${record.goodsId}`)}>
          {record?.attr?.map((item) => item.vidName).join('') || '-'}
        </a>
      ),
    },
    {
      title: '商品编号/商品货号',
      dataIndex: 'goodsId',
      key: 'goodsId',
      width: 100,
    },
    {
      title: '零售价(元)',
      dataIndex: 'storePrice',
      key: 'storePrice',
      width: 80,
      render: (_, record) => {
        return <Text>{record.storePrice || 0}</Text>;
      },
    },
    {
      title: '成本价(元)',
      dataIndex: 'skuCostPrice',
      key: 'skuCostPrice',
      width: 80,
      render: (_, record) => {
        return <Text>{record.skuCostPrice || 0}</Text>;
      },
    },
    {
      title: '商品分类',
      dataIndex: 'categoryName',
      key: 'categoryName',
      width: 80,
    },
    {
      title: '库存',
      dataIndex: 'stock',
      key: 'stock',
      width: 80,
      render: (_, record) => {
        return <Text>{record.stock || 0}</Text>;
      },
    },
    {
      title: '销量',
      dataIndex: 'saleCount',
      key: 'saleCount',
      width: 80,
      render: (_, record) => {
        return <Text>{record.saleCount || 0}</Text>;
      },
    },
    {
      dataIndex: 'sellType',
      key: 'sellType',
      width: 80,
      valueEnum: {
        1: { text: '香港现货' },
        2: { text: '期货' },
        3: { text: '欧洲现货' },
        4: { text: '国内现货' },
      },
      fieldProps: {
        placeholder: '请选择销售类型',
      },
    },

    {
      title: '操作',
      key: 'option',
      width: 150,
      valueType: 'option',
      render: (_, record) => {
        return [
          btnKeys.includes('btn_merchant_unibuy_goods_price')&&<a key="setPrice" onClick={() => toSetReferPrice(goods)}>
            设价
          </a>,
          btnKeys.includes('btn_merchant_unibuy_goods_on')&&record.skuStatus === 0 && (
            <a key="on" onClick={() => record?.goodsId && putUsing({ goodsIds: [record?.goodsId] })}>
              上架
            </a>
          ),
          btnKeys.includes('btn_merchant_unibuy_goods_off')&&record.skuStatus === 1 && (
            <a key="on" onClick={() => record?.goodsId && downUsing({ goodsIds: [record?.goodsId] })}>
              下架
            </a>
          ),
        ];
      },
    },
  ]);

/** 处理表格列 */
export function useColumns(goods: GoodsInfoVo,btnKeys:string[]) {
  return {
    columns: loadColumns(goods,btnKeys),
  };
}

表格渲染的部分这里就完结了,但是我还是想说一下设价的这个逻辑

设价

批量设价,以及状态数据的处理
image.png

import ProTable from '@ant-design/pro-table';
import { Button, Drawer, Image, Space } from 'antd';
import { FC } from 'react';
import { doSetReferPrice, store } from '../../store';
import { useColumns } from './index.hook';
import styles from './index.less';

const SetPriceDrawer: FC = () => {
  const { drawerVisible, goodsItem } = store.useState();
  const { columns } = useColumns(goodsItem);
  return (
    <Drawer
      onClose={() => {
        store.changeDrawerVisible(false);
      }}
      title="商品设价"
      width="78%"
      visible={drawerVisible}
      footer={
        <Space>
          <Button
            key="cancel"
            onClick={() => {
              store.changeDrawerVisible(false);
            }}
          >
            取消
          </Button>
          <Button key="sure" type="primary" onClick={() => {doSetReferPrice(),store.changeDrawerVisible(false)}}>
            保存
          </Button>
        </Space>
      }
      footerStyle={{ textAlign: 'center' }}
    >
      <div className={styles.setPrice}>
        <Image src={goodsItem.spuPic?.[0] || ''} preview={false} width={64} height={64} />
        <div className={styles.detailBox}>
          <div className={styles.goodDetail}>{goodsItem.goodsName}</div>
          <div className={styles.category}>{goodsItem.categoryName}</div>
        </div>
      </div>
      <ProTable<GoodsVo>
        bordered
        options={false}
        columns={columns}
        rowKey="goodsId"
        dataSource={goodsItem?.skusList || []}
        dateFormatter="string"
        search={false}
        pagination={false}
        toolbar={{
          menu: {
            type: 'tab',
            activeKey: 'w',
            items: [{ key: 'w', label: '基础价格' }],
          },
        }}
      />
    </Drawer>
  );
};
export default SetPriceDrawer;

index.hook.tsx
动态渲染table的列表行,根据返回值尺寸,还是颜色,拼接到列表列字段设置中

import { defineProTableColumn } from '@/utils/defineProTableColumn';
import { Input, InputNumber } from 'antd';
import { store } from '../../store';
const columnsMeta = (attrs: GoodsAttrVo[], goodsPriceObj: Record<string, number>) =>
  defineProTableColumn<GoodsVo>(
    // @ts-ignore
    attrs
      .map((item) => ({
        title: item.pidName,
        key: item.pid,
        isEditable: false,
        render: (_: any, record: any) => {
          // @ts-ignore
          return <>{record.attr.find((attr) => attr.pid === item.pid).vidName}</>;
        },
      }))
      .concat([
        {
          title: '零售价(元)',
          dataIndex: 'price',
          key: 'price',
          render: (_, record) => (
            <InputNumber
              min={0}
              max={1000000000}
              precision={2}
              style={{ width: '150px' }}
              value={goodsPriceObj[record.goodsId]}
              onChange={(val) => store.onChangeGoodsPriceObj({ ...goodsPriceObj, [record.goodsId]: val })}
            />
          ),
        },
        {
          title: '成本价(元)',
          dataIndex: 'skuCostPrice',
          key: 'skuCostPrice',
          render: (_, record) => <Input style={{ width: '150px' }} value={record.skuCostPrice} disabled />,
        },
      ]),
  );

/** 处理表格列 */
export function useColumns(goodsItem: GoodsInfoVo) {
  const { goodsPriceObj } = store.useState();
  return { columns: columnsMeta(goodsItem.attrs || [], goodsPriceObj) };
}

store.ts
数据的状态处理

import { message } from '@/components';
import { reduxStore } from '@/createStore';
import {
  downUsingPOST,
  getUnibuyBrandUsingGET,
  getUnibuyCategoryUsingGET,
  getUnibuyGoodsListUsingPOST,
  putUsingPOST,
  setReferPriceUsingPOST,
} from '@/services/UnibuyGoodsServices';

type TGoodsPriceObj = Record<string, number>;
export const store = reduxStore.defineLeaf({
  namespace: 'unibuy_goods',
  initialState: {
    queryParams: {
      pageNum: 1,
      pageSize: 10,
    } as UnibuyGoodsForm,
    total: 0,
    list: [] as GoodsInfoVo[],
    cateVisible: true,
    unibuyBrandList: [] as API.TOption[],
    categoryList: [] as Array<UnibuyCategoryVo>,
    drawerVisible: false,
    goodsItem: {} as GoodsInfoVo,
    selectedRowKeys: [] as string[],
    goodsPriceObj: {} as TGoodsPriceObj,
  },
  reducers: {
    onChangeSelectedRowKeys(state, payload: string[]) {
      state.selectedRowKeys = payload;
    },
    onChangeTotal(state, payload: number) {
      state.total = payload;
    },
    onChangeQueryParams(state, payload: UnibuyGoodsForm) {
      state.queryParams = payload;
    },
    onChangeList(state, payload: GoodsInfoVo[]) {
      state.list = payload;
    },
    onChangeCateVisible(state, payload: boolean) {
      state.cateVisible = payload;
    },
    onChangeUnibuyBrandList(state, unibuyBrandList: API.TOption[]) {
      state.unibuyBrandList = unibuyBrandList;
    },
    onChangeCategoryList(state, payload: Array<UnibuyCategoryVo>) {
      state.categoryList = payload;
    },
    changeDrawerVisible(state, payload: boolean) {
      state.drawerVisible = payload;
    },
    onChangeGoodsItem(state, payload: GoodsInfoVo) {
      state.goodsItem = payload;
    },
    onChangeGoodsPriceObj(state, payload: TGoodsPriceObj) {
      state.goodsPriceObj = payload;
    },
  },
});

/**查询unibuy品牌列表 */
export const loadGoodsList = async (queryParams: UnibuyGoodsForm) => {
  const { success, errMessage, data, total } = await getUnibuyGoodsListUsingPOST({ body: queryParams });
  if (!success || !data) {
    message.error(errMessage || '查询失败!');
    return;
  }
  store.onChangeQueryParams(queryParams);
  store.onChangeTotal(total || 0);
  store.onChangeList(data);
};
/**unibuy品牌接口 */
export const getUnibuyBrand = async () => {
  const { success, errMessage, data } = await getUnibuyBrandUsingGET();
  if (!success || !data) {
    message.error(errMessage || '查询失败!');
    return;
  }
  const unibuyBrandList = data.map((item) => ({ label: item.brandName, value: item.brandId } as API.TOption));
  store.onChangeUnibuyBrandList(unibuyBrandList);
};

/**unibuy目录接口 */
export const loadCategoryList = async () => {
  const { success, errMessage, data } = await getUnibuyCategoryUsingGET();
  if (!success || !data) {
    message.error(errMessage || '查询失败!');
    return;
  }
  store.onChangeCategoryList(data);
};
/**设置成本价 */
export const toSetReferPrice = (goodsItem: GoodsInfoVo) => {
  store.onChangeGoodsItem(goodsItem);
  const _goodsItem = goodsItem.skusList?.reduce((pre, cur) => {
    if (cur.goodsId) {
      pre[`${cur.goodsId}`] = cur.storePrice || 0;
    }
    return pre;
  }, {} as TGoodsPriceObj);
  store.onChangeGoodsPriceObj(_goodsItem || {});
  store.changeDrawerVisible(true);
};
// uniBuy下架
export const downUsing = async (queryParams: UniBuyGoodsStatusForm) => {
  const { success, errMessage, data } = await downUsingPOST({ body: queryParams });
  if (!success || !data) {
    message.error(errMessage || '查询失败!');
    return;
  }
  message.success('下架成功');
  store.onChangeSelectedRowKeys([])
  loadGoodsList(store.getState().queryParams);
};

// uniBuy上架
export const putUsing = async (queryParams: UniBuyGoodsStatusForm) => {
  const { success, errMessage, data } = await putUsingPOST({ body: queryParams });
  if (!success || !data) {
    message.error(errMessage || '查询失败!');
    return;
  }
  message.success('上架成功');
  store.onChangeSelectedRowKeys([])
  loadGoodsList(store.getState().queryParams);
};

设价
export const doSetReferPrice = async () => {
  const { goodsPriceObj } = store.getState();
  const _list = Object.entries(goodsPriceObj).map((item) => ({
    goodsId: item[0],
    storePrice: item[1],
  })) as UniBuySetReferPriceForm[];
  const { data, success, errMessage } = await setReferPriceUsingPOST({ body: { goodsInfos: _list } });
  if (!success || !data) {
    message.error(errMessage || '设价失败!');
    return;
  }
  message.success('设价成功');
  loadGoodsList(store.getState().queryParams);
};

ProList与ProTable(二)

这是一个proList与ProTable结合的结构
可以把整个看成一个列表,列表又可以分为每一项列表行的表单以及表单的文字
它的搜索,表格与表格顶上的文字都是分开的组件
image.png
最外层的父元素 index.tsx

import { PageContainer } from '@ant-design/pro-layout';
import ProList from '@ant-design/pro-list';
import { useMount } from 'ahooks';
import { Col, Row } from 'antd';
import { E_ORDER_TYPE, ORDER_TYPE } from '../constant';
import styles from '../index.less';
import OrderGoods from './components/OrderGoods';
import OrderInfoRow from './components/OrderInfoRow';
import SalesDetail from './components/SalesDetail';
import SearchForm from './components/SearchForm';
import RefundDialog from './components/RefundDialog';
import { loadRefundList, loadRefundOrderStatusList, loadStatusList, store, queryAppRefundCount } from './store';
export default () => {
  store.useMount();
  const { queryParams, total, list, loading, refundCount } = store.useState();
  useMount(() => {
    loadRefundList(queryParams);
    loadRefundOrderStatusList();
    loadStatusList();
    // queryAppRefundCount()
  });
  return (
    <PageContainer className={styles.orderListContainer}>
      搜索组件
      <SearchForm />
      详情组件
      <SalesDetail />
      拒绝弹框
      <RefundDialog />
      <ProList<QueryOrderListVo>
        table栏切换
        toolbar={{
          menu: {
            type: 'tab',
            activeKey: queryParams.state,
            items: E_ORDER_TYPE,
            onChange: (key) => {
              let arr = [];
              if (key === 'RETURN_APPLYING') {
                arr = ['RETURN_APPLYING', 'REFUND_APPLYING'];
              } else if (key === 'RETURN_AGREE') {
                arr = ['RETURN_AGREE', 'CANCEL', 'REFUND_AGREE'];
              } else {
                arr = [key];
              }
              loadRefundList({ ...queryParams, states: arr, pageNum: 1 });
            },
          },
        }}
        loading={loading}
        rowKey="orderId"
        itemLayout="vertical"
        dataSource={list}
        设置这个列表的列表项
        metas={{
          title: {
            render: (_, record, index) => {
              if (index !== 0) return null;
              return (
                <Row>
                  <Col style={{ width: '20%' }}>商品</Col>
                  <Col style={{ width: '15%' }}>成交单价/退货数量</Col>
                  <Col style={{ width: '10%' }}>申请退款金额</Col>
                  <Col style={{ width: '10%' }}>实退金额</Col>
                  <Col style={{ width: '10%' }}>订单来源</Col>
                  <Col style={{ width: '10%' }}>售后类型</Col>
                  <Col style={{ width: '10%' }}>订单状态</Col>
                  <Col style={{ width: '15%' }}>操作</Col>
                </Row>
              );
            },
          },
          每一行列表的头部描述文字
          description: {
            render: (_, record) => {
              return <OrderInfoRow order={record} />;
            },
          },
          每一列列表的内容 这里是一个表单组件
          content: {
            render: (_, record) => {
              return <OrderGoods record={record} />;
            },
          },
        }}
        分页
        pagination={{
          showQuickJumper: true,
          size: 'default',
          pageSize: queryParams.pageSize,
          current: queryParams.pageNum,
          total,
          onChange: (current, pageSize) => {
            loadRefundList({ ...queryParams, pageSize: pageSize || 10, pageNum: current });
          },
        }}
      />
    </PageContainer>
  );
};

index.less
.orderListContainer {
  font-size: 14px;

  // :global .ant-pro-list .ant-pro-list-row-title .ant-col {
  //   text-align: center !important;
  // }

  :global .ant-pro-list {
    .ant-spin-container {
      overflow-x: auto;
    }

    .ant-list-items {
      min-width: 1280px;
    }

    .ant-pro-table-list-toolbar-left {
      gap: 0;
    }

    .ant-pro-list-row-title {
      width: 100%;
      height: 46px;
      margin-right: 0;
      line-height: 46px;
      background: #fafafa;
      border-bottom: 1px solid #f0f0f0;

      &:hover {
        color: rgba(0, 0, 0, 0.85);
      }

      .ant-col {
        padding: 0 8px;
      }
    }

    .ant-list-vertical {
      .ant-pro-list-row-description {
        margin-top: 0;
      }

      .ant-list-item-meta {
        margin-bottom: 0;
      }

      .ant-pro-list-row {
        padding: 0;

        &:hover {
          background-color: transparent;
          transition: none;
        }

        .ant-pro-table .ant-card-body {
          padding: 0;
        }
      }
    }
  }
}

头部的搜索组件 SearchForm/index.tsx

import { Button, Col, DatePicker, Form, FormProps, Input, Row, Select } from 'antd';
import type { Moment } from 'moment';
import { FC } from 'react';
import { loadRefundList, store } from '../../store';
import styles from './index.less';
const Option = Select.Option;
const RangePicker = DatePicker.RangePicker;
type IFormValues = QueryOrderListForm & { startEndDate?: Moment[]; payStartEndDate?: Moment[] };
const SearchForm: FC = () => {
  const [form] = Form.useForm<QueryOrderListForm>();
  const { queryParams, statusList, refundStatusList } = store.useState();
  const handleValues = (values: IFormValues) => {
    if (values.startEndDate) {
      values = {
        ...values,
        beginTime: values.startEndDate[0].format('YYYY-MM-DD') + ' 00:00:00',
        endTime: values.startEndDate[1].format('YYYY-MM-DD') + ' 23:59:59',
      };
      delete values['startEndDate'];
    }
    return values;
  };
  const onFinish: FormProps<QueryOrderListForm>['onFinish'] = (values) => {
    loadRefundList({ ...queryParams, ...handleValues(values), pageNum: 1 });
  };
  const onReset = () => {
    form.resetFields();
    loadRefundList({ pageSize: 10, pageNum: 1, states: queryParams.states });
  };
  return (
    <div className={styles.searchFrom}>
      <Form<QueryOrderListForm> form={form} onFinish={onFinish}>
        <Row gutter={20}>
          <Col span={6}>
            <Form.Item name="searchContent">
              <Input placeholder="请输入订单编号/归属店铺/商品名称" />
            </Form.Item>
          </Col>
          <Col span={6}>
            <Form.Item name="orderState" rules={[{ required: false }]}>
              <Select placeholder="请选择订单状态">
                <Option value={''}>全部</Option>
                {statusList.map((item, index) => (
                  <Option value={item.code || ''} key={index}>
                    {item.name}
                  </Option>
                ))}
              </Select>
            </Form.Item>
          </Col>

          <Col span={6}>
            <Form.Item name="refundType" rules={[{ required: false }]}>
              <Select placeholder="请选择售后类型">
                <Option value={''}>全部</Option>
                {refundStatusList.map((item, index) => (
                  <Option value={item.code || ''} key={index}>
                    {item.name}
                  </Option>
                ))}
              </Select>
            </Form.Item>
          </Col>
          <Col span={6}>
            <Form.Item name="startEndDate">
              <RangePicker
                placeholder={['下单:开始时间', '结束时间']}
                style={{ width: '100%' }}
                inputReadOnly={true}
                allowClear={false}
              />
            </Form.Item>
          </Col>
          <Col span={24} style={{ textAlign: 'right' }}>
            <Button style={{ margin: '0 10px' }} onClick={onReset}>
              重置
            </Button>
            <Button type="primary" htmlType="submit">
              搜索
            </Button>
          </Col>
        </Row>
      </Form>
    </div>
  );
};

export default SearchForm;



每一列的描述文字组件OrderInfoRow/index.tsx

import { FC } from 'react';
import styles from './styles.less';
const OrderInfoRow: FC<{ order: QueryOrderVo }> = ({ order }) => {
  return (
    <div className={styles.orderInfoRow}>
      <span>退货单号:{order.qmApplyId || '-'}</span>
      <span>申请时间:{order.createTime}</span>
      <span>买家:{order.refundOrderVo?.buyerName}</span>
      <span>联系电话:{order.refundOrderVo?.buyerPhone}</span>
    </div>
  );
};

export default OrderInfoRow;

每一列的table组件OrderGoods/index.tsx
里层的表格项的宽度要和外层的列表项宽度保持一致
这里的table组件可以合并table表格项 可以看成是列表每一行都要渲染的table组件

import { DownOutlined, UpOutlined } from '@ant-design/icons';
import ProTable from '@ant-design/pro-table';
import { FC,useState } from 'react';
import { useColumns } from './goods.hooks';
import styles from './styles.less';
import { useModel } from 'umi';
const OrderGoods: FC<{ record: QueryOrderListVo }> = ({ record }) => {
  const { initialState } = useModel('@@initialState');
  const btnKeys = initialState?.currentPageBtnKeys || [];
  const { columns } = useColumns(record,btnKeys);
  const [status, setStatus] = useState(false);
  return (
    <div className={styles.after_goods}>
    <ProTable<OrderItem>
      options={false}
      每一列有多个商品可以收起的数据处理
      dataSource={status ? record.orderItemVos : record.orderItemVos?.slice(0, 1)}
      columns={columns}
      rowKey="goodsId"
      search={false}
      dateFormatter="string"
      showHeader={false}
      pagination={false}
      bordered
    />
      每一列有多个商品可以收起
      {
        (record.orderItemVos?.length||1)>1&&(
          <a className={styles.btn_arrow}  onClick={() => setStatus(!status)}>
            {status? <UpOutlined /> : <DownOutlined />}
          </a>
        )
      }
    
    </div>
  );
};

export default OrderGoods;

每一列的table组件的good.hook.tsx

import { defineProTableColumn } from '@/utils/defineProTableColumn';
import { Image, Space, Typography, Tooltip, Popconfirm } from 'antd';
import { loadAfterShopDetail, refuseUsing, store } from '../../store';
import styles from './styles.less';
const { Paragraph, Text } = Typography;
const loadColumns = (order: QueryOrderVo,keyStatus: string[],btnKeys:string[]) => {
  const _length = order.orderItemVos?.length || 1;
  return defineProTableColumn<OrderItem>([
    {
      title: '商品',
      key: 'goods',
      width: '20%',
      hideInSearch: true,
      render: (_, record) => {
        return (
          <div className={styles.goods}>
            <Image src={record.skuPic} width={54} height={54} />
            <Paragraph className={styles.name} ellipsis={{ rows: 2 }}>
              <Tooltip title={record.skuName}>
                <div className={styles.nameTitle}>{record.skuName}</div>
              </Tooltip>
              <div className={styles.norms}>规格: {record.specification || '暂无'}</div>
            </Paragraph>
          </div>
        );
      },
    },
    {
      title: '成交单价/数量',
      align: 'center',
      key: 'returnedNum',
      width: '15%',
      render: (_, record) => {
        return (
          <Space direction="vertical">
            <Text>¥{record.singleRefundableAmount}</Text>
            {record.retailPrice && (
              <Text type="secondary" delete>
                ¥{record.retailPrice}
              </Text>
            )}
            {!record.retailPrice && <Text>×{record.buyNum}</Text>}
          </Space>
        );
      },
    },
    {
      title: '退款金额',
      align: 'center',
      key: 'totalPrice',
      width: '10%',
      render: (_, __, index) => {
        合并表格行
        const obj = {
          children: <Text>¥{order.totalRefundPrice}</Text>,
          props: { rowSpan: index === 0 ? _length : 0 },
        };
        return obj;
      },
    },
    {
      title: '实退金额',
      align: 'center',
      key: 'actualRefundPrice',
      width: '10%',
      render: (_, __, index) => {
        const obj = {
          children: order.actualRefundPrice ? (
            <Text type={'warning'}>¥{order.actualRefundPrice}</Text>
          ) : (
            <Text>无</Text>
          ),
          props: { rowSpan: index === 0 ? _length : 0 },
        };
        return obj;
      },
    },
    {
      title: '订单来源',
      align: 'center',
      key: 'platform',
      width: '10%',
      render: (_, __, index) => {
        const obj = {
          children: <Text>{order.refundOrderVo?.orderSourceText}</Text>,
          props: { rowSpan: index === 0 ? _length : 0 },
        };
        return obj;
      },
    },
    {
      title: '售后类型',
      align: 'center',
      key: 'distribution',
      width: '10%',
      render: (_, __, index) => {
        合并表格行
        const obj = {
          children: <Text>{order.refundTypeText}</Text>,
          props: { rowSpan: index === 0 ? _length : 0 },
        };
        return obj;
      },
    },
    {
      title: '订单状态',
      align: 'center',
      key: 'refundStatus',
      width: '10%',
      render: (_, __, index) => {
        合并表格行
        const obj = {
          children: <Text>{order.refundStatusText}</Text>,
          props: { rowSpan: index === 0 ? _length : 0 },
        };
        return obj;
      },
    },
    {
      title: '操作',
      align: 'center',
      key: 'option',
      valueType: 'option',
      width: '15%',
      render: (_, __, index) => {
        const obj = {
          children: (
            <div>
              {btnKeys.includes('btn_merchant_unibuy_after_refund')&&keyStatus.includes('RETURN_APPLYING')&&<a
                onClick={() => {
                  store.onChangeDialogVisible(true);
                  store.onChangeAfterItem(order);
                }}
              >
                退款
              </a>}
              &nbsp;&nbsp;
              {btnKeys.includes('btn_merchant_unibuy_after_refuse')&&keyStatus.includes('RETURN_APPLYING')&&<Popconfirm
                key="delete"
                title="确定要拒绝该申请?"
                placement="topRight"
                onConfirm={() =>
                  refuseUsing({
                    applyId: order.qmApplyId,
                  })
                }
                okText="确定"
                cancelText="取消"
              >
                <a>拒绝</a>
              </Popconfirm>}
              &nbsp;&nbsp;
              <a
                onClick={() => {
                  loadAfterShopDetail(order);
                }}
              >
                查看详情
              </a>
            </div>
          ),
          props: { rowSpan: index === 0 ? _length : 0 },
        };
        return obj;
      },
    },
  ]);
};

/** 处理表格列 */
export function useColumns(record: QueryOrderListVo,btnKeys:string[]) {
  const {queryParams} = store.useState()
  return {
    columns: loadColumns(record,queryParams?.states,btnKeys),
  };
}

Antd组件

Map 地图编辑以及回显

根据输入的地址同步到地图上
地图组件CTMap

import { FC } from 'react';
import { Map, Marker } from 'react-amap';
const mapKey = 'b15fb269af7d8d61042e208f2cb3ef68';
export interface TCTMapValue {
  address?: string[];
  addressDetail?: string;
  longitude?: number;
  latitude?: number;
}
interface IMapComponentProps {
  onChange?: (value: TCTMapValue) => void;
  value?: TCTMapValue;
  height?: string;
  readonly?: boolean;
}
const CTMap: FC<IMapComponentProps> = ({ onChange, value, height = '300px', readonly = false }) => {
  const selectAddress = {
    created: (e: any) => {
      let auto;
      let geocoder;
      window.AMap.plugin('AMap.Autocomplete', () => {
        auto = new window.AMap.Autocomplete({ input: 'searchInput' });
      });

      window.AMap.plugin(['AMap.Geocoder'], function () {
        geocoder = new AMap.Geocoder({
          radius: 1000, //以已知坐标为中心点,radius为半径,返回范围内兴趣点和道路信息
          extensions: 'all', //返回地址描述以及附近兴趣点和道路信息,默认"base"
        });
      });

      window.AMap.plugin('AMap.PlaceSearch', () => {
        let place = new window.AMap.PlaceSearch({});
        window.AMap.event.addListener(auto, 'select', (e) => {
          place.search(e.poi.name);
          geocoder.getAddress(e.poi.location, function (status, result) {
            if (status === 'complete' && result.regeocode) {
              const data = result.regeocode.addressComponent;
              const name = data.township + data.street + data.streetNumber;
              onChange &&
                onChange({
                  address: [data.province, data.city, data.district],
                  addressDetail: name,
                  longitude: e.poi.location.lng,
                  latitude: e.poi.location.lat,
                });
            }
          });
        });
      });
    },
    click: (e) => {
      let geocoder;

      window.AMap.plugin(['AMap.Geocoder'], function () {
        geocoder = new AMap.Geocoder({
          radius: 1000, //以已知坐标为中心点,radius为半径,返回范围内兴趣点和道路信息
          extensions: 'all', //返回地址描述以及附近兴趣点和道路信息,默认"base"
        });
        geocoder.getAddress(e.lnglat, function (status, result) {
          if (status === 'complete' && result.regeocode) {
            const data = result.regeocode.addressComponent;
            const name = data.township + data.street + data.streetNumber;
            if (readonly) return;
            onChange &&
              onChange({
                address: [data.province, data.city, data.district],
                addressDetail: name,
                longitude: e.lnglat.lng,
                latitude: e.lnglat.lat,
              });
          }
        });
      });
    },
  };

  return (
    <div style={{ width: '100%', height }}>
      {value?.longitude ? (
        <Map
          amapkey={mapKey}
          plugins={['ToolBar']}
          events={selectAddress}
          center={[value?.longitude || 0, value?.latitude || 0]}
          zoom={13}
        >
          <Marker position={[value?.longitude || 0, value?.latitude || 0]} />
        </Map>
      ) : (
        <Map amapkey={mapKey} plugins={['ToolBar']} events={selectAddress} zoom={13}>
          <Marker position={[value?.longitude || 0, value?.latitude || 0]} />
        </Map>
      )}
    </div>
  );
};
export default CTMap;

使用这个组件

import { CTMap } from '@/components';
import type { TCTMapValue } from '@/components/CTMap';
import ProCard from '@ant-design/pro-card';
import ProForm, { ProFormText } from '@ant-design/pro-form';
import CHINA_REGION from '@province-city-china/level';
import { usePersistFn } from 'ahooks';
import type { FormInstance } from 'antd';
import { Button, Cascader, Space } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { useModel } from 'umi';
import { getDetails, store, updateCommunity } from '../../store';
import styles from './index.less';
interface IUpdateCommunityForm extends UpdateCommunityForm {
  address: string[];
}
export default () => {
  const { refresh } = useModel('@@initialState');
  const { dataObj, actionStatus } = store.useState();
  const formRef = useRef<FormInstance>();
  const [mapValues, setMapValues] = useState<TCTMapValue>({});

  useEffect(() => {
    if (!dataObj.communityId) return;
    const { storeName, province, city, area, addressDetail, telePhone, createTime, latitude, longitude } = dataObj;
    formRef?.current?.setFieldsValue({
      storeName,
      telePhone,
      createTime,
    });
    onMapChange({
      latitude,
      longitude,
      address: [province || '', city || '', area || ''],
      addressDetail: addressDetail || '',
    });
  }, [dataObj.communityId, actionStatus]);

  const onMapChange = usePersistFn((values: TCTMapValue) => {
    setMapValues(values);
    formRef?.current?.setFieldsValue({ address: values.address, addressDetail: values.addressDetail });
  });

  const onFinish = async (values: IUpdateCommunityForm) => {
    const address = values.address;
    updateCommunity({
      ...values,
      province: address[0],
      city: address.length === 2 ? address[0] : address[1],
      area: address.length === 2 ? address[1] : address[2],
      latitude: mapValues.latitude || 0,
      longitude: mapValues.longitude || 0,
    });
  };
  return (
    <ProCard bordered title="小区信息" headerBordered style={{ paddingBottom: '50px' }}>
      <ProForm<IUpdateCommunityForm>
        className={actionStatus ? styles.readonlyForm : ''}
        style={{ width: 400, padding: '0 16px' }}
        formRef={formRef}
        onFinish={onFinish}
        submitter={{
          render: () => {
            return [];
          },
        }}
      >
        <ProFormText
          rules={[
            {
              required: true,
              message: '请输入小区名称',
            },
            {
              pattern:
                /^[\u4E00-\u9FA5A-Za-z0-9`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]{2,20}$/,
              message: '请输入非空的2-20字小区名称',
            },
          ]}
          name="storeName"
          label="小区名称"
          placeholder="请输入"
          readonly={actionStatus}
        />
        {actionStatus ? (
          <ProFormText label="小区地址" readonly name="address" className="were" />
        ) : (
          <ProForm.Item
            name="address"
            label="小区地址"
            rules={[
              {
                required: true,
                message: '请选择小区地址',
              },
            ]}
          >
            <Cascader
              fieldNames={{ label: 'name', value: 'name' }}
              options={CHINA_REGION}
              placeholder="请选择省/市/区"
            />
          </ProForm.Item>
        )}
        {actionStatus && <ProFormText name="addressDetail" label="" readonly />}
        <ProFormText
          hidden={actionStatus}
          rules={[
            {
              required: true,
              message: '请输入详细地址',
            },
            {
              pattern:
                /^[\u4E00-\u9FA5A-Za-z0-9`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]{2,40}$/,
              message: '请输入非空的2-40字详细地址',
            },
          ]}
          name="addressDetail"
          label=""
          placeholder="请输入详细地址"
          fieldProps={{ id: 'searchInput' }}
        />

        <ProForm.Item label="地图定位">
          <CTMap onChange={onMapChange} value={mapValues} readonly={actionStatus} />
        </ProForm.Item>
        <ProFormText
          rules={[
            {
              pattern: /^[0-9-]*$/,
              message: '请输入正确的电话号格式',
            },
          ]}
          name="telePhone"
          label="物业电话"
          placeholder="请输入"
          readonly={actionStatus}
        />
        <ProFormText name="createTime" label="创建时间" readonly />
        <div className={styles.bottomBox}>
          <Space>
            {!actionStatus && (
              <Button
                onClick={() => {
                  store.onChangeActionStatus(true);
                  getDetails({ communityId: '' });
                }}
              >
                取消
              </Button>
            )}
            {actionStatus && (
              <Button
                type="primary"
                onClick={() => {
                  store.onChangeActionStatus(false);
                }}
              >
                编辑
              </Button>
            )}
            {!actionStatus && (
              <Button type="primary" htmlType="submit">
                保存
              </Button>
            )}
          </Space>
        </div>
      </ProForm>
    </ProCard>
  );
};

Tabs 动态显示切换栏的个数

image.png

/*
 * @Author: yang
 * @Date: 2021-11-01 18:03:59
 * @LastEditors: yang
 * @LastEditTime: 2021-11-17 16:55:37
 * @FilePath: \ct-admin-web\src\pages\order\sales\components\Logistics\index.tsx
 */
import ProTable from '@ant-design/pro-table';
import { Modal, Tabs, Timeline } from 'antd';
import { store } from '../../store';
import { useColumns } from './index.hooks';
import styles from './index.less';
const { TabPane } = Tabs;
const Logistics = () => {
  const { modalVisible, expressList } = store.useState();
  const { columns } = useColumns();
  return (
    <Modal
      title="物流信息"
      footer={
        [] // 设置footer为空,去掉 取消 确定默认按钮
      }
      width={600}
      onCancel={() => {
        store.changeModalVisible(false);
      }}
      className={styles.logistics}
      visible={modalVisible}
    >
      <Tabs defaultActiveKey="0" type="card" size="small">
        {expressList.length > 0 &&
          expressList.map((listItem, index) => {
            return (
              <TabPane
                tab={
                  index == 0
                    ? '包裹一'
                    : index == 1
                    ? '包裹二'
                    : index == 2
                    ? '包裹三'
                    : index == 3
                    ? '包裹四'
                    : index === 4
                    ? '包裹五'
                    : index === 5
                    ? '包裹六'
                    : index === 6
                    ? '包裹七'
                    : index === 7
                    ? '包裹八'
                    : index === 8
                    ? '包裹九'
                    : index === 9
                    ? '包裹十'
                    : ''
                }
                key={index}
              >
                <ProTable<ShipItemResLists>
                  options={false}
                  columns={columns}
                  dataSource={listItem.ship_item_res_lists.ship_item_res_lists}
                  className={styles.modalTable}
                  rowKey="item_id"
                  search={false}
                  pagination={false}
                  dateFormatter="string"
                />
                <div className={styles.timeline}>
                  <div className={styles.timeline_header}>
                    <span style={{ paddingLeft: 8 }}>{listItem.logistic.company}</span>
                    <span style={{ paddingLeft: 8 }}>{listItem.logistic.nu}</span>
                  </div>
                  <Timeline className={styles.timeline_body}>
                    {listItem.logistic.content_lists.content_lists.map((press, i) => {
                      return (
                        <Timeline.Item>
                          {press.context}
                          <br /> {press.time}
                        </Timeline.Item>
                      );
                    })}
                  </Timeline>
                </div>
              </TabPane>
            );
          })}
      </Tabs>
    </Modal>
  );
};

export default Logistics;

HappyCodingTop
526 声望847 粉丝

Talk is cheap, show the code!!