使用ProComponents和Antd的一些筆記

HappyCodingTop發表於2021-12-06

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;

相關文章