From 16e3efd6192fa5b4472493aed3ec375a6c5e6f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E5=B0=91?= <1769057083@qq.com> Date: Thu, 30 Dec 2021 17:16:40 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=20=E5=9F=8E=E5=B8=82=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8=E7=A7=BB=E5=8A=A8=E7=AB=AF=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E3=80=81=E7=A7=BB=E5=8A=A8=E7=AB=AF=E7=A6=81=E6=AD=A2=E8=A1=A8?= =?UTF-8?q?=E5=8D=95inline=E3=80=81=E5=BC=B9=E5=87=BA=E5=B1=82=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E8=B0=83=E6=95=B4=20(#3312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: useMobileUI接受位置修改 * fix:移动端禁用inline模式 * feat: 移动端城市选择器优化 * fix: 颜色选择器移动端禁止输入否则键盘和popup冲突 * fix: 弹出框高度调整 * fix: 去除标题 * fix: 禁止focus选中 * Update _picker-columns.scss * Update _picker-columns.scss Co-authored-by: zhangxulong --- scss/components/_city-area.scss | 27 ++ scss/components/form/_color.scss | 4 +- scss/components/form/_tree-select.scss | 2 +- scss/themes/_common.scss | 1 + src/components/CityArea.tsx | 316 +++++++++++++++ src/components/ColorPicker.tsx | 6 +- src/renderers/Form/CityDB.ts | 528 ++++++++++++++++++++++++- src/renderers/Form/InputCity.tsx | 22 +- src/renderers/Form/Item.tsx | 17 +- src/renderers/Form/TreeSelect.tsx | 10 +- 10 files changed, 914 insertions(+), 19 deletions(-) create mode 100644 scss/components/_city-area.scss create mode 100644 src/components/CityArea.tsx diff --git a/scss/components/_city-area.scss b/scss/components/_city-area.scss new file mode 100644 index 000000000..998d3d6ad --- /dev/null +++ b/scss/components/_city-area.scss @@ -0,0 +1,27 @@ +.#{$ns}CityArea { + &-popup { + height: px2rem(280px); + } + &-Input { + margin-top: var(--gap-xs); + outline: none; + vertical-align: top; + border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor); + border-radius: var(--Form-input-borderRadius); + line-height: var(--Form-input-lineHeight); + padding: var(--Form-input-paddingY) var(--Form-input-paddingX); + font-size: var(--Form-input-fontSize); + display: inline-flex !important; + + &::placeholder { + color: var(--Form-input-placeholderColor); + user-select: none; + } + + &:focus { + border-color: var(--Form-input-onFocused-borderColor); + box-shadow: var(--Form-input-boxShadow); + background: var(--Form-input-onFocused-bg); + } + } +} diff --git a/scss/components/form/_color.scss b/scss/components/form/_color.scss index 7e30965d3..65ecdd9c8 100644 --- a/scss/components/form/_color.scss +++ b/scss/components/form/_color.scss @@ -10,8 +10,8 @@ color: var(--ColorPicker-color); border-radius: var(--borderRadius); - &-popup{ - height: 80vh; + &-popup { + height: px2rem(400px); } &:not(.is-disabled) { diff --git a/scss/components/form/_tree-select.scss b/scss/components/form/_tree-select.scss index b1bfc70e8..93852768f 100644 --- a/scss/components/form/_tree-select.scss +++ b/scss/components/form/_tree-select.scss @@ -17,7 +17,7 @@ } &-popup { - height: 80vh; + height: px2rem(400px); } } diff --git a/scss/themes/_common.scss b/scss/themes/_common.scss index c7c956d54..8a3c14acb 100644 --- a/scss/themes/_common.scss +++ b/scss/themes/_common.scss @@ -82,6 +82,7 @@ @import '../components/form/checks'; @import '../components/form/selection'; @import '../components/form/city'; +@import '../components/city-area'; @import '../components/form/switch'; @import '../components/form/number'; @import '../components/form/select'; diff --git a/src/components/CityArea.tsx b/src/components/CityArea.tsx new file mode 100644 index 000000000..f76735a07 --- /dev/null +++ b/src/components/CityArea.tsx @@ -0,0 +1,316 @@ +/** + * @file 移动端城市选择器 + */ +import React, {useEffect, useState, memo} from 'react'; + +import Picker from './Picker'; +import ResultBox from './ResultBox'; +import {useSetState, useUpdateEffect} from '../hooks'; +import {localeable, LocaleProps} from '../locale'; +import {themeable, ThemeProps} from '../theme'; +import {Icon} from './icons'; +import {uncontrollable} from 'uncontrollable'; +import PopUp from './PopUp'; +import {PickerObjectOption} from './PickerColumn'; + +export type AreaColumnOption = { + text: string; + value: number; +}; + +export interface AreaProps extends LocaleProps, ThemeProps { + value: any; + /** + * 允许选择城市? + */ + allowCity?: boolean; + /** + * 允许选择地区? + */ + allowDistrict?: boolean; + /** + * 允许选择街道? + */ + allowStreet?: boolean; + /** + * 开启后只会存城市的 code 信息 + */ + extractValue?: boolean; + /** + * 是否将各个信息拼接成字符串。 + */ + joinValues?: boolean; + /** + * 拼接的符号是啥? + */ + delimiter?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + useMobileUI?: boolean; + onChange: (value: any) => void; + /** 点击完成按钮时触发 */ + onConfirm?: (result: AreaColumnOption[], index: number) => void; + /** 点击取消按钮时触发 */ + onCancel?: (...args: unknown[]) => void; +} +/** + * 街道 + */ +type district = { + [propName: number]: { + [propName: number]: Array; + }; +}; +interface DbState { + province: number[]; + district: district; + [key: number]: string; + city: { + [key: number]: number[]; + }; +} +interface StateObj { + columns: {options: Array}[]; +} + +const CityArea = memo(props => { + const { + joinValues = true, + extractValue = true, + delimiter = ',', + allowCity = true, + allowDistrict = true, + allowStreet = false, + // 默认北京东城区 + value = 110101, + classnames: cx, + translate: __, + disabled = false, + useMobileUI + } = props; + + const [values, setValues] = useState>([]); + const [street, setStreet] = useState(''); + const [confirmValues, setConfirmValues] = + useState>(); + const [db, updateDb] = useSetState(); + const [state, updateState] = useSetState({ + columns: [] + }); + const [isOpened, setIsOpened] = useState(false); + + const onChange = ( + columnValues: Array, + columnIndex: number + ) => { + values[columnIndex] = columnValues[columnIndex]?.value; + // 清空后面的值 + while (values[columnIndex++]) { + values[columnIndex++] = -1; + } + let [provience, city, district] = values; + if (city === -1) { + city = db.city?.[provience]?.[0]; + } + if (district === -1) { + district = db.district?.[provience]?.[city]?.[0]; + } + let tempValues = [provience, city, district]; + if (!allowDistrict) { + tempValues.splice(2, 1); + } + if (!allowCity) { + tempValues.splice(1, 1); + } + setValues(tempValues); + }; + + const propsChange = () => { + const {onChange} = props; + const [province, city, district] = values; + const code = + allowDistrict && district + ? district + : allowCity && city + ? city + : province; + if (typeof extractValue === 'undefined' ? joinValues : extractValue) { + code + ? onChange( + allowStreet && street + ? [code, street].join(delimiter) + : String(code) + ) + : onChange(''); + } else { + onChange({ + code, + province: db[province], + city: db[city], + district: db[district], + street + }); + } + }; + + const onConfirm = () => { + const confirmValues = values.map((item: number) => ({ + text: db[item], + value: item + })); + setConfirmValues(confirmValues); + propsChange(); + setIsOpened(false); + }; + + const onCancel = () => { + setIsOpened(false); + if (props.onCancel) props.onCancel(); + }; + + const getPropsValue = () => { + // 最后一项的值 + let code = + (value && value.code) || + (typeof value === 'number' && value) || + (typeof value === 'string' && /(\d{6})/.test(value) && RegExp.$1); + const values: Array = []; + if (code && db[code]) { + code = parseInt(code, 10); + let provinceCode = code - (code % 10000); + let cityCode = code - (code % 100); + if (db[provinceCode]) { + values[0] = provinceCode; + } + if (db[cityCode] && allowCity) { + values[1] = cityCode; + } else if (~db.city[provinceCode]?.indexOf(code) && allowCity) { + values[1] = code; + } + + if (code % 100 && allowDistrict) { + values[2] = code; + } + setValues(values); + } + }; + + const updateColumns = () => { + if (!db) { + return; + } + let [provience, city, district] = values; + const provienceColumn = db.province.map((code: number) => { + return {text: db[code], value: code, disabled}; + }); + const cityColumn = city + ? db.city[provience].map((code: number) => { + return {text: db[code], value: code, disabled}; + }) + : []; + const districtColumn = + city && district + ? db.district[provience][city].map((code: number) => { + return {text: db[code], value: code, disabled}; + }) + : []; + const columns = [ + {options: provienceColumn}, + {options: cityColumn}, + {options: districtColumn} + ]; + if (!allowDistrict || !allowCity) { + columns.splice(2, 1); + } + if (!allowCity) { + columns.splice(1, 1); + } + updateState({columns}); + }; + + const loadDb = () => { + import('../renderers/Form/CityDB').then(db => { + updateDb({ + ...db.default, + province: db.province as any, + city: db.city, + district: db.district as district + }); + }); + }; + + useEffect(() => { + loadDb(); + }, []); + + useEffect(() => { + isOpened && db && getPropsValue(); + }, [db, isOpened]); + + useEffect(() => { + street && propsChange(); + }, [street]); + + useUpdateEffect(() => { + values.length && updateColumns(); + }, [values]); + + const result = confirmValues + ?.filter(item => item?.value) + ?.map(item => item.text) + .join(delimiter); + + return ( +
+ {}} + onResultClick={() => setIsOpened(!isOpened)} + placeholder={__('Condition.cond_placeholder')} + useMobileUI={useMobileUI} + > + {allowStreet && values[0] ? ( + ) => + setStreet(e.currentTarget.value) + } + placeholder={__('City.street')} + disabled={disabled} + /> + ) : null} + + + +
+ ); +}); + +export default themeable( + localeable( + uncontrollable(CityArea, { + value: 'onChange' + }) + ) +); diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 0f30f569d..f06a119a3 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -227,6 +227,7 @@ export class ColorControl extends React.PureComponent< const __ = this.props.translate; const isOpened = this.state.isOpened; const isFocused = this.state.isFocused; + const mobileUI = useMobileUI && isMobile(); return (
{clearable && !disabled && value ? ( @@ -273,7 +275,7 @@ export class ColorControl extends React.PureComponent< - {!(useMobileUI && isMobile()) && isOpened ? ( + {!mobileUI && isOpened ? ( findDOMNode(this)} @@ -320,7 +322,7 @@ export class ColorControl extends React.PureComponent< ) : null} - {useMobileUI && isMobile() && ( + {mobileUI && ( = []; diff --git a/src/renderers/Form/InputCity.tsx b/src/renderers/Form/InputCity.tsx index 72b13f937..46c87c111 100644 --- a/src/renderers/Form/InputCity.tsx +++ b/src/renderers/Form/InputCity.tsx @@ -3,7 +3,8 @@ import {FormItem, FormControlProps, FormBaseControl} from './Item'; import {ClassNamesFn, themeable, ThemeProps} from '../../theme'; import Spinner from '../../components/Spinner'; import Select from '../../components/Select'; -import {autobind} from '../../utils/helper'; +import CityArea from '../../components/CityArea'; +import {autobind, isMobile} from '../../utils/helper'; import {Option} from './Options'; import {localeable, LocaleProps} from '../../locale'; @@ -65,6 +66,7 @@ export interface CityPickerProps allowCity: boolean; allowDistrict: boolean; allowStreet: boolean; + useMobileUI?: boolean; } export interface CityPickerState { @@ -428,9 +430,23 @@ export class LocationControl extends React.Component { joinValues, allowStreet, disabled, - searchable + searchable, + useMobileUI } = this.props; - return ( + const mobileUI = useMobileUI && isMobile(); + return mobileUI ? ( + + ) : ( { sizeMutable, size, defaultSize, + useMobileUI, ...rest } = this.props; + const mobileUI = useMobileUI && isMobile(); if (renderControl) { const controlSize = size || defaultSize; return renderControl({ @@ -470,7 +478,7 @@ export class FormItemWrap extends React.Component { className: cx( `Form-control`, { - 'is-inline': !!rest.inline, + 'is-inline': !!rest.inline && !mobileUI, 'is-error': model && !model.valid, [`Form-control--withSize Form-control--size${ucFirst( controlSize @@ -1205,11 +1213,12 @@ export function asFormItem(config: Omit) { type, size, defaultSize, + useMobileUI, ...rest } = this.props; const controlSize = size || defaultSize; - + const mobileUI = useMobileUI && isMobile(); return ( ) { className={cx( `Form-control`, { - 'is-inline': !!rest.inline, + 'is-inline': !!rest.inline && !mobileUI, 'is-error': model && !model.valid, [`Form-control--withSize Form-control--size${ucFirst( controlSize diff --git a/src/renderers/Form/TreeSelect.tsx b/src/renderers/Form/TreeSelect.tsx index 1aad88e75..f6ad8e46e 100644 --- a/src/renderers/Form/TreeSelect.tsx +++ b/src/renderers/Form/TreeSelect.tsx @@ -568,12 +568,12 @@ export default class TreeSelectControl extends React.Component< selectedOptions, placeholder, popOverContainer, - env, + useMobileUI, translate: __ } = this.props; const {isOpened} = this.state; - const {useMobileUI} = env; + const mobileUI = useMobileUI && isMobile(); return (
{loading ? : undefined} - {!(useMobileUI && isMobile()) && isOpened ? ( + {!mobileUI && isOpened ? ( this.container.current)} target={() => this.target} @@ -631,7 +631,7 @@ export default class TreeSelectControl extends React.Component< ) : null} - {useMobileUI && isMobile() && ( + {mobileUI ? ( {this.renderOuter()} - )} + ) : null}
); }