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>
這個元件是醬紫的~
時間元件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;
}
}
}
}
ProTable(一)
- 直接跳過一般進高階,完成的是下面圖片的效果分為: 左邊分類, 批量操作, 子table展示sku(顏色、規格)*
<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),
};
}
表格渲染的部分這裡就完結了,但是我還是想說一下設價的這個邏輯
設價
批量設價,以及狀態資料的處理
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結合的結構
可以把整個看成一個列表,列表又可以分為每一項列表行的表單以及表單的文字
它的搜尋,表格與表格頂上的文字都是分開的元件
最外層的父元素 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>}
{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>}
<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 動態顯示切換欄的個數
/*
* @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;