refactor: Area component

This commit is contained in:
chenjiahan 2022-01-21 10:49:08 +08:00 committed by neverland
parent 113e1b7a62
commit cca428aac6
5 changed files with 133 additions and 284 deletions

View File

@ -2,16 +2,12 @@ import {
ref,
watch,
computed,
reactive,
nextTick,
onMounted,
defineComponent,
type PropType,
type ExtractPropTypes,
} from 'vue';
// Utils
import { deepClone } from '../utils/deep-clone';
import {
pick,
extend,
@ -20,19 +16,16 @@ import {
createNamespace,
} from '../utils';
import { pickerSharedProps } from '../picker/Picker';
// Composables
import { useExpose } from '../composables/use-expose';
import { formatDataForCascade } from './utils';
// Components
import { Picker, PickerInstance } from '../picker';
import { Picker } from '../picker';
// Types
import type { AreaList, AreaColumnType, AreaColumnOption } from './types';
import type { AreaList } from './types';
const [name, bem] = createNamespace('area');
const EMPTY_CODE = '000000';
const INHERIT_SLOTS = [
'title',
'cancel',
@ -52,20 +45,14 @@ const INHERIT_PROPS = [
'confirmButtonText',
] as const;
const isOverseaCode = (code: string) => code[0] === '9';
const areaProps = extend({}, pickerSharedProps, {
value: String,
modelValue: String,
columnsNum: makeNumericProp(3),
columnsPlaceholder: makeArrayProp<string>(),
areaList: {
type: Object as PropType<AreaList>,
default: () => ({}),
},
isOverseaCode: {
type: Function as PropType<(code: string) => boolean>,
default: isOverseaCode,
},
});
export type AreaProps = ExtractPropTypes<typeof areaProps>;
@ -75,262 +62,62 @@ export default defineComponent({
props: areaProps,
emits: ['change', 'confirm', 'cancel'],
emits: ['change', 'confirm', 'cancel', 'update:modelValue'],
setup(props, { emit, slots }) {
const pickerRef = ref<PickerInstance>();
const codes = ref<string[]>([]);
const state = reactive({
code: props.value,
columns: [{ values: [] }, { values: [] }, { values: [] }],
});
const areaList = computed(() => {
const { areaList } = props;
return {
province: areaList.province_list || {},
city: areaList.city_list || {},
county: areaList.county_list || {},
};
});
const placeholderMap = computed(() => {
const { columnsPlaceholder } = props;
return {
province: columnsPlaceholder[0] || '',
city: columnsPlaceholder[1] || '',
county: columnsPlaceholder[2] || '',
};
});
const getDefaultCode = () => {
if (props.columnsPlaceholder.length) {
return EMPTY_CODE;
}
const { county, city } = areaList.value;
const countyCodes = Object.keys(county);
if (countyCodes[0]) {
return countyCodes[0];
}
const cityCodes = Object.keys(city);
if (cityCodes[0]) {
return cityCodes[0];
}
return '';
};
const getColumnValues = (type: AreaColumnType, code?: string) => {
let column: AreaColumnOption[] = [];
if (type !== 'province' && !code) {
return column;
}
const list = areaList.value[type];
column = Object.keys(list).map((listCode) => ({
code: listCode,
name: list[listCode],
}));
if (code) {
// oversea code
if (type === 'city' && props.isOverseaCode(code)) {
code = '9';
}
column = column.filter((item) => item.code.indexOf(code!) === 0);
}
if (placeholderMap.value[type] && column.length) {
// set columns placeholder
let codeFill = '';
if (type === 'city') {
codeFill = EMPTY_CODE.slice(2, 4);
} else if (type === 'county') {
codeFill = EMPTY_CODE.slice(4, 6);
}
column.unshift({
code: code + codeFill,
name: placeholderMap.value[type],
});
}
return column;
};
// get index by code
const getIndex = (type: AreaColumnType, code: string) => {
let compareNum = code.length;
if (type === 'province') {
compareNum = props.isOverseaCode(code) ? 1 : 2;
}
if (type === 'city') {
compareNum = 4;
}
code = code.slice(0, compareNum);
const list = getColumnValues(
type,
compareNum > 2 ? code.slice(0, compareNum - 2) : ''
);
for (let i = 0; i < list.length; i++) {
if (list[i].code.slice(0, compareNum) === code) {
return i;
}
}
return 0;
};
const setValues = () => {
const picker = pickerRef.value;
if (!picker) {
return;
}
let code = state.code || getDefaultCode();
const province = getColumnValues('province');
const city = getColumnValues('city', code.slice(0, 2));
picker.setColumnValues(0, province);
picker.setColumnValues(1, city);
if (
city.length &&
code.slice(2, 4) === '00' &&
!props.isOverseaCode(code)
) {
[{ code }] = city;
}
picker.setColumnValues(2, getColumnValues('county', code.slice(0, 4)));
picker.setIndexes([
getIndex('province', code),
getIndex('city', code),
getIndex('county', code),
]);
};
// parse output columns data
const parseValues = (values: AreaColumnOption[]) =>
values.map((value, index) => {
if (value) {
value = deepClone(value);
if (!value.code || value.name === props.columnsPlaceholder[index]) {
value.code = '';
value.name = '';
}
}
return value;
});
const getValues = () => {
if (pickerRef.value) {
const values = pickerRef.value
.getValues<AreaColumnOption>()
.filter(Boolean);
return parseValues(values);
}
return [];
};
const getArea = () => {
const values = getValues();
const area = {
code: '',
country: '',
province: '',
city: '',
county: '',
};
if (!values.length) {
return area;
}
const names = values.map((item) => item.name);
const validValues = values.filter((value) => value.code);
area.code = validValues.length
? validValues[validValues.length - 1].code
: '';
if (props.isOverseaCode(area.code)) {
area.country = names[1] || '';
area.province = names[2] || '';
} else {
area.province = names[0] || '';
area.city = names[1] || '';
area.county = names[2] || '';
}
return area;
};
const reset = (newCode = '') => {
state.code = newCode;
setValues();
};
const onChange = (values: AreaColumnOption[], index: number) => {
state.code = values[index].code;
setValues();
if (pickerRef.value) {
const parsedValues = parseValues(pickerRef.value.getValues());
emit('change', parsedValues, index);
}
};
const onConfirm = (values: AreaColumnOption[], index: number) => {
setValues();
emit('confirm', parseValues(values), index);
};
const columns = computed(() =>
formatDataForCascade(
props.areaList,
props.columnsNum,
props.columnsPlaceholder
)
);
const onChange = (...args: unknown[]) => emit('change', ...args);
const onCancel = (...args: unknown[]) => emit('cancel', ...args);
onMounted(setValues);
const onConfirm = (...args: unknown[]) => emit('confirm', ...args);
watch(
() => props.value,
(value) => {
state.code = value;
setValues();
}
codes,
(newCodes) => {
const lastCode = newCodes.length ? newCodes[newCodes.length - 1] : '';
if (lastCode && lastCode !== props.modelValue) {
emit('update:modelValue', lastCode);
}
},
{ deep: true }
);
watch(() => props.areaList, setValues, { deep: true });
watch(
() => props.columnsNum,
() => nextTick(setValues)
() => props.modelValue,
(newCode) => {
const lastCode = codes.value.length
? codes.value[codes.value.length - 1]
: '';
if (newCode && newCode !== lastCode) {
codes.value = [
`${newCode.slice(0, 2)}0000`,
`${newCode.slice(0, 4)}00`,
newCode,
].slice(0, +props.columnsNum);
}
},
{ immediate: true }
);
useExpose({ reset, getArea, getValues });
return () => {
const columns = state.columns.slice(0, +props.columnsNum);
return (
<Picker
v-slots={pick(slots, INHERIT_SLOTS)}
ref={pickerRef}
class={bem()}
columns={columns}
columnsFieldNames={{ text: 'name' }}
onChange={onChange}
onCancel={onCancel}
onConfirm={onConfirm}
{...pick(props, INHERIT_PROPS)}
/>
);
};
return () => (
<Picker
v-model={codes.value}
v-slots={pick(slots, INHERIT_SLOTS)}
class={bem()}
columns={columns.value}
onChange={onChange}
onCancel={onCancel}
onConfirm={onConfirm}
{...pick(props, INHERIT_PROPS)}
/>
);
},
});

View File

@ -27,7 +27,7 @@ const value = ref('330302');
<template>
<demo-block card :title="t('basicUsage')">
<van-area :title="t('title')" :area-list="t('areaList')" />
<van-area v-model="value" :title="t('title')" :area-list="t('areaList')" />
</demo-block>
<demo-block card :title="t('title2')">

View File

@ -4,7 +4,7 @@ import _Area from './Area';
export const Area = withInstall(_Area);
export default Area;
export type { AreaProps } from './Area';
export type { AreaList, AreaInstance, AreaColumnOption } from './types';
export type { AreaList, AreaInstance } from './types';
declare module 'vue' {
export interface GlobalComponents {

View File

@ -8,23 +8,4 @@ export type AreaList = {
province_list: Record<string, string>;
};
export type AreaColumnOption = {
name: string;
code: string;
};
export type AreaColumnType = 'province' | 'county' | 'city';
export type AreaExpose = {
reset: (newCode?: string) => void;
getArea: () => {
code: string;
country: string;
province: string;
city: string;
county: string;
};
getValues: () => AreaColumnOption[];
};
export type AreaInstance = ComponentPublicInstance<AreaProps, AreaExpose>;
export type AreaInstance = ComponentPublicInstance<AreaProps>;

View File

@ -0,0 +1,81 @@
import type { AreaList } from '.';
import type { PickerOption } from '../picker';
const EMPTY_CODE = '000000';
const makeOption = (
text = '',
value = EMPTY_CODE,
children?: PickerOption[]
): PickerOption => ({
text,
value,
children,
});
export function formatDataForCascade(
{ city_list: city, county_list: county, province_list: province }: AreaList,
columnsNum: number | string,
placeholder: string[]
) {
const showCity = columnsNum > 1;
const showCounty = columnsNum > 2;
const getCityChildren = () => {
if (showCounty) {
return placeholder.length ? [makeOption(placeholder[1])] : [];
}
};
const getProvinceChildren = () => {
if (showCity) {
return placeholder.length
? [makeOption(placeholder[0], EMPTY_CODE, showCounty ? [] : undefined)]
: [];
}
};
const provinceMap = new Map<string, PickerOption>();
Object.keys(province).forEach((code) => {
provinceMap.set(
code.slice(0, 2),
makeOption(province[code], code, getProvinceChildren())
);
});
const cityMap = new Map<string, PickerOption>();
if (showCity) {
Object.keys(city).forEach((code) => {
const option = makeOption(city[code], code, getCityChildren());
cityMap.set(code.slice(0, 4), option);
const province = provinceMap.get(code.slice(0, 2));
if (province) {
province.children!.push(option);
}
});
}
if (showCounty) {
Object.keys(county).forEach((code) => {
const city = cityMap.get(code.slice(0, 4));
if (city) {
city.children!.push(makeOption(county[code], code));
}
});
}
const options = Array.from(provinceMap.values()) as PickerOption[];
if (placeholder.length) {
const county = showCounty
? [makeOption(placeholder[2], EMPTY_CODE)]
: undefined;
const city = showCity
? [makeOption(placeholder[1], EMPTY_CODE, county)]
: undefined;
options.unshift(makeOption(placeholder[0], EMPTY_CODE, city));
}
return options;
}